---
phase: 14-postgresql-migration
plan: 03
type: execute
wave: 2
depends_on: [14-01]
files_modified:
- 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
autonomous: true
requirements: [DB-01, DB-02]
must_haves:
truths:
- "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"
artifacts:
- path: "src/server/services/item.service.ts"
provides: "Async item CRUD operations"
contains: "async function"
- path: "src/server/services/thread.service.ts"
provides: "Async thread operations with async transactions"
contains: "async (tx)"
- path: "src/server/index.ts"
provides: "Async server startup with seed"
contains: "await seedDefaults"
key_links:
- from: "src/server/services/*.ts"
to: "src/db/schema.ts"
via: "import table definitions"
pattern: "from.*db/schema"
- from: "src/server/index.ts"
to: "src/db/seed.ts"
via: "await seedDefaults()"
pattern: "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.
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
@.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"
- 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
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:
```typescript
// 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:**
```bash
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"
- 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
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
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.