feat(migrate): BookStack importer preserves Book › Chapter › Page hierarchy (parent_id)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -8,20 +8,30 @@ import { importBookstack } from '../../migrate/sources/bookstack.js';
|
||||
let spaceId;
|
||||
beforeAll(async () => { await resetDb(); await migrateUp(); spaceId = await ensureSpace('wiki', 'Wiki'); });
|
||||
|
||||
describe('bookstack importer', () => {
|
||||
it('imports pages with markdown body, idempotently', async () => {
|
||||
const list = { data: [{ id: 1, name: 'Page One' }, { id: 2, name: 'Page Two' }] };
|
||||
const detail = (id) => ({ id, name: id === 1 ? 'Page One' : 'Page Two', markdown: `# P${id}\nbody`, html: `<p>body</p>` });
|
||||
const fetchImpl = vi.fn(async (u) => {
|
||||
if (u.endsWith('/api/pages')) return { ok: true, json: async () => list };
|
||||
const id = Number(u.split('/').pop());
|
||||
return { ok: true, json: async () => detail(id) };
|
||||
});
|
||||
const r1 = await importBookstack({ apiUrl: 'http://bs', tokenId: 'i', tokenSecret: 's', spaceId, fetchImpl });
|
||||
expect(r1.created).toBe(2);
|
||||
const { rows } = await pool.query(`SELECT title FROM pages WHERE space_id=$1 ORDER BY title`, [spaceId]);
|
||||
expect(rows.map(r => r.title)).toEqual(['Page One', 'Page Two']);
|
||||
const r2 = await importBookstack({ apiUrl: 'http://bs', tokenId: 'i', tokenSecret: 's', spaceId, fetchImpl });
|
||||
function fetchMock() {
|
||||
const books = { data: [{ id: 10, name: 'Infra Book', description: '' }] };
|
||||
const chapters = { data: [{ id: 20, name: 'Storage', book_id: 10 }] };
|
||||
const list = { data: [{ id: 1, name: 'Overview' }, { id: 2, name: 'Disks' }] };
|
||||
const detail = (id) => ({ id, name: id === 1 ? 'Overview' : 'Disks', markdown: `# P${id}\nbody`, book_id: 10, chapter_id: id === 2 ? 20 : 0 });
|
||||
return vi.fn(async (u) => {
|
||||
if (u.endsWith('/api/books')) return { ok: true, json: async () => books };
|
||||
if (u.endsWith('/api/chapters')) return { ok: true, json: async () => chapters };
|
||||
if (u.endsWith('/api/pages')) return { ok: true, json: async () => list };
|
||||
return { ok: true, json: async () => detail(Number(u.split('/').pop())) };
|
||||
});
|
||||
}
|
||||
|
||||
describe('bookstack importer (hierarchy)', () => {
|
||||
it('creates Book › Chapter › Page with parent links, idempotently', async () => {
|
||||
const r1 = await importBookstack({ apiUrl: 'http://bs', tokenId: 'i', tokenSecret: 's', spaceId, fetchImpl: fetchMock() });
|
||||
expect(r1.created).toBe(4); // 1 book + 1 chapter + 2 pages
|
||||
const byTitle = {};
|
||||
for (const r of (await pool.query(`SELECT id, title, parent_id FROM pages WHERE space_id=$1`, [spaceId])).rows) byTitle[r.title] = r;
|
||||
expect(byTitle['Infra Book'].parent_id).toBeNull();
|
||||
expect(byTitle['Storage'].parent_id).toBe(byTitle['Infra Book'].id); // chapter under book
|
||||
expect(byTitle['Overview'].parent_id).toBe(byTitle['Infra Book'].id); // page directly under book (chapter_id 0)
|
||||
expect(byTitle['Disks'].parent_id).toBe(byTitle['Storage'].id); // page under chapter
|
||||
const r2 = await importBookstack({ apiUrl: 'http://bs', tokenId: 'i', tokenSecret: 's', spaceId, fetchImpl: fetchMock() });
|
||||
expect(r2.created).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user