Project Files
src / engine / index-manager.ts
import path from "node:path";
import fs from "node:fs/promises";
import { existsSync } from "node:fs";
import type {
DatabaseIndex,
DatabaseInfo,
TableInfo,
TableMeta,
} from "../types";
import { NotFoundError, DuplicateError, FilesystemError } from "../errors";
function now(): string {
return new Date().toISOString();
}
function dbDir(dataRoot: string, database: string): string {
return path.join(dataRoot, "databases", database);
}
function metaPath(dataRoot: string, database: string): string {
return path.join(dbDir(dataRoot, database), ".metaon");
}
function databasesDir(dataRoot: string): string {
return path.join(dataRoot, "databases");
}
export async function databaseExists(
dataRoot: string,
name: string,
): Promise<boolean> {
return existsSync(dbDir(dataRoot, name));
}
export async function tableExists(
dataRoot: string,
database: string,
table: string,
): Promise<boolean> {
try {
const idx = await getIndex(dataRoot, database);
return table in idx.tables;
} catch {
return false;
}
}
export async function getIndex(
dataRoot: string,
database: string,
): Promise<DatabaseIndex> {
const fp = metaPath(dataRoot, database);
try {
const raw = await fs.readFile(fp, "utf-8");
return JSON.parse(raw) as DatabaseIndex;
} catch (cause) {
if ((cause as NodeJS.ErrnoException).code === "ENOENT") {
throw new NotFoundError(`Database "${database}" not found`);
}
throw new FilesystemError(`Cannot read index for "${database}"`);
}
}
export async function updateIndex(
dataRoot: string,
database: string,
updates: Partial<DatabaseIndex>,
): Promise<void> {
const current = await getIndex(dataRoot, database);
const merged: DatabaseIndex = { ...current, ...updates, updatedAt: now() };
const fp = metaPath(dataRoot, database);
await fs.writeFile(fp, JSON.stringify(merged, null, 2), "utf-8");
}
export async function initDatabase(
dataRoot: string,
name: string,
displayName?: string,
description?: string,
): Promise<void> {
const dir = dbDir(dataRoot, name);
if (existsSync(dir)) {
throw new DuplicateError(`Database "${name}" already exists`);
}
await fs.mkdir(dir, { recursive: true });
const index: DatabaseIndex = {
version: 1,
name,
displayName: displayName ?? name,
description,
createdAt: now(),
updatedAt: now(),
tables: {},
};
await fs.writeFile(
metaPath(dataRoot, name),
JSON.stringify(index, null, 2),
"utf-8",
);
}
export async function deleteDatabase(
dataRoot: string,
name: string,
): Promise<void> {
const dir = dbDir(dataRoot, name);
if (!existsSync(dir)) {
throw new NotFoundError(`Database "${name}" not found`);
}
await fs.rm(dir, { recursive: true, force: true });
}
export async function getDatabases(dataRoot: string): Promise<DatabaseInfo[]> {
const dir = databasesDir(dataRoot);
if (!existsSync(dir)) {
return [];
}
const entries = await fs.readdir(dir, { withFileTypes: true });
const infos: DatabaseInfo[] = [];
for (const entry of entries) {
if (!entry.isDirectory()) continue;
try {
const idx = await getIndex(dataRoot, entry.name);
const tableCount = Object.keys(idx.tables).length;
const totalSizeBytes = Object.values(idx.tables).reduce(
(sum, t) => sum + (t.sizeBytes ?? 0),
0,
);
infos.push({
name: entry.name,
displayName: idx.displayName,
tableCount,
totalSizeBytes,
createdAt: idx.createdAt,
updatedAt: idx.updatedAt,
});
} catch {
// skip corrupt databases
}
}
return infos;
}
export async function getTables(
dataRoot: string,
database: string,
): Promise<TableInfo[]> {
const idx = await getIndex(dataRoot, database);
return Object.entries(idx.tables).map(([name, meta]) => ({
name,
displayName: meta.displayName,
rowCount: meta.rowCount,
createdAt: meta.createdAt,
updatedAt: idx.updatedAt,
sizeBytes: meta.sizeBytes,
}));
}