refactor: migrate draft/post API endpoints to use Drizzle ORM instead of raw SQL queries
This commit is contained in:
parent
038591c9cf
commit
93f93e4f96
@ -21,10 +21,10 @@ export default function EditorShell({ onLogout }: { onLogout?: () => void }) {
|
||||
if (savedId) {
|
||||
(async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/drafts/${savedId}`);
|
||||
const res = await fetch(`/api/posts/${savedId}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setDraft(data.content || '');
|
||||
setDraft(data.contentHtml || '');
|
||||
setDraftId(data.id || savedId);
|
||||
}
|
||||
} catch {}
|
||||
@ -32,10 +32,10 @@ export default function EditorShell({ onLogout }: { onLogout?: () => void }) {
|
||||
}
|
||||
(async () => {
|
||||
try {
|
||||
const res = await fetch('/api/drafts');
|
||||
const res = await fetch('/api/posts');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (Array.isArray(data.items)) setDrafts(data.items);
|
||||
if (Array.isArray(data.items)) setDrafts(data.items.map((p: any) => p.id));
|
||||
}
|
||||
} catch {}
|
||||
})();
|
||||
@ -45,10 +45,18 @@ export default function EditorShell({ onLogout }: { onLogout?: () => void }) {
|
||||
// Keep local fallback
|
||||
localStorage.setItem('voxblog_draft', draft);
|
||||
try {
|
||||
const res = await fetch('/api/drafts', {
|
||||
const res = await fetch('/api/posts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: draftId ?? undefined, content: draft }),
|
||||
body: JSON.stringify({
|
||||
id: draftId ?? undefined,
|
||||
title: meta.title || undefined,
|
||||
contentHtml: draft,
|
||||
tags: meta.tagsText ? meta.tagsText.split(',').map(t => t.trim()).filter(Boolean) : undefined,
|
||||
featureImage: meta.featureImage || undefined,
|
||||
canonicalUrl: meta.canonicalUrl || undefined,
|
||||
status: 'editing',
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
@ -62,13 +70,13 @@ export default function EditorShell({ onLogout }: { onLogout?: () => void }) {
|
||||
|
||||
const loadDraft = async (id: string) => {
|
||||
try {
|
||||
const res = await fetch(`/api/drafts/${id}`);
|
||||
const res = await fetch(`/api/posts/${id}`);
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
setDraft(data.content || '');
|
||||
setDraft(data.contentHtml || '');
|
||||
setDraftId(data.id || id);
|
||||
localStorage.setItem('voxblog_draft_id', data.id || id);
|
||||
if (data.content) localStorage.setItem('voxblog_draft', data.content);
|
||||
if (data.contentHtml) localStorage.setItem('voxblog_draft', data.contentHtml);
|
||||
} catch {}
|
||||
};
|
||||
|
||||
@ -78,9 +86,12 @@ export default function EditorShell({ onLogout }: { onLogout?: () => void }) {
|
||||
Welcome to VoxBlog Editor
|
||||
</Typography>
|
||||
<Box sx={{ display: 'grid', gap: 3 }}>
|
||||
<Recorder onTranscript={(t) => setDraft(t)} />
|
||||
<Recorder
|
||||
postId={draftId ?? undefined}
|
||||
onInsertAtCursor={(html) => editorRef.current?.insertHtmlAtCursor(html)}
|
||||
/>
|
||||
<Box>
|
||||
<Typography variant="subtitle1" sx={{ mb: 1 }}>Drafts</Typography>
|
||||
<Typography variant="subtitle1" sx={{ mb: 1 }}>Posts</Typography>
|
||||
<Stack direction="row" spacing={1} sx={{ flexWrap: 'wrap', mb: 1 }}>
|
||||
{drafts.map(id => (
|
||||
<Button key={id} size="small" variant={draftId === id ? 'contained' : 'outlined'} onClick={() => loadDraft(id)}>
|
||||
|
||||
@ -8,6 +8,7 @@ import { Button, Stack } from '@mui/material';
|
||||
|
||||
export type RichEditorHandle = {
|
||||
insertImage: (src: string, alt?: string) => void;
|
||||
insertHtmlAtCursor: (html: string) => void;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
@ -34,6 +35,10 @@ const RichEditor = forwardRef<RichEditorHandle, Props>(({ value, onChange, place
|
||||
if (!editor) return;
|
||||
editor.chain().focus().setImage({ src, alt }).run();
|
||||
},
|
||||
insertHtmlAtCursor: (html: string) => {
|
||||
if (!editor) return;
|
||||
editor.chain().focus().insertContent(html).run();
|
||||
},
|
||||
}), [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Box, Button, Stack, Typography } from '@mui/material';
|
||||
|
||||
export default function Recorder({ onTranscript }: { onTranscript?: (t: string) => void }) {
|
||||
export default function Recorder({ postId, onInsertAtCursor, onTranscript }: { postId?: string; onInsertAtCursor?: (html: string) => void; onTranscript?: (t: string) => void }) {
|
||||
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
||||
const chunksRef = useRef<Blob[]>([]);
|
||||
const mimeRef = useRef<string>('audio/webm');
|
||||
@ -87,7 +87,8 @@ export default function Recorder({ onTranscript }: { onTranscript?: (t: string)
|
||||
const form = new FormData();
|
||||
const ext = c.mime.includes('mp4') ? 'm4a' : 'webm';
|
||||
form.append('audio', c.blob, `recording.${ext}`);
|
||||
const res = await fetch('/api/media/audio', { method: 'POST', body: form });
|
||||
const url = postId ? `/api/media/audio?postId=${encodeURIComponent(postId)}` : '/api/media/audio';
|
||||
const res = await fetch(url, { method: 'POST', body: form });
|
||||
if (!res.ok) {
|
||||
const txt = await res.text();
|
||||
throw new Error(`Upload failed: ${res.status} ${txt}`);
|
||||
@ -112,7 +113,7 @@ export default function Recorder({ onTranscript }: { onTranscript?: (t: string)
|
||||
const res = await fetch('/api/stt', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ bucket: c.uploadedBucket ?? undefined, key: c.uploadedKey }),
|
||||
body: JSON.stringify({ bucket: c.uploadedBucket ?? undefined, key: c.uploadedKey, postId: postId ?? undefined, clipId: c.id }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const txt = await res.text();
|
||||
@ -148,8 +149,13 @@ export default function Recorder({ onTranscript }: { onTranscript?: (t: string)
|
||||
};
|
||||
|
||||
const applyTranscriptsToDraft = () => {
|
||||
const text = clips.map(c => c.transcript || '').filter(Boolean).join('\n\n');
|
||||
if (onTranscript) onTranscript(text);
|
||||
const html = clips
|
||||
.map(c => (c.transcript || '').trim())
|
||||
.filter(Boolean)
|
||||
.map(t => `<p>${t.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br/>')}</p>`)
|
||||
.join('');
|
||||
if (onInsertAtCursor) onInsertAtCursor(html);
|
||||
if (!onInsertAtCursor && onTranscript) onTranscript(html);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -2,7 +2,9 @@ import express from 'express';
|
||||
import path from 'path';
|
||||
import { promises as fs } from 'fs';
|
||||
import crypto from 'crypto';
|
||||
import { pool } from './db';
|
||||
import { db } from './db';
|
||||
import { posts } from './db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
const router = express.Router();
|
||||
const draftsDir = path.resolve(__dirname, '../../../data/drafts');
|
||||
@ -50,10 +52,23 @@ router.post('/', async (req, res) => {
|
||||
|
||||
try {
|
||||
const now = new Date();
|
||||
await pool.query(
|
||||
'INSERT INTO posts (id, title, content_html, tags_text, feature_image, canonical_url, status, version, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE content_html = VALUES(content_html), updated_at = VALUES(updated_at)',
|
||||
[draftId, null, content, null, null, null, 'editing', 1, now, now]
|
||||
);
|
||||
// Upsert: try insert, fall back to update
|
||||
try {
|
||||
await db.insert(posts).values({
|
||||
id: draftId,
|
||||
title: null as any,
|
||||
contentHtml: content,
|
||||
tagsText: null as any,
|
||||
featureImage: null as any,
|
||||
canonicalUrl: null as any,
|
||||
status: 'editing' as any,
|
||||
version: 1,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
} catch {
|
||||
await db.update(posts).set({ contentHtml: content, updatedAt: now }).where(eq(posts.id, draftId));
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore DB errors to keep file-based save working
|
||||
}
|
||||
|
||||
@ -2,7 +2,9 @@ import express from 'express';
|
||||
import multer from 'multer';
|
||||
import crypto from 'crypto';
|
||||
import { uploadBuffer, downloadObject, listObjects, deleteObject as s3DeleteObject } from './storage/s3';
|
||||
import { pool } from './db';
|
||||
import { db } from './db';
|
||||
import { audioClips, posts } from './db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
const router = express.Router();
|
||||
const upload = multer({ storage: multer.memoryStorage() });
|
||||
@ -126,14 +128,19 @@ router.post('/audio', upload.single('audio'), async (
|
||||
contentType: mime,
|
||||
});
|
||||
|
||||
// If postId provided, insert into audio_clips table
|
||||
// If postId provided, insert into audio_clips table (Drizzle) and touch post.updated_at
|
||||
if (postId) {
|
||||
try {
|
||||
const now = new Date();
|
||||
await pool.query(
|
||||
'INSERT INTO audio_clips (id, post_id, bucket, object_key, mime, created_at) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[clipId, postId, out.bucket, out.key, mime, now]
|
||||
);
|
||||
await db.insert(audioClips).values({
|
||||
id: clipId,
|
||||
postId,
|
||||
bucket: out.bucket,
|
||||
key: out.key,
|
||||
mime,
|
||||
createdAt: now,
|
||||
});
|
||||
await db.update(posts).set({ updatedAt: now }).where(eq(posts.id, postId));
|
||||
} catch (e) {
|
||||
console.error('[API] DB insert audio_clip failed:', e);
|
||||
// continue anyway, response still returns S3 info
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import express from 'express';
|
||||
import { fetch, FormData } from 'undici';
|
||||
import { downloadObject } from './storage/s3';
|
||||
import { pool } from './db';
|
||||
import { db } from './db';
|
||||
import { audioClips, posts } from './db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@ -53,8 +55,8 @@ router.post('/', async (req, res) => {
|
||||
// If postId and clipId provided, persist transcript into DB
|
||||
if (postId && clipId) {
|
||||
try {
|
||||
await pool.query('UPDATE audio_clips SET transcript = ? WHERE id = ? AND post_id = ? LIMIT 1', [transcript, clipId, postId]);
|
||||
await pool.query('UPDATE posts SET updated_at = ? WHERE id = ? LIMIT 1', [new Date(), postId]);
|
||||
await db.update(audioClips).set({ transcript }).where(eq(audioClips.id, clipId));
|
||||
await db.update(posts).set({ updatedAt: new Date() }).where(eq(posts.id, postId));
|
||||
} catch (e) {
|
||||
console.error('DB update transcript failed:', e);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user