203 lines
11 KiB
Markdown
203 lines
11 KiB
Markdown
---
|
|
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]" && 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 and update server index</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.
|
|
</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 && 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
|
|
</acceptance_criteria>
|
|
<done>Auth, OAuth, CSV, and image services fully async. OAuth boolean conversion complete. Server startup awaits async seed.</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
|
|
</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.
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/14-postgresql-migration/14-03-SUMMARY.md`
|
|
</output>
|