feat: products api (list/add/detail/delete/manual-scrape)

This commit is contained in:
2026-05-25 14:25:26 +00:00
parent fabf6a5c38
commit 8a284edcb1
3 changed files with 109 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
import { NextRequest, NextResponse } from 'next/server'
import { eq, desc } from 'drizzle-orm'
import { db, products, priceSnapshots, alerts } from '@/lib/db'
export async function GET(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const [product] = await db.select().from(products).where(eq(products.id, id))
if (!product) return NextResponse.json({ error: 'not found' }, { status: 404 })
const snapshots = await db.select().from(priceSnapshots)
.where(eq(priceSnapshots.productId, id))
.orderBy(desc(priceSnapshots.scrapedAt))
.limit(1000)
const productAlerts = await db.select().from(alerts).where(eq(alerts.productId, id))
return NextResponse.json({ product, snapshots, alerts: productAlerts })
}
export async function DELETE(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params
await db.delete(products).where(eq(products.id, id))
return NextResponse.json({ ok: true })
}

View File

@@ -0,0 +1,27 @@
import { NextRequest, NextResponse } from 'next/server'
import { eq } from 'drizzle-orm'
import { db, products, priceSnapshots } from '@/lib/db'
import { scrapeUrl } from '@/lib/scrapers'
import { ensureScrapersRegistered } from '@/lib/scrapers/register'
export async function POST(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
ensureScrapersRegistered()
const { id } = await params
const [product] = await db.select().from(products).where(eq(products.id, id))
if (!product) return NextResponse.json({ error: 'not found' }, { status: 404 })
const result = await scrapeUrl(product.url)
await db.insert(priceSnapshots).values({
productId: id,
price: result.price !== null ? String(result.price) : null,
currency: result.currency,
availability: result.availability,
error: result.error ?? null,
})
await db.update(products).set({
lastScrapedAt: new Date(),
consecutiveFailures: result.price === null ? product.consecutiveFailures + 1 : 0,
}).where(eq(products.id, id))
return NextResponse.json(result)
}

View File

@@ -0,0 +1,59 @@
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { eq, sql } from 'drizzle-orm'
import { db, products, priceSnapshots } from '@/lib/db'
import { detectShop } from '@/lib/shops'
import { scrapeUrl } from '@/lib/scrapers'
import { ensureScrapersRegistered } from '@/lib/scrapers/register'
const AddSchema = z.object({ url: z.string().url() })
export async function GET() {
const rows = await db.execute<{
id: string; url: string; shop: string; name: string; image_url: string | null;
last_price: string | null; last_scraped_at: Date | null; min_price: string | null;
}>(sql`
select p.id, p.url, p.shop, p.name, p.image_url, p.last_scraped_at,
(select price from price_snapshots s where s.product_id = p.id and s.price is not null order by scraped_at desc limit 1) as last_price,
(select min(price) from price_snapshots s where s.product_id = p.id and s.price is not null) as min_price
from products p
where p.enabled = true
order by p.created_at desc
`)
return NextResponse.json(rows)
}
export async function POST(req: NextRequest) {
ensureScrapersRegistered()
const body = await req.json()
const parsed = AddSchema.safeParse(body)
if (!parsed.success) return NextResponse.json({ error: 'invalid url' }, { status: 400 })
const shop = detectShop(parsed.data.url)
if (!shop) return NextResponse.json({ error: 'unsupported shop' }, { status: 400 })
const result = await scrapeUrl(parsed.data.url)
const name = result.name || parsed.data.url
const [inserted] = await db.insert(products).values({
url: parsed.data.url,
shop,
name,
imageUrl: result.imageUrl ?? null,
}).returning()
await db.insert(priceSnapshots).values({
productId: inserted.id,
price: result.price !== null ? String(result.price) : null,
currency: result.currency,
availability: result.availability,
error: result.error ?? null,
})
await db.update(products).set({
lastScrapedAt: new Date(),
consecutiveFailures: result.price === null ? 1 : 0,
}).where(eq(products.id, inserted.id))
return NextResponse.json({ id: inserted.id }, { status: 201 })
}