--- 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. After completion, create `.planning/phases/14-postgresql-migration/14-03-SUMMARY.md`