diff --git a/migrate/sources/plans.js b/migrate/sources/plans.js new file mode 100644 index 0000000..b6752f8 --- /dev/null +++ b/migrate/sources/plans.js @@ -0,0 +1,25 @@ +import { readdirSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import * as pages from '../../lib/db/repos/pages.js'; +import * as map from '../../lib/db/repos/migration_map.js'; + +const SYS = { kind: 'system', id: null }; +const slugify = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '').slice(0, 60) || 'page'; +function titleOf(body, file) { + const m = body.match(/^#\s+(.+)$/m); + return m ? m[1].trim() : file.replace(/\.md$/, ''); +} + +export async function importPlans({ dir, spaceId, dryRun = false }) { + let created = 0; + for (const file of readdirSync(dir).filter(f => f.endsWith('.md')).sort()) { + if (await map.seen('plans', file, 'page')) continue; + const body = readFileSync(join(dir, file), 'utf8'); + const title = titleOf(body, file); + created++; + if (dryRun) continue; + const page = await pages.create({ space_id: spaceId, slug: slugify(title), title, body_md: body }, SYS); + await map.record('plans', file, 'page', page.id); + } + return { created }; +} diff --git a/tests/fixtures/plans/alpha.md b/tests/fixtures/plans/alpha.md new file mode 100644 index 0000000..17bf6d3 --- /dev/null +++ b/tests/fixtures/plans/alpha.md @@ -0,0 +1,2 @@ +# Alpha Plan +body one diff --git a/tests/fixtures/plans/beta.md b/tests/fixtures/plans/beta.md new file mode 100644 index 0000000..a85e4a4 --- /dev/null +++ b/tests/fixtures/plans/beta.md @@ -0,0 +1,2 @@ +# Beta +body two diff --git a/tests/migrate/plans.test.js b/tests/migrate/plans.test.js new file mode 100644 index 0000000..1f1f696 --- /dev/null +++ b/tests/migrate/plans.test.js @@ -0,0 +1,29 @@ +import { describe, it, expect, beforeAll } from 'vitest'; +import { fileURLToPath } from 'url'; +import { pool } from '../../lib/db/pool.js'; +import { resetDb } from '../helpers/db.js'; +import { migrateUp } from '../../lib/db/migrate.js'; +import { ensureSpace } from '../../migrate/spaces.js'; +import { importPlans } from '../../migrate/sources/plans.js'; + +const DIR = fileURLToPath(new URL('../fixtures/plans', import.meta.url)); +let spaceId; +beforeAll(async () => { await resetDb(); await migrateUp(); spaceId = await ensureSpace('plans', 'Plans'); }); + +describe('plans importer', () => { + it('imports each .md as a page, idempotently', async () => { + const r1 = await importPlans({ dir: DIR, spaceId }); + 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(['Alpha Plan', 'Beta']); + const r2 = await importPlans({ dir: DIR, spaceId }); // re-run + expect(r2.created).toBe(0); + }); + it('dry-run writes nothing', async () => { + await resetDb(); await migrateUp(); const s = await ensureSpace('plans', 'Plans'); + const r = await importPlans({ dir: DIR, spaceId: s, dryRun: true }); + expect(r.created).toBe(2); + const { rows } = await pool.query(`SELECT count(*)::int n FROM pages WHERE space_id=$1`, [s]); + expect(rows[0].n).toBe(0); + }); +});