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:
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
```
|
||||
Reference in New Issue
Block a user