const postsDb = require('./posts.db') const { cleanBody, deriveExcerpt } = require('../../utils/sanitizeHtml') // Sanitize body HTML and treat an empty editor (TipTap emits `
`) as null // so we never store a meaningless empty paragraph. function normalizeBody(body) { const clean = cleanBody(body) if (clean == null) return null return String(clean).trim() === '' ? null : clean } // URL category (kebab) <-> DB enum value. const CATEGORY_MAP = { news: 'news', 'five-on-friday': 'five_on_friday', newsletter: 'newsletter', screenshots: 'screenshot', } const URL_CATEGORIES = Object.keys(CATEGORY_MAP) const DB_CATEGORIES = Object.values(CATEGORY_MAP) function toDbCategory(urlCategory) { return CATEGORY_MAP[urlCategory] || null } function isValidUrlCategory(urlCategory) { return Boolean(CATEGORY_MAP[urlCategory]) } function isValidDbCategory(dbCategory) { return DB_CATEGORIES.includes(dbCategory) } async function listPublished(urlCategory) { return postsDb.listPublished(toDbCategory(urlCategory)) } async function getPublished(urlCategory, idOrSlug) { const id = Number.isInteger(Number(idOrSlug)) ? Number(idOrSlug) : -1 return postsDb.findPublished(toDbCategory(urlCategory), id, String(idOrSlug)) } async function listAll(dbCategory) { return postsDb.listAll(dbCategory || null) } async function getById(id) { return postsDb.findById(id) } async function create(post) { const body = normalizeBody(post.body) // Auto-fill the excerpt from the first of the body when left blank. const excerpt = post.excerpt && String(post.excerpt).trim() ? post.excerpt : deriveExcerpt(body) const id = await postsDb.insert({ ...post, body, excerpt: excerpt || null }) return postsDb.findById(id) } async function update(id, fields) { const next = { ...fields } if ('body' in next) next.body = normalizeBody(next.body) // If the body is being updated and no non-empty excerpt was supplied, // derive one from the new body. if ('body' in next && !(next.excerpt && String(next.excerpt).trim())) { next.excerpt = deriveExcerpt(next.body) || null } await postsDb.update(id, next) return postsDb.findById(id) } async function setPublished(id, published) { const current = await postsDb.findById(id) if (!current) return null const fields = { published: published ? 1 : 0 } // Stamp published_at the first time a post goes live. if (published && !current.published_at) fields.published_at = new Date() await postsDb.update(id, fields) return postsDb.findById(id) } // Announcement pipeline back-pointers (see model/announceJobs). async function linkAnnounceJob(id, jobId) { await postsDb.update(id, { announce_job_id: jobId }) } async function markAnnounced(id, at = new Date()) { await postsDb.update(id, { announced_at: at }) } async function remove(id) { return postsDb.remove(id) } async function counts() { const rows = await postsDb.countByCategory() return rows.reduce((acc, row) => { acc[row.category] = Number(row.c) return acc }, {}) } module.exports = { URL_CATEGORIES, DB_CATEGORIES, toDbCategory, isValidUrlCategory, isValidDbCategory, listPublished, getPublished, listAll, getById, create, update, setPublished, linkAnnounceJob, markAnnounced, remove, counts, }