feat: products api (list/add/detail/delete/manual-scrape)
This commit is contained in:
23
src/app/api/products/[id]/route.ts
Normal file
23
src/app/api/products/[id]/route.ts
Normal 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 })
|
||||
}
|
||||
27
src/app/api/products/[id]/scrape/route.ts
Normal file
27
src/app/api/products/[id]/scrape/route.ts
Normal 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)
|
||||
}
|
||||
59
src/app/api/products/route.ts
Normal file
59
src/app/api/products/route.ts
Normal 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 })
|
||||
}
|
||||
Reference in New Issue
Block a user