feat(migrate): plans importer
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
25
migrate/sources/plans.js
Normal file
25
migrate/sources/plans.js
Normal file
@@ -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 };
|
||||
}
|
||||
2
tests/fixtures/plans/alpha.md
vendored
Normal file
2
tests/fixtures/plans/alpha.md
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Alpha Plan
|
||||
body one
|
||||
2
tests/fixtures/plans/beta.md
vendored
Normal file
2
tests/fixtures/plans/beta.md
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Beta
|
||||
body two
|
||||
29
tests/migrate/plans.test.js
Normal file
29
tests/migrate/plans.test.js
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user