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 |
|
|
true |
|
|
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... })
item.service.ts -- 5 exported functions (getAllItems, getItemById, createItem, updateItem, duplicateItem, deleteItem):
getAllItems:async, remove.all(),return await db.select()...getItemById:async, replace.get() ?? nullwithconst [row] = await db.select()...; return row ?? nullcreateItem:async, replace.returning().get()withconst [row] = await db.insert()...returning(); return rowupdateItem:async, existence check uses destructureconst [existing] = await db.select()..., update usesconst [row] = await db.insert()...returning(); return rowduplicateItem:async, same pattern as createItemdeleteItem:async, existence checkconst [item] = await db.select()..., deleteawait 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 resolveThreadtransaction:await db.transaction(async (tx) => { ... })with all inner operations awaitedunresolveThreadtransaction: 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:
deleteCategorycontainsawait db.transaction(async (tx) => - thread.service.ts:
resolveThreadandunresolveThreadcontainawait db.transaction(async (tx) => - setup.service.ts:
updateSetupItemscontainsawait 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.
- item.service.ts: every exported function starts with
oauth.service.ts -- OAuth client, code, token management:
- All functions:
async - Same conversion patterns
- IMPORTANT: The
usedcolumn onoauthCodesis nowbooleantype. 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()toawait 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
seedDefaultsimport 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.
<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`