- init() loads config, schema and the SQLite client once into globals - all CRUD queries go through lib/db-handler.ts (optional per-call config) - split db-schema-manager.ts into lib/schema/* with FK-aware table ordering - add bun:test tests for schema ordering, FK constraints and schema sync - config/schema files can be .ts/.js/.json/.yaml/.yml; db_schema_file_name is now optional (defaults to schema.* in db_dir) - add export/import commands (database snapshot + schema archives) - package exports map, type re-exports, hardened publish.sh - validate table name in delete Co-Authored-By: Claude Opus 5.5 <[email protected]>
133 lines
3.5 KiB
TypeScript
133 lines
3.5 KiB
TypeScript
import fs from "fs";
|
|
import path from "path";
|
|
import { AppData } from "../data/app-data";
|
|
|
|
export type DataFileFormat = "ts" | "js" | "json" | "yaml";
|
|
|
|
export type ResolvedDataFile = {
|
|
path: string;
|
|
basename: string;
|
|
extension: (typeof AppData.SupportedDataFileExtensions)[number];
|
|
format: DataFileFormat;
|
|
};
|
|
|
|
export type LoadedDataFile<T> = ResolvedDataFile & {
|
|
data: T;
|
|
};
|
|
|
|
function extensionToFormat(
|
|
extension: (typeof AppData.SupportedDataFileExtensions)[number],
|
|
): DataFileFormat {
|
|
switch (extension) {
|
|
case ".ts":
|
|
return "ts";
|
|
case ".js":
|
|
return "js";
|
|
case ".json":
|
|
return "json";
|
|
case ".yaml":
|
|
case ".yml":
|
|
return "yaml";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve a basename (no extension) to an existing file among supported formats.
|
|
* Priority: .ts > .js > .json > .yaml > .yml
|
|
* Errors if multiple matches exist.
|
|
*/
|
|
export function resolveDataFile(
|
|
dir: string,
|
|
baseName: string,
|
|
): ResolvedDataFile | null {
|
|
const matches: ResolvedDataFile[] = [];
|
|
|
|
for (const extension of AppData.SupportedDataFileExtensions) {
|
|
const filePath = path.join(dir, `${baseName}${extension}`);
|
|
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
|
matches.push({
|
|
path: filePath,
|
|
basename: `${baseName}${extension}`,
|
|
extension,
|
|
format: extensionToFormat(extension),
|
|
});
|
|
}
|
|
}
|
|
|
|
if (matches.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
if (matches.length > 1) {
|
|
const found = matches.map((m) => m.basename).join(", ");
|
|
throw new Error(
|
|
`Multiple \`${baseName}\` files found (${found}). Keep only one of: ${AppData.SupportedDataFileExtensions.join(", ")}`,
|
|
);
|
|
}
|
|
|
|
return matches[0]!;
|
|
}
|
|
|
|
export function supportedDataFileNames(baseName: string): string {
|
|
return AppData.SupportedDataFileExtensions.map(
|
|
(ext) => `\`${baseName}${ext}\``,
|
|
).join(", ");
|
|
}
|
|
|
|
export function isSchemaFileName(name: string): boolean {
|
|
const base = path.basename(name);
|
|
if (base === AppData.DbSchemaFileName) {
|
|
return true;
|
|
}
|
|
return AppData.SupportedDataFileExtensions.some(
|
|
(ext) => base === `${AppData.DbSchemaFileName}${ext}`,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Load a data file as a plain object.
|
|
* - .ts / .js: require() and use default export (or module itself)
|
|
* - .json: JSON.parse
|
|
* - .yaml / .yml: Bun.YAML.parse
|
|
*/
|
|
export function loadDataFile<T>(resolved: ResolvedDataFile): T {
|
|
if (resolved.format === "ts" || resolved.format === "js") {
|
|
const imported = require(resolved.path);
|
|
const data =
|
|
imported && typeof imported === "object" && "default" in imported
|
|
? imported.default
|
|
: imported;
|
|
|
|
if (data == null) {
|
|
throw new Error(
|
|
`No default export from \`${resolved.path}\`. Please export a default module.`,
|
|
);
|
|
}
|
|
|
|
return data as T;
|
|
}
|
|
|
|
const text = fs.readFileSync(resolved.path, "utf-8");
|
|
|
|
if (resolved.format === "json") {
|
|
return JSON.parse(text) as T;
|
|
}
|
|
|
|
return Bun.YAML.parse(text) as T;
|
|
}
|
|
|
|
export function resolveAndLoadDataFile<T>(
|
|
dir: string,
|
|
baseName: string,
|
|
): LoadedDataFile<T> | null {
|
|
const resolved = resolveDataFile(dir, baseName);
|
|
if (!resolved) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
...resolved,
|
|
data: loadDataFile<T>(resolved),
|
|
};
|
|
}
|