(root: string, rel: string)
| 186 | * stable `id` (one-time migration when opening a plain CSV as a database). |
| 187 | */ |
| 188 | export async function readDatabase(root: string, rel: string): Promise<DatabaseDoc> { |
| 189 | const csvAbs = databaseDataPath(root, rel) |
| 190 | let csvText: string |
| 191 | try { |
| 192 | csvText = await fs.readFile(csvAbs, 'utf8') |
| 193 | } catch (err) { |
| 194 | if (isMissing(err)) throw new Error(`Database not found: ${rel}`) |
| 195 | throw err |
| 196 | } |
| 197 | |
| 198 | const existing = await readSidecar(root, rel) |
| 199 | if (existing) { |
| 200 | const rows = parseRows(csvText, existing.fields, existing.idFieldId, randomUUID) |
| 201 | const pageHasContent = await readPageContentFlags(root, existing.pages) |
| 202 | return hydrate(rel, existing, rows, pageHasContent) |
| 203 | } |
| 204 | |
| 205 | // Adopt a plain CSV: infer + materialize. |
| 206 | const grid = parseCsv(csvText) |
| 207 | const headers = grid[0] ?? [] |
| 208 | const { fields, idFieldId } = inferFields(headers, grid.slice(1, 1 + SCHEMA_SAMPLE_ROWS), randomUUID) |
| 209 | const { views, activeViewId } = buildDefaultViews(fields, randomUUID) |
| 210 | const sidecar: DatabaseSidecar = { version: 1, idFieldId, fields, views, activeViewId } |
| 211 | const rows = parseRows(csvText, fields, idFieldId, randomUUID) |
| 212 | await persistSidecar(root, rel, sidecar) |
| 213 | await writeFileAtomic(csvAbs, serializeRows(rows, fields)) // canonicalize + persist ids |
| 214 | return hydrate(rel, sidecar, rows) |
| 215 | } |
| 216 | |
| 217 | /** Persist rows to the CSV (schema/header come from the sidecar). */ |
| 218 | export async function writeDatabaseRows( |
no test coverage detected