Updates
This commit is contained in:
Vendored
+5
-3
@@ -1,15 +1,17 @@
|
||||
export declare const ExportArchiveMembers: {
|
||||
readonly SqlFileName: "dump.sql";
|
||||
readonly SchemaFileName: "schema.ts";
|
||||
readonly SchemaFileName: "schema";
|
||||
};
|
||||
export type ExportArchiveContents = {
|
||||
sql: string;
|
||||
schemaTs: string;
|
||||
schema: string;
|
||||
/** Archive member basename, e.g. schema.ts / schema.json / schema.yaml */
|
||||
schemaFileName: string;
|
||||
};
|
||||
export declare function isArchivePath(filePath: string): boolean;
|
||||
export declare function isSqlPath(filePath: string): boolean;
|
||||
/**
|
||||
* Create a portable export archive (tar / tar.gz / zip) with dump.sql + schema.ts.
|
||||
* Create a portable export archive (tar / tar.gz / zip) with dump.sql + schema file.
|
||||
*/
|
||||
export declare function writeExportArchive({ contents, outPath, }: {
|
||||
contents: ExportArchiveContents;
|
||||
|
||||
Vendored
+55
-16
@@ -2,6 +2,7 @@ import fs from "fs";
|
||||
import path from "path";
|
||||
import { AppData } from "../data/app-data";
|
||||
import grabDirNames from "../data/grab-dir-names";
|
||||
import { isSchemaFileName } from "./resolve-and-load-data-file";
|
||||
export const ExportArchiveMembers = {
|
||||
SqlFileName: "dump.sql",
|
||||
SchemaFileName: AppData.DbSchemaFileName,
|
||||
@@ -15,7 +16,7 @@ export function isSqlPath(filePath) {
|
||||
return filePath.toLowerCase().endsWith(".sql");
|
||||
}
|
||||
/**
|
||||
* Create a portable export archive (tar / tar.gz / zip) with dump.sql + schema.ts.
|
||||
* Create a portable export archive (tar / tar.gz / zip) with dump.sql + schema file.
|
||||
*/
|
||||
export async function writeExportArchive({ contents, outPath, }) {
|
||||
const lower = outPath.toLowerCase();
|
||||
@@ -25,7 +26,7 @@ export async function writeExportArchive({ contents, outPath, }) {
|
||||
}
|
||||
const members = {
|
||||
[ExportArchiveMembers.SqlFileName]: contents.sql,
|
||||
[ExportArchiveMembers.SchemaFileName]: contents.schemaTs,
|
||||
[contents.schemaFileName]: contents.schema,
|
||||
};
|
||||
const gzip = lower.endsWith(".gz") || lower.endsWith(".tgz");
|
||||
if (gzip) {
|
||||
@@ -48,18 +49,20 @@ export async function readExportArchive(archivePath) {
|
||||
const files = await archive.files();
|
||||
const sql = (await readArchiveMember(files, ExportArchiveMembers.SqlFileName)) ??
|
||||
(await readFirstMatching(files, (name) => name.endsWith(".sql")));
|
||||
const schemaTs = (await readArchiveMember(files, ExportArchiveMembers.SchemaFileName)) ??
|
||||
(await readFirstMatching(files, (name) => name.endsWith("schema.ts")));
|
||||
const schemaEntry = await readFirstSchemaMember(files);
|
||||
if (!sql) {
|
||||
throw new Error(`Archive is missing SQL dump (expected \`${ExportArchiveMembers.SqlFileName}\`)`);
|
||||
}
|
||||
if (!schemaTs) {
|
||||
throw new Error(`Archive is missing schema TypeScript (expected \`${ExportArchiveMembers.SchemaFileName}\`)`);
|
||||
if (!schemaEntry) {
|
||||
throw new Error(`Archive is missing schema file (expected \`${AppData.DbSchemaFileName}.ts|json|yaml|yml\`)`);
|
||||
}
|
||||
return { sql, schemaTs };
|
||||
return {
|
||||
sql,
|
||||
schema: schemaEntry.content,
|
||||
schemaFileName: schemaEntry.fileName,
|
||||
};
|
||||
}
|
||||
async function readArchiveMember(files, name) {
|
||||
// Exact match, or basename match for nested paths
|
||||
for (const [entry, file] of files) {
|
||||
if (entry === name || path.basename(entry) === name) {
|
||||
return await file.text();
|
||||
@@ -75,15 +78,27 @@ async function readFirstMatching(files, predicate) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
async function readFirstSchemaMember(files) {
|
||||
for (const [entry, file] of files) {
|
||||
const base = path.basename(entry);
|
||||
if (isSchemaFileName(base)) {
|
||||
return {
|
||||
content: await file.text(),
|
||||
fileName: base === AppData.DbSchemaFileName ? "schema.ts" : base,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
async function writeZipArchive({ contents, outPath, }) {
|
||||
const { BUN_MARIADB_TEMP_DIR } = grabDirNames();
|
||||
const tempDir = path.join(BUN_MARIADB_TEMP_DIR, `export-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
try {
|
||||
const sqlPath = path.join(tempDir, ExportArchiveMembers.SqlFileName);
|
||||
const schemaPath = path.join(tempDir, ExportArchiveMembers.SchemaFileName);
|
||||
const schemaPath = path.join(tempDir, contents.schemaFileName);
|
||||
fs.writeFileSync(sqlPath, contents.sql, "utf-8");
|
||||
fs.writeFileSync(schemaPath, contents.schemaTs, "utf-8");
|
||||
fs.writeFileSync(schemaPath, contents.schema, "utf-8");
|
||||
const absOut = path.resolve(outPath);
|
||||
const proc = Bun.spawn([
|
||||
"zip",
|
||||
@@ -91,7 +106,7 @@ async function writeZipArchive({ contents, outPath, }) {
|
||||
"-j",
|
||||
absOut,
|
||||
ExportArchiveMembers.SqlFileName,
|
||||
ExportArchiveMembers.SchemaFileName,
|
||||
contents.schemaFileName,
|
||||
], {
|
||||
cwd: tempDir,
|
||||
stdout: "pipe",
|
||||
@@ -127,15 +142,18 @@ async function readZipArchive(archivePath) {
|
||||
throw new Error(`unzip failed (exit ${exitCode}): ${stderr || "unknown error"}. Ensure \`unzip\` is installed.`);
|
||||
}
|
||||
const sql = findFileContents(tempDir, (name) => name === ExportArchiveMembers.SqlFileName || name.endsWith(".sql"));
|
||||
const schemaTs = findFileContents(tempDir, (name) => name === ExportArchiveMembers.SchemaFileName ||
|
||||
name.endsWith("schema.ts"));
|
||||
const schemaHit = findSchemaFile(tempDir);
|
||||
if (!sql) {
|
||||
throw new Error(`Archive is missing SQL dump (expected \`${ExportArchiveMembers.SqlFileName}\`)`);
|
||||
}
|
||||
if (!schemaTs) {
|
||||
throw new Error(`Archive is missing schema TypeScript (expected \`${ExportArchiveMembers.SchemaFileName}\`)`);
|
||||
if (!schemaHit) {
|
||||
throw new Error(`Archive is missing schema file (expected \`${AppData.DbSchemaFileName}.ts|json|yaml|yml\`)`);
|
||||
}
|
||||
return { sql, schemaTs };
|
||||
return {
|
||||
sql,
|
||||
schema: schemaHit.content,
|
||||
schemaFileName: schemaHit.fileName,
|
||||
};
|
||||
}
|
||||
finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
@@ -157,3 +175,24 @@ function findFileContents(dir, predicate) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function findSchemaFile(dir) {
|
||||
const stack = [dir];
|
||||
while (stack.length) {
|
||||
const current = stack.pop();
|
||||
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
||||
const full = path.join(current, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
stack.push(full);
|
||||
}
|
||||
else if (isSchemaFileName(entry.name)) {
|
||||
return {
|
||||
content: fs.readFileSync(full, "utf-8"),
|
||||
fileName: entry.name === AppData.DbSchemaFileName
|
||||
? "schema.ts"
|
||||
: entry.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
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;
|
||||
};
|
||||
/**
|
||||
* Resolve a basename (no extension) to an existing file among supported formats.
|
||||
* Priority: .ts > .js > .json > .yaml > .yml
|
||||
* Errors if multiple matches exist.
|
||||
*/
|
||||
export declare function resolveDataFile(dir: string, baseName: string): ResolvedDataFile | null;
|
||||
export declare function supportedDataFileNames(baseName: string): string;
|
||||
export declare function isSchemaFileName(name: string): boolean;
|
||||
/**
|
||||
* 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 declare function loadDataFile<T>(resolved: ResolvedDataFile): T;
|
||||
export declare function resolveAndLoadDataFile<T>(dir: string, baseName: string): LoadedDataFile<T> | null;
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { AppData } from "../data/app-data";
|
||||
function extensionToFormat(extension) {
|
||||
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, baseName) {
|
||||
const matches = [];
|
||||
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) {
|
||||
return AppData.SupportedDataFileExtensions.map((ext) => `\`${baseName}${ext}\``).join(", ");
|
||||
}
|
||||
export function isSchemaFileName(name) {
|
||||
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(resolved) {
|
||||
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;
|
||||
}
|
||||
const text = fs.readFileSync(resolved.path, "utf-8");
|
||||
if (resolved.format === "json") {
|
||||
return JSON.parse(text);
|
||||
}
|
||||
return Bun.YAML.parse(text);
|
||||
}
|
||||
export function resolveAndLoadDataFile(dir, baseName) {
|
||||
const resolved = resolveDataFile(dir, baseName);
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...resolved,
|
||||
data: loadDataFile(resolved),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user