feat: add stock database with prisma for portfolio persistence

- Initialize Prisma with SQLite and Stock model
- Create database service layer with singleton client
- Add API routes for stock CRUD operations
- Integrate database with analyze page to persist ticker entries
- Add Playwright tests for stock database functionality
This commit is contained in:
2026-05-14 10:23:56 +02:00
parent f40eec1420
commit 3340fd11ca
29 changed files with 2530 additions and 221 deletions
@@ -0,0 +1,247 @@
# Stock Portfolio Database Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Store manually added tickers in SQLite database using Prisma ORM with full CRUD operations and tests.
**Architecture:** Use Prisma ORM with SQLite to persist stock tickers added via the analyze page. Each stock entry stores ticker symbol, optional notes, and timestamps. The analyze route fetches stored tickers from DB and merges with Alpaca positions.
**Tech Stack:** Prisma ORM, SQLite, TypeScript, React Router 7
---
## Prerequisites Check
- [ ] Check if Prisma is already installed in package.json
- [ ] Check existing database/schema files
---
### Task 1: Initialize Prisma and Create Stock Model
**Files:**
- Create: `prisma/schema.prisma`
- Create: `prisma/migrations/xxxxxxxxxxxx_init/migration.sql` (generated)
- [ ] **Step 1: Install Prisma dependencies**
```bash
npm install prisma @prisma/client
npx prisma init --datasource-provider sqlite
```
Expected: Creates prisma/ directory with schema.prisma
- [ ] **Step 2: Define Stock model in schema.prisma**
```prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
model Stock {
id String @id @default(cuid())
ticker String @unique
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
```
Expected: Schema file saved
- [ ] **Step 3: Generate Prisma client and migrate**
```bash
npx prisma generate
npx prisma migrate dev --name init
```
Expected: `prisma/dev.db` created, client generated
- [ ] **Step 4: Commit**
```bash
git add prisma/
git commit -m "feat: initialize prisma with Stock model"
```
---
### Task 2: Create Database Service Layer
**Files:**
- Create: `app/lib/db.server.ts`
- [ ] **Step 1: Create Prisma client singleton**
```typescript
import { PrismaClient } from "@prisma/client";
declare global {
var prisma: PrismaClient | undefined;
}
export const db = global.prisma || new PrismaClient();
if (process.env.NODE_ENV !== "production") {
global.prisma = db;
}
```
Expected: File created without TypeScript errors
- [ ] **Step 2: Commit**
```bash
git add app/lib/db.server.ts
git commit -m "feat: create prisma client singleton"
```
---
### Task 3: Create Stock API Routes
**Files:**
- Create: `app/routes/api/stocks/index.ts`
- Create: `app/routes/api/stocks/$ticker.ts`
- [ ] **Step 1: Create GET /api/stocks route**
```typescript
import { db } from "../../../lib/db.server";
export async function loader() {
const stocks = await db.stock.findMany({
orderBy: { ticker: "asc" },
});
return Response.json(stocks);
}
export async function action({ request }: { request: Request }) {
const formData = await request.formData();
const ticker = formData.get("ticker")?.toString().toUpperCase();
if (!ticker) {
return Response.json({ error: "Ticker is required" }, { status: 400 });
}
const stock = await db.stock.create({
data: { ticker },
});
return Response.json(stock);
}
```
- [ ] **Step 2: Register routes in routes.ts**
```typescript
route("api/stocks", "routes/api/stocks/index.ts"),
route("api/stocks/$ticker", "routes/api/stocks/\$ticker.ts"),
```
- [ ] **Step 3: Commit**
```bash
git add app/routes/api/stocks/
git commit -m "feat: add stock CRUD API routes"
```
---
### Task 4: Modify Analyze Route to Use Database
**Files:**
- Modify: `app/routes/analyze.tsx`
- [ ] **Step 1: Merge Alpaca positions with database stocks on load**
```typescript
// In loadPortfolio useEffect, after fetching Alpaca positions:
const [alpacaPositions, dbStocks] = await Promise.all([
fetch("/api/alpaca/positions").then(r => r.ok ? r.json() : []),
fetch("/api/stocks").then(r => r.ok ? r.json() : [])
]);
// Build initial stocks from both sources
const initialStocks = await Promise.all([
// Alpaca positions first
...alpacaPositions.map(async (p: { ticker: string; qty: number }) => {
// ... existing logic
}),
// Then DB stocks not in Alpaca
...dbStocks.map(async (s: { ticker: string }) => {
const existing = alpacaPositions.find((p: { ticker: string }) => p.ticker === s.ticker);
if (existing) return null;
return {
id: `db-${s.ticker}`,
ticker: s.ticker,
currentPrice: null,
position: 0,
rsi: null,
analysis: null,
loading: false,
};
})
].filter(Boolean));
```
- [ ] **Step 2: Save new ticker to database when added**
```typescript
// In addStock function, after successfully adding ticker:
await fetch("/api/stocks", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ticker }),
});
```
- [ ] **Step 3: Commit**
```bash
git add app/routes/analyze.tsx
git commit -m "feat: integrate stock database with analyze page"
```
---
### Task 5: Write Playwright Tests
**Files:**
- Create: `tests/stock-db.spec.ts`
- [ ] **Step 1: Write test for stock CRUD API**
```typescript
import { test, expect } from "@playwright/test";
test.describe("Stock Database", () => {
test("should add and list stocks", async ({ page }) => {
// POST to create
const createRes = await page.request.post("/api/stocks", {
data: { ticker: "TEST" },
headers: { "Content-Type": "application/x-www-form-urlencoded" },
});
expect(createRes.ok()).toBeTruthy();
// GET to list
const listRes = await page.request.get("/api/stocks");
const stocks = await listRes.json();
expect(stocks).toContainEqual(expect.objectContaining({ ticker: "TEST" }));
});
});
```
- [ ] **Step 2: Commit**
```bash
git add tests/stock-db.spec.ts
git commit -m "test: add stock database E2E tests"
```
---
### Task 6: Verify Installation
**Files:**
- Check: `package.json`
- [ ] **Step 1: Run typecheck and tests**
```bash
npm run typecheck
npm run test:e2e
```
Expected: All commands succeed
- [ ] **Step 2: Commit**
```bash
git commit -am "chore: verify prisma integration"
```