( root: string, csvRel: string, sidecarRel: string )
| 1356 | } |
| 1357 | |
| 1358 | async function migrateOneLegacyDatabase( |
| 1359 | root: string, |
| 1360 | csvRel: string, |
| 1361 | sidecarRel: string |
| 1362 | ): Promise<boolean> { |
| 1363 | const name = path.basename(csvRel).replace(/\.csv$/i, '') |
| 1364 | const parentRel = csvRel.includes('/') ? csvRel.slice(0, csvRel.lastIndexOf('/')) : '' |
| 1365 | const formDirRel = parentRel ? `${parentRel}/${name}${FORM_DIR_SUFFIX}` : `${name}${FORM_DIR_SUFFIX}` |
| 1366 | const formDirAbs = resolveSafe(root, formDirRel) |
| 1367 | // Idempotent / collision safety: never clobber an existing folder. |
| 1368 | try { |
| 1369 | await fs.access(formDirAbs) |
| 1370 | return false |
| 1371 | } catch { |
| 1372 | /* target free — proceed */ |
| 1373 | } |
| 1374 | |
| 1375 | let sidecar: Record<string, unknown> = {} |
| 1376 | try { |
| 1377 | sidecar = JSON.parse(await fs.readFile(resolveSafe(root, sidecarRel), 'utf8')) as Record< |
| 1378 | string, |
| 1379 | unknown |
| 1380 | > |
| 1381 | } catch { |
| 1382 | /* unreadable sidecar — still migrate the data, infer schema on next open */ |
| 1383 | } |
| 1384 | |
| 1385 | await fs.mkdir(formDirAbs, { recursive: true }) |
| 1386 | |
| 1387 | // Move record-page notes into the folder; rewrite pages → relative basenames. |
| 1388 | const pages = |
| 1389 | sidecar.pages && typeof sidecar.pages === 'object' |
| 1390 | ? (sidecar.pages as Record<string, unknown>) |
| 1391 | : {} |
| 1392 | const newPages: Record<string, string> = {} |
| 1393 | const oldPageDirs = new Set<string>() |
| 1394 | for (const [rowId, p] of Object.entries(pages)) { |
| 1395 | if (typeof p !== 'string') continue |
| 1396 | const srcAbs = resolveSafe(root, p) |
| 1397 | try { |
| 1398 | await fs.access(srcAbs) |
| 1399 | } catch { |
| 1400 | continue // page note already gone — drop the mapping |
| 1401 | } |
| 1402 | const finalTitle = await uniqueTitle(formDirAbs, sanitizeNoteTitle(path.basename(p, '.md'))) |
| 1403 | await fs.rename(srcAbs, resolveSafe(root, `${formDirRel}/${finalTitle}.md`)) |
| 1404 | newPages[rowId] = `${finalTitle}.md` |
| 1405 | if (p.includes('/')) oldPageDirs.add(p.slice(0, p.lastIndexOf('/'))) |
| 1406 | } |
| 1407 | |
| 1408 | // Move the data file, write schema.json (relative pages), drop the old sidecar. |
| 1409 | await fs.rename(resolveSafe(root, csvRel), resolveSafe(root, `${formDirRel}/${FORM_DATA_FILE}`)) |
| 1410 | const outSidecar: Record<string, unknown> = { ...sidecar } |
| 1411 | if (Object.keys(newPages).length > 0) outSidecar.pages = newPages |
| 1412 | else delete outSidecar.pages |
| 1413 | await writeFileAtomic( |
| 1414 | resolveSafe(root, `${formDirRel}/${FORM_SCHEMA_FILE}`), |
| 1415 | `${JSON.stringify(outSidecar, null, 2)}\n` |
no test coverage detected