---
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"
---

<objective>
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.
</objective>

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

<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
</context>

<interfaces>
<!-- Db type will change after Plan 01. Services use `type Db = typeof prodDb` which will now be a PostgresJsDatabase instance. -->
<!-- Key pattern: all services take `db: Db = prodDb` as first parameter -->
<!-- After Plan 01, src/db/index.ts exports: `export const db = drizzle(queryClient, { schema })` from postgres-js driver -->

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... })`
</interfaces>

<tasks>

<task type="auto">
  <name>Task 1: Convert core data services to async (item, category, thread, setup, totals)</name>
  <files>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</files>
  <read_first>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</read_first>
  <action>
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
  </action>
  <verify>
    <automated>! 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"</automated>
  </verify>
  <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>
  <done>Core data services (item, category, thread, setup, totals) fully converted to async with all SQLite-only methods removed.</done>
</task>

<task type="auto">
  <name>Task 2: Convert auth/oauth/csv/image services, update server index, and run PGlite smoke test</name>
  <files>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</files>
  <read_first>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</read_first>
  <action>
**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.
  </action>
  <verify>
    <automated>! 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"</automated>
  </verify>
  <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>
  <done>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.</done>
</task>

</tasks>

<verification>
- `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
</verification>

<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>

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