const wikiDb = require('./wiki.db') const { cleanBody } = require('../../utils/sanitizeHtml') const { extractTargets } = require('./wiki.links') function slugifyTag(label) { return String(label) .toLowerCase() .trim() .replace(/[^a-z0-9]+/g, '-') .replace(/(^-|-$)/g, '') } // Upsert each label into wiki_tags and set the page's tag set exactly. async function syncTags(pageId, tags) { const ids = [] const seen = new Set() for (const raw of tags) { const label = String(raw).trim() if (!label) continue const slug = slugifyTag(label) if (!slug || seen.has(slug)) continue seen.add(slug) ids.push(await wikiDb.upsertTag(slug, label)) } await wikiDb.setPageTags(pageId, ids) await wikiDb.deleteOrphanTags() } // Rebuild the page's outgoing internal-link rows from its (sanitized) body. async function rebuildLinks(pageId, html) { await wikiDb.clearLinks(pageId) for (const target of extractTargets(html)) { await wikiDb.insertLink(pageId, target) } } // Snapshot the current content of a page into the revision history. async function writeRevision(page, editorId, changeNote = null) { await wikiDb.insertRevision({ pageId: page.id, title: page.title, body: page.body, excerpt: page.excerpt, categoryId: page.category_id, editorId, changeNote, }) } // ── Pages ────────────────────────────────────────────────────────────── async function listPublished(filters = {}) { return wikiDb.listPublishedSummaries(filters) } async function listAll(filters = {}) { return wikiDb.listAllSummaries(filters) } async function search(q, opts = {}) { return wikiDb.searchSummaries(q, opts) } // Admin detail: page + its tags. async function getBySlug(slug) { const page = await wikiDb.findBySlug(slug) if (!page) return null page.tags = await wikiDb.getTagsForPage(page.id) return page } // Public detail: page + tags + backlinks + missing (red) link targets. async function getPublishedBySlug(slug) { const page = await wikiDb.findPublishedBySlug(slug) if (!page) return null page.tags = await wikiDb.getTagsForPage(page.id) page.backlinks = await wikiDb.getBacklinks(slug, { publishedOnly: true }) const targets = extractTargets(page.body) const existing = await wikiDb.getExistingSlugs(targets) page.missing_links = targets.filter((t) => !existing.has(t)) return page } async function create({ slug, title, body, excerpt, categoryId, published, updatedBy, tags }) { const clean = cleanBody(body) await wikiDb.insert({ slug, title, body: clean, excerpt: excerpt || null, categoryId: categoryId ?? null, published: published !== false, updatedBy, }) const page = await wikiDb.findBySlug(slug) if (Array.isArray(tags)) await syncTags(page.id, tags) await rebuildLinks(page.id, clean) await writeRevision(page, updatedBy, 'Created') return getBySlug(slug) } // Partial update — only keys present in `input` are written. async function update(slug, input) { const current = await wikiDb.findBySlug(slug) if (!current) return null const fields = { updated_by: input.updatedBy ?? null } let cleanForLinks = null if ('title' in input) fields.title = input.title if ('body' in input) { fields.body = cleanBody(input.body) cleanForLinks = fields.body } if ('excerpt' in input) fields.excerpt = input.excerpt || null if ('categoryId' in input) fields.category_id = input.categoryId ?? null if ('published' in input) { fields.published = input.published ? 1 : 0 if (input.published && !current.published_at) fields.published_at = new Date() } await wikiDb.updateBySlug(slug, fields) if (Array.isArray(input.tags)) await syncTags(current.id, input.tags) if (cleanForLinks != null) await rebuildLinks(current.id, cleanForLinks) const page = await wikiDb.findBySlug(slug) await writeRevision(page, input.updatedBy ?? null, input.changeNote || null) return getBySlug(slug) } async function setPublished(slug, published) { const current = await wikiDb.findBySlug(slug) if (!current) return null const fields = { published: published ? 1 : 0 } if (published && !current.published_at) fields.published_at = new Date() await wikiDb.updateBySlug(slug, fields) return getBySlug(slug) } async function remove(slug) { const res = await wikiDb.deleteBySlug(slug) await wikiDb.deleteOrphanTags() // page_tags cascade on delete; drop now-empty tags return res } // ── Revisions ────────────────────────────────────────────────────────── async function listRevisions(slug) { const page = await wikiDb.findBySlug(slug) if (!page) return null return wikiDb.listRevisions(page.id) } async function getRevision(slug, revId) { const page = await wikiDb.findBySlug(slug) if (!page) return null const rev = await wikiDb.findRevision(revId) if (!rev || rev.page_id !== page.id) return null return rev } // Restore an old revision: overwrite the page with the snapshot, rebuild links, // then record a new revision (history stays append-only). async function restoreRevision(slug, revId, editorId) { const page = await wikiDb.findBySlug(slug) if (!page) return null const rev = await wikiDb.findRevision(revId) if (!rev || rev.page_id !== page.id) return null await wikiDb.updateBySlug(slug, { title: rev.title, body: rev.body, excerpt: rev.excerpt, category_id: rev.category_id, updated_by: editorId, }) await rebuildLinks(page.id, rev.body) const restored = await wikiDb.findBySlug(slug) await writeRevision(restored, editorId, `Restored from revision #${revId}`) return getBySlug(slug) } // ── Tags ─────────────────────────────────────────────────────────────── async function listTags() { return wikiDb.listTags() } async function getTagBySlug(slug) { return wikiDb.findTagBySlug(slug) } // ── Categories ───────────────────────────────────────────────────────── async function listCategories() { return wikiDb.listCategories() } async function getCategoryBySlug(slug) { return wikiDb.findCategoryBySlug(slug) } async function getCategoryById(id) { return wikiDb.findCategoryById(id) } async function createCategory({ slug, title, description, sortOrder }) { const id = await wikiDb.insertCategory({ slug, title, description, sortOrder }) return wikiDb.findCategoryById(id) } async function updateCategory(id, input) { const fields = {} if ('title' in input) fields.title = input.title if ('slug' in input) fields.slug = input.slug if ('description' in input) fields.description = input.description || null if ('sortOrder' in input) fields.sort_order = input.sortOrder await wikiDb.updateCategory(id, fields) return wikiDb.findCategoryById(id) } async function removeCategory(id) { return wikiDb.deleteCategory(id) } module.exports = { listPublished, listAll, search, getBySlug, getPublishedBySlug, create, update, setPublished, remove, listRevisions, getRevision, restoreRevision, listTags, getTagBySlug, listCategories, getCategoryBySlug, getCategoryById, createCategory, updateCategory, removeCategory, }