Port bun-mariadb conventions: init globals, db handler, split schema sync
- 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]>
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
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),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user