Files
Void-Homelab/lib/db/repos/pages.js
root 3f77f3faad feat(pages): explicit position ordering + sectioned space view
Add position column to pages (migration 020), update listBySpace to ORDER BY position, title,
expose position in update(), add to patchSchema, and replace the space view flat table with a
tree renderer grouping pages by parent_id under h4 section headers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 22:33:10 +10:00

98 lines
3.0 KiB
JavaScript

import { pool } from '../pool.js';
import { recordAudit } from './audit_stub.js';
import { triggerEmbed } from '../../jobs/triggers.js';
async function snapshot(client, page_id, body_md, edited_by) {
await client.query(
`INSERT INTO page_revisions(page_id, body_md, edited_by)
VALUES($1,$2,$3)`,
[page_id, body_md, edited_by || null]
);
}
export async function create({ space_id, slug, title, body_md = '', parent_id }, actor) {
const client = await pool.connect();
try {
await client.query('BEGIN');
const { rows: [r] } = await client.query(
`INSERT INTO pages(space_id, slug, title, body_md, parent_id)
VALUES($1,$2,$3,$4,$5) RETURNING *`,
[space_id, slug, title, body_md, parent_id || null]
);
await snapshot(client, r.id, body_md, actor?.kind);
await client.query('COMMIT');
await recordAudit(actor, 'create', 'page', r.id, null, r);
await triggerEmbed('page', r.id);
return r;
} catch (e) {
await client.query('ROLLBACK'); throw e;
} finally {
client.release();
}
}
export async function getById(id) {
const { rows: [r] } = await pool.query(`SELECT * FROM pages WHERE id=$1`, [id]);
return r;
}
export async function getBySlug(space_id, slug) {
const { rows: [r] } = await pool.query(
`SELECT * FROM pages WHERE space_id=$1 AND slug=$2`, [space_id, slug]
);
return r;
}
export async function listBySpace(space_id) {
const { rows } = await pool.query(
`SELECT id, space_id, slug, title, parent_id, position, updated_at
FROM pages WHERE space_id=$1 ORDER BY position, title`, [space_id]
);
return rows;
}
export async function listRevisions(page_id) {
const { rows } = await pool.query(
`SELECT * FROM page_revisions WHERE page_id=$1 ORDER BY created_at DESC`,
[page_id]
);
return rows;
}
export async function update(id, patch, actor) {
const before = await getById(id);
const client = await pool.connect();
try {
await client.query('BEGIN');
const fields = ['slug','title','body_md','body_html','parent_id','position','embedding'];
const sets = [], vals = [];
let i = 1;
for (const f of fields) {
if (patch[f] !== undefined) { sets.push(`${f}=$${i++}`); vals.push(patch[f]); }
}
sets.push(`updated_at=now()`);
vals.push(id);
const { rows: [r] } = await client.query(
`UPDATE pages SET ${sets.join(', ')} WHERE id=$${i} RETURNING *`,
vals
);
if (patch.body_md !== undefined && patch.body_md !== before.body_md) {
await snapshot(client, id, patch.body_md, actor?.kind);
}
await client.query('COMMIT');
await recordAudit(actor, 'update', 'page', id, before, r);
if (patch.embedding === undefined) await triggerEmbed('page', id);
return r;
} catch (e) {
await client.query('ROLLBACK'); throw e;
} finally {
client.release();
}
}
export async function del(id, actor) {
const before = await getById(id);
await pool.query(`DELETE FROM pages WHERE id=$1`, [id]);
await recordAudit(actor, 'delete', 'page', id, before, null);
}