Files
GearBox/.planning/phases/14-postgresql-migration/14-03-PLAN.md

12 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
14-postgresql-migration 03 execute 2
14-01
src/server/services/item.service.ts
src/server/services/category.service.ts
src/server/services/thread.service.ts
src/server/services/setup.service.ts
src/server/services/auth.service.ts
src/server/services/oauth.service.ts
src/server/services/image.service.ts
src/server/services/csv.service.ts
src/server/services/totals.service.ts
src/server/index.ts
true
DB-01
DB-02
truths artifacts key_links
Every service function is async and awaits all database calls
No .all(), .get(), or .run() SQLite-only methods remain in any service
Transactions use async callbacks with await on inner operations
Server startup awaits async seed function
path provides contains
src/server/services/item.service.ts Async item CRUD operations async function
path provides contains
src/server/services/thread.service.ts Async thread operations with async transactions async (tx)
path provides contains
src/server/index.ts Async server startup with seed await seedDefaults
from to via pattern
src/server/services/*.ts src/db/schema.ts import table definitions from.*db/schema
from to via pattern
src/server/index.ts src/db/seed.ts await seedDefaults() await seedDefaults
Convert all 9 service files from synchronous SQLite operations to async PostgreSQL operations. Update server startup to await async seed.

Purpose: Services are the data access layer. Every database call must be async for postgres.js. This is the bulk of the mechanical conversion work (~82 call sites per the research). Output: All service files use async/await. Server index awaits seed.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/14-postgresql-migration/14-CONTEXT.md @.planning/phases/14-postgresql-migration/14-RESEARCH.md @.planning/phases/14-postgresql-migration/14-01-SUMMARY.md

@src/db/schema.ts @src/db/index.ts

Conversion rules (apply to ALL service files):

  • function foo(db) -> async function foo(db)
  • .all() -> remove (await the query directly, returns array)
  • .get() -> destructure: const [row] = await db.select()...
  • .run() -> remove (await the query directly)
  • .returning().get() -> const [row] = await db.insert()...returning()
  • db.transaction(() => { ... }) -> await db.transaction(async (tx) => { await tx... })
Task 1: Convert core data services to async (item, category, thread, setup, totals) src/server/services/item.service.ts, src/server/services/category.service.ts, src/server/services/thread.service.ts, src/server/services/setup.service.ts, src/server/services/totals.service.ts src/server/services/item.service.ts, src/server/services/category.service.ts, src/server/services/thread.service.ts, src/server/services/setup.service.ts, src/server/services/totals.service.ts Convert each service file following the async conversion rules. Read each file fully before modifying.

item.service.ts -- 5 exported functions (getAllItems, getItemById, createItem, updateItem, duplicateItem, deleteItem):

  • getAllItems: async, remove .all(), return await db.select()...
  • getItemById: async, replace .get() ?? null with const [row] = await db.select()...; return row ?? null
  • createItem: async, replace .returning().get() with const [row] = await db.insert()...returning(); return row
  • updateItem: async, existence check uses destructure const [existing] = await db.select()..., update uses const [row] = await db.insert()...returning(); return row
  • duplicateItem: async, same pattern as createItem
  • deleteItem: async, existence check const [item] = await db.select()..., delete await db.delete()...

category.service.ts -- Has a transaction in deleteCategory (moves items to Uncategorized then deletes):

  • All functions: async
  • Transaction: await db.transaction(async (tx) => { await tx.update()...; await tx.delete()...; })
  • All .all() -> remove, .get() -> destructure, .run() -> remove

thread.service.ts -- Has transactions in resolveThread and unresolveThread:

  • All functions: async
  • resolveThread transaction: await db.transaction(async (tx) => { ... }) with all inner operations awaited
  • unresolveThread transaction: same pattern
  • .all() -> remove, .get() -> destructure, .run() -> remove
  • .returning().get() -> const [row] = await ...returning()

setup.service.ts -- Has a transaction in updateSetupItems (delete all + re-insert):

  • All functions: async
  • Transaction: await db.transaction(async (tx) => { await tx.delete()...; for (const item of items) { await tx.insert()...; } })
  • .all() -> remove, .get() -> destructure, .run() -> remove

totals.service.ts -- Read-only aggregate queries:

  • All functions: async
  • Remove .all(), .get() -> destructure ! grep -n ".all()|.get()|.run()" src/server/services/item.service.ts src/server/services/category.service.ts src/server/services/thread.service.ts src/server/services/setup.service.ts src/server/services/totals.service.ts && grep -c "async function" src/server/services/item.service.ts | grep -q "[3-9]" && bun run lint 2>&1 | tail -3 && echo "PASS" || echo "FAIL" <acceptance_criteria>
    • item.service.ts: every exported function starts with export async function
    • item.service.ts: does NOT contain .all(), .get(), or .run()
    • category.service.ts: deleteCategory contains await db.transaction(async (tx) =>
    • thread.service.ts: resolveThread and unresolveThread contain await db.transaction(async (tx) =>
    • setup.service.ts: updateSetupItems contains await db.transaction(async (tx) =>
    • totals.service.ts: every exported function is async
    • No file in this set contains .all(), .get(), or .run() calls on db/tx objects </acceptance_criteria> Core data services (item, category, thread, setup, totals) fully converted to async with all SQLite-only methods removed.
Task 2: Convert auth/oauth/csv/image services, update server index, and run PGlite smoke test src/server/services/auth.service.ts, src/server/services/oauth.service.ts, src/server/services/csv.service.ts, src/server/services/image.service.ts, src/server/index.ts src/server/services/auth.service.ts, src/server/services/oauth.service.ts, src/server/services/csv.service.ts, src/server/services/image.service.ts, src/server/index.ts **auth.service.ts** -- User and session management: - All functions: `async` - Remove `.all()`, `.get()` -> destructure, `.run()` -> remove - `.returning().get()` -> `const [row] = await ...returning()` - Pay attention to boolean checks on `oauthCodes.used` -- the column is now native `boolean` (true/false), not integer (0/1). If any code checks `=== 0` or `=== 1` for the `used` field, change to `=== false` or `=== true`.

oauth.service.ts -- OAuth client, code, token management:

  • All functions: async
  • Same conversion patterns
  • IMPORTANT: The used column on oauthCodes is now boolean type. Any .set({ used: 1 }) must become .set({ used: true }). Any .where(eq(oauthCodes.used, 0)) must become .where(eq(oauthCodes.used, false)).

csv.service.ts -- CSV export:

  • All functions: async
  • This is read-only, straightforward .all() removal

image.service.ts -- Image handling:

  • All functions: async
  • Same conversion patterns. May have fewer DB calls than other services.

src/server/index.ts -- Server startup:

  • Change seedDefaults() to await seedDefaults() at the top level
  • Since the file is a module (ESM), top-level await is supported. Wrap the seed call:
    // Seed default data on startup
    await seedDefaults();
    
  • If the file structure does not support top-level await cleanly (e.g., exports are synchronous), wrap in an async IIFE or move the await before the export.
  • The seedDefaults import already points to the async version from Plan 01.

After all conversions, run a PGlite smoke test to verify at least one service works end-to-end:

bun -e "
import { createTestDb } from './tests/helpers/db.ts';
import * as schema from './src/db/schema.ts';
const db = await createTestDb();
// Test a basic item service operation
const { createItem } = await import('./src/server/services/item.service.ts');
const [cat] = await db.select().from(schema.categories);
const item = await createItem(db as any, { name: 'Smoke Test', categoryId: cat.id, quantity: 1 });
if (!item || !item.id) { console.error('FAIL: createItem returned no result'); process.exit(1); }
console.log('Service smoke test PASSED: item created with id', item.id);
"

This validates that the async conversion is actually functional, not just structurally correct. ! grep -n ".all()|.get()|.run()" src/server/services/auth.service.ts src/server/services/oauth.service.ts src/server/services/csv.service.ts src/server/services/image.service.ts && grep -q "await seedDefaults" src/server/index.ts && bun run lint 2>&1 | tail -3 && echo "PASS" || echo "FAIL" <acceptance_criteria> - auth.service.ts: every exported function is async - auth.service.ts: does NOT contain .all(), .get(), or .run() - oauth.service.ts: every exported function is async - oauth.service.ts: does NOT contain .set({ used: 1 }) -- uses .set({ used: true }) instead - oauth.service.ts: does NOT contain eq(oauthCodes.used, 0) -- uses eq(oauthCodes.used, false) instead - csv.service.ts: every exported function is async, no .all() calls - image.service.ts: every exported function is async - src/server/index.ts: contains await seedDefaults() - No file in this set contains .all(), .get(), or .run() calls on db objects - PGlite smoke test creating an item via service function exits 0 </acceptance_criteria> Auth, OAuth, CSV, and image services fully async. OAuth boolean conversion complete. Server startup awaits async seed. PGlite smoke test confirms services work against async DB.

- `grep -rn "\.all()\|\.get()\|\.run()" src/server/services/` returns NO matches (except possibly string literals in error messages) - `grep -c "async function" src/server/services/*.ts` shows every service has async functions - `grep "await seedDefaults" src/server/index.ts` returns a match - `bun run lint` passes - PGlite smoke test exits 0

<success_criteria> All 9 service files use async/await for every database operation. No SQLite-only methods (.all, .get, .run) remain. Transactions use async callbacks. OAuth boolean conversion complete. Server index awaits async seed. PGlite smoke test validates at least one service works end-to-end. Lint passes. </success_criteria>

After completion, create `.planning/phases/14-postgresql-migration/14-03-SUMMARY.md`