diff --git a/src/routes/articles.js b/src/routes/articles.js index a76fe7b..475e721 100644 --- a/src/routes/articles.js +++ b/src/routes/articles.js @@ -1,18 +1,18 @@ const express = require('express'); const router = express.Router(); const articlesSvc = require('../services/articles'); +const { query } = require('../config/db'); -// GET /api/articles — список +// GET /api/articles — список опубликованных router.get('/', async (req, res) => { try { const limit = Math.min(parseInt(req.query.limit) || 20, 100); const offset = parseInt(req.query.offset) || 0; const tag = req.query.tag || null; - const list = await articlesSvc.listArticles({ limit, offset, tag }); + const category = req.query.category || null; + const list = await articlesSvc.listArticles({ limit, offset, tag, category }); res.json(list); - } catch (err) { - res.status(500).json({ error: err.message }); - } + } catch (err) { res.status(500).json({ error: err.message }); } }); // GET /api/articles/tags — топ тегов @@ -20,40 +20,12 @@ router.get('/tags', async (_, res) => { try { const tags = await articlesSvc.getAllTags(); res.json(tags); - } catch (err) { - res.status(500).json({ error: err.message }); - } + } catch (err) { res.status(500).json({ error: err.message }); } }); -// GET /api/articles/:slug — одна -router.get('/:slug', async (req, res) => { - try { - const a = await articlesSvc.getArticleBySlug(req.params.slug); - if (!a) return res.status(404).json({ error: 'Not found' }); - res.json(a); - } catch (err) { - res.status(500).json({ error: err.message }); - } -}); - -// POST /api/articles/generate — сгенерировать и сохранить (синхронно, для cron) -router.post('/generate', async (req, res) => { - try { - const { topic, keywords = [], tags = [], autoPublish = true } = req.body; - if (!topic) return res.status(400).json({ error: 'topic is required' }); - const article = await articlesSvc.generateAndSaveArticle({ topic, keywords, tags, autoPublish }); - res.json(article); - } catch (err) { - console.error('[Articles] generate', err); - res.status(500).json({ error: err.message }); - } -}); - - -// GET /api/articles/admin — все статьи для админки (включая черновики, все поля) +// GET /api/articles/admin — все статьи для админки (включая черновики) router.get('/admin', async (req, res) => { try { - const { query } = require('../config/db'); const limit = Math.min(parseInt(req.query.limit) || 100, 200); const offset = parseInt(req.query.offset) || 0; const { rows } = await query( @@ -69,34 +41,51 @@ router.get('/admin', async (req, res) => { // GET /api/articles/id/:id — одна статья по числовому id router.get('/id/:id', async (req, res) => { try { - const { query } = require('../config/db'); const { rows } = await query('SELECT * FROM articles WHERE id=$1', [req.params.id]); if (!rows.length) return res.status(404).json({ error: 'Not found' }); res.json(rows[0]); } catch (err) { res.status(500).json({ error: err.message }); } }); -// PATCH /api/articles/:id — обновить статью +// POST /api/articles/generate +router.post('/generate', async (req, res) => { + try { + const { topic, keywords = [], tags = [], autoPublish = true, category = 'ai-tools' } = req.body; + if (!topic) return res.status(400).json({ error: 'topic is required' }); + const article = await articlesSvc.generateAndSaveArticle({ topic, keywords, tags, autoPublish, category }); + res.json(article); + } catch (err) { + console.error('[Articles] generate', err); + res.status(500).json({ error: err.message }); + } +}); + +// POST /api/articles/backfill-covers +router.post('/backfill-covers', async (req, res) => { + try { + const covers = require('../services/covers'); + const limit = parseInt(req.body?.limit) || 3; + const result = await covers.backfillCovers({ limit }); + res.json(result); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// PATCH /api/articles/:id router.patch('/:id', async (req, res) => { try { - const { query } = require('../config/db'); - const { title, excerpt, content, tags, status, seo_title, seo_descr, cover_url } = req.body; - const fields = []; - const vals = []; - let i = 1; - if (title !== undefined) { fields.push(`title=${i++}`); vals.push(title); } - if (excerpt !== undefined) { fields.push(`excerpt=${i++}`); vals.push(excerpt); } - if (content !== undefined) { fields.push(`content=${i++}`); vals.push(content); } - if (tags !== undefined) { fields.push(`tags=${i++}`); vals.push(tags); } - if (status !== undefined) { fields.push(`status=${i++}`); vals.push(status); } - if (seo_title !== undefined) { fields.push(`seo_title=${i++}`); vals.push(seo_title); } - if (seo_descr !== undefined) { fields.push(`seo_descr=${i++}`); vals.push(seo_descr); } - if (cover_url !== undefined) { fields.push(`cover_url=${i++}`); vals.push(cover_url); } + const allowed = ['title','excerpt','content','tags','status','seo_title','seo_descr','cover_url','category']; + const fields = []; const vals = []; let i = 1; + for (const key of allowed) { + if (req.body[key] !== undefined) { + fields.push(`${key}=$${i++}`); + vals.push(req.body[key]); + } + } if (!fields.length) return res.status(400).json({ error: 'Nothing to update' }); fields.push(`updated_at=NOW()`); vals.push(req.params.id); const { rows } = await query( - `UPDATE articles SET ${fields.join(', ')} WHERE id=${i} RETURNING *`, + `UPDATE articles SET ${fields.join(', ')} WHERE id=$${i} RETURNING *`, vals ); if (!rows.length) return res.status(404).json({ error: 'Not found' }); @@ -104,26 +93,22 @@ router.patch('/:id', async (req, res) => { } catch (err) { res.status(500).json({ error: err.message }); } }); -// DELETE /api/articles/:id — удалить статью +// DELETE /api/articles/:id router.delete('/:id', async (req, res) => { try { - const { query } = require('../config/db'); const { rowCount } = await query('DELETE FROM articles WHERE id=$1', [req.params.id]); if (!rowCount) return res.status(404).json({ error: 'Not found' }); res.json({ ok: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); -// POST /api/articles/backfill-covers — досгенерировать обложки для статей без них -router.post('/backfill-covers', async (req, res) => { +// GET /api/articles/:slug — одна статья по slug (ПОСЛЕДНИЙ — чтобы не перехватывал /admin, /tags и т.д.) +router.get('/:slug', async (req, res) => { try { - const covers = require('../services/covers'); - const limit = parseInt(req.body?.limit) || 3; - const result = await covers.backfillCovers({ limit }); - res.json(result); - } catch (err) { - res.status(500).json({ error: err.message }); - } + const a = await articlesSvc.getArticleBySlug(req.params.slug); + if (!a) return res.status(404).json({ error: 'Not found' }); + res.json(a); + } catch (err) { res.status(500).json({ error: err.message }); } }); module.exports = router;