import express from 'express'; import crypto from 'crypto'; import { db } from './db'; import { posts, audioClips } from './db/schema'; import { desc, asc, eq, and, or, like, sql } from 'drizzle-orm'; const router = express.Router(); // List posts (minimal info) router.get('/', async (req, res) => { try { const page = Math.max(parseInt((req.query.page as string) || '1', 10), 1); const pageSizeRaw = Math.max(parseInt((req.query.pageSize as string) || '20', 10), 1); const pageSize = Math.min(pageSizeRaw, 100); const q = ((req.query.q as string) || '').trim(); const sort = ((req.query.sort as string) || 'updatedAt:desc').toLowerCase(); const [sortField, sortDir] = sort.split(':'); const whereConds = [] as any[]; if (q) { const likeQ = `%${q}%`; whereConds.push(or(like(posts.title, likeQ), like(posts.contentHtml, likeQ))); } const whereExpr = whereConds.length > 0 ? and(...whereConds) : undefined; // total count const countRows = await db .select({ cnt: sql`COUNT(*)` }) .from(posts) .where(whereExpr as any); const total = (countRows?.[0]?.cnt as unknown as number) || 0; // sorting let orderByExpr: any = desc(posts.updatedAt); const dir = sortDir === 'asc' ? 'asc' : 'desc'; if (sortField === 'title') orderByExpr = dir === 'asc' ? asc(posts.title) : desc(posts.title); else if (sortField === 'status') orderByExpr = dir === 'asc' ? asc(posts.status) : desc(posts.status); else orderByExpr = dir === 'asc' ? asc(posts.updatedAt) : desc(posts.updatedAt); const offset = (page - 1) * pageSize; const rows = await db .select({ id: posts.id, title: posts.title, status: posts.status, updatedAt: posts.updatedAt }) .from(posts) .where(whereExpr as any) .orderBy(orderByExpr) .limit(pageSize) .offset(offset); return res.json({ items: rows, total, page, pageSize }); } catch (err) { console.error('List posts error:', err); return res.status(500).json({ error: 'Failed to list posts' }); } }); // Get full post with audio clips router.get('/:id', async (req, res) => { try { const id = req.params.id; const rows = await db .select({ id: posts.id, title: posts.title, contentHtml: posts.contentHtml, tagsText: posts.tagsText, featureImage: posts.featureImage, canonicalUrl: posts.canonicalUrl, status: posts.status, createdAt: posts.createdAt, updatedAt: posts.updatedAt, version: posts.version, }) .from(posts) .where(eq(posts.id, id)) .limit(1); if (!rows || rows.length === 0) return res.status(404).json({ error: 'Post not found' }); const post = rows[0]; const clips = await db .select({ id: audioClips.id, postId: audioClips.postId, bucket: audioClips.bucket, key: audioClips.key, mime: audioClips.mime, transcript: audioClips.transcript, durationMs: audioClips.durationMs, createdAt: audioClips.createdAt, }) .from(audioClips) .where(eq(audioClips.postId, id)) .orderBy(audioClips.createdAt); return res.json({ ...post, audioClips: clips }); } catch (err) { console.error('Get post error:', err); return res.status(500).json({ error: 'Failed to get post' }); } }); // Create/update post (no audio clips here) router.post('/', async (req, res) => { try { const { id, title, contentHtml, tags, featureImage, canonicalUrl, status, } = req.body as { id?: string; title?: string; contentHtml?: string; tags?: string[] | string; featureImage?: string; canonicalUrl?: string; status?: string; }; const tagsText = Array.isArray(tags) ? tags.join(',') : (typeof tags === 'string' ? tags : null); if (!contentHtml) return res.status(400).json({ error: 'contentHtml is required' }); const now = new Date(); if (!id) { const newId = crypto.randomUUID(); await db.insert(posts).values({ id: newId, title: title ?? null as any, contentHtml, tagsText: tagsText ?? null as any, featureImage: featureImage ?? null as any, canonicalUrl: canonicalUrl ?? null as any, status: (status as any) ?? 'editing', version: 1, createdAt: now, updatedAt: now, }); return res.json({ id: newId }); } else { await db.update(posts).set({ title: title ?? null as any, contentHtml, tagsText: tagsText ?? null as any, featureImage: featureImage ?? null as any, canonicalUrl: canonicalUrl ?? null as any, status: (status as any) ?? 'editing', updatedAt: now, }).where(eq(posts.id, id)); return res.json({ id }); } } catch (err) { console.error('Save post error:', err); return res.status(500).json({ error: 'Failed to save post' }); } }); // Delete post (and its audio clips) router.delete('/:id', async (req, res) => { try { const id = req.params.id; if (!id) return res.status(400).json({ error: 'id is required' }); await db.delete(audioClips).where(eq(audioClips.postId, id)); await db.delete(posts).where(eq(posts.id, id)); return res.json({ success: true }); } catch (err) { console.error('Delete post error:', err); return res.status(500).json({ error: 'Failed to delete post' }); } }); export default router;