feat: drizzle schema + migrations for products/snapshots/alerts

This commit is contained in:
2026-05-25 13:49:18 +00:00
parent a96a2e60d8
commit fb308da5c5
8 changed files with 393 additions and 0 deletions

10
src/lib/db/index.ts Normal file
View File

@@ -0,0 +1,10 @@
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import * as schema from './schema'
const connectionString = process.env.DATABASE_URL
if (!connectionString) throw new Error('DATABASE_URL not set')
const client = postgres(connectionString, { max: 5 })
export const db = drizzle(client, { schema })
export * from './schema'

46
src/lib/db/schema.ts Normal file
View File

@@ -0,0 +1,46 @@
import { pgTable, uuid, text, timestamp, boolean, integer, numeric, bigserial, jsonb, index, check } from 'drizzle-orm/pg-core'
import { sql } from 'drizzle-orm'
export const products = pgTable('products', {
id: uuid('id').primaryKey().defaultRandom(),
url: text('url').notNull().unique(),
shop: text('shop').notNull(),
name: text('name').notNull(),
imageUrl: text('image_url'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
enabled: boolean('enabled').notNull().default(true),
lastScrapedAt: timestamp('last_scraped_at', { withTimezone: true }),
consecutiveFailures: integer('consecutive_failures').notNull().default(0),
}, (t) => ({
shopCheck: check('shop_check', sql`${t.shop} in ('amazon','idealo','geizhals')`),
}))
export const priceSnapshots = pgTable('price_snapshots', {
id: bigserial('id', { mode: 'number' }).primaryKey(),
productId: uuid('product_id').notNull().references(() => products.id, { onDelete: 'cascade' }),
price: numeric('price', { precision: 10, scale: 2 }),
currency: text('currency').notNull().default('EUR'),
availability: text('availability'),
error: text('error'),
scrapedAt: timestamp('scraped_at', { withTimezone: true }).notNull().defaultNow(),
}, (t) => ({
productScrapedIdx: index('snapshots_product_scraped_idx').on(t.productId, t.scrapedAt.desc()),
}))
export const alerts = pgTable('alerts', {
id: uuid('id').primaryKey().defaultRandom(),
productId: uuid('product_id').notNull().references(() => products.id, { onDelete: 'cascade' }),
type: text('type').notNull(),
config: jsonb('config').notNull(),
enabled: boolean('enabled').notNull().default(true),
lastTriggeredAt: timestamp('last_triggered_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
}, (t) => ({
typeCheck: check('alert_type_check', sql`${t.type} in ('target_price','all_time_low','percent_drop')`),
}))
export type Product = typeof products.$inferSelect
export type NewProduct = typeof products.$inferInsert
export type PriceSnapshot = typeof priceSnapshots.$inferSelect
export type Alert = typeof alerts.$inferSelect
export type NewAlert = typeof alerts.$inferInsert