Update schema and config file types. Allow js, ts, json, yaml, yml
This commit is contained in:
+25
-8
@@ -3,10 +3,14 @@ import path from "path";
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import grabDBDir from "../utils/grab-db-dir";
|
import grabDBDir from "../utils/grab-db-dir";
|
||||||
import { AppData } from "../data/app-data";
|
|
||||||
import { dumpDatabase } from "../utils/mariadb-dump-restore";
|
import { dumpDatabase } from "../utils/mariadb-dump-restore";
|
||||||
import { writeExportArchive } from "../utils/export-archive";
|
import { writeExportArchive } from "../utils/export-archive";
|
||||||
import trimExports from "../utils/trim-exports";
|
import trimExports from "../utils/trim-exports";
|
||||||
|
import {
|
||||||
|
resolveDataFile,
|
||||||
|
supportedDataFileNames,
|
||||||
|
} from "../utils/resolve-and-load-data-file";
|
||||||
|
import { AppData } from "../data/app-data";
|
||||||
|
|
||||||
function defaultExportFileName(
|
function defaultExportFileName(
|
||||||
dbName: string,
|
dbName: string,
|
||||||
@@ -18,7 +22,7 @@ function defaultExportFileName(
|
|||||||
export default function () {
|
export default function () {
|
||||||
return new Command("export")
|
return new Command("export")
|
||||||
.description(
|
.description(
|
||||||
"Export database SQL dump + schema.ts into a portable archive",
|
"Export database SQL dump + schema into a portable archive",
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"-o, --output <path>",
|
"-o, --output <path>",
|
||||||
@@ -39,10 +43,19 @@ export default function () {
|
|||||||
fs.mkdirSync(export_dir, { recursive: true });
|
fs.mkdirSync(export_dir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const schemaPath = path.join(db_dir, AppData.DbSchemaFileName);
|
const schemaResolved =
|
||||||
if (!fs.existsSync(schemaPath)) {
|
(global.SCHEMA_FILE_PATH &&
|
||||||
|
fs.existsSync(global.SCHEMA_FILE_PATH) && {
|
||||||
|
path: global.SCHEMA_FILE_PATH,
|
||||||
|
basename: path.basename(global.SCHEMA_FILE_PATH),
|
||||||
|
}) ||
|
||||||
|
resolveDataFile(db_dir, AppData.DbSchemaFileName);
|
||||||
|
|
||||||
|
if (!schemaResolved) {
|
||||||
console.error(
|
console.error(
|
||||||
chalk.red(`Schema file not found: ${schemaPath}`),
|
chalk.red(
|
||||||
|
`Schema file not found in \`${db_dir}\` (${supportedDataFileNames(AppData.DbSchemaFileName)})`,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -67,10 +80,14 @@ export default function () {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const sql = await dumpDatabase(config);
|
const sql = await dumpDatabase(config);
|
||||||
const schemaTs = fs.readFileSync(schemaPath, "utf-8");
|
const schema = fs.readFileSync(schemaResolved.path, "utf-8");
|
||||||
|
|
||||||
await writeExportArchive({
|
await writeExportArchive({
|
||||||
contents: { sql, schemaTs },
|
contents: {
|
||||||
|
sql,
|
||||||
|
schema,
|
||||||
|
schemaFileName: schemaResolved.basename,
|
||||||
|
},
|
||||||
outPath,
|
outPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,7 +100,7 @@ export default function () {
|
|||||||
);
|
);
|
||||||
console.log(
|
console.log(
|
||||||
chalk.dim(
|
chalk.dim(
|
||||||
`Contains: dump.sql + ${AppData.DbSchemaFileName}`,
|
`Contains: dump.sql + ${schemaResolved.basename}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|||||||
+25
-10
@@ -6,13 +6,14 @@ import { select } from "@inquirer/prompts";
|
|||||||
import grabDBDir from "../utils/grab-db-dir";
|
import grabDBDir from "../utils/grab-db-dir";
|
||||||
import grabSortedExports from "../utils/grab-sorted-exports";
|
import grabSortedExports from "../utils/grab-sorted-exports";
|
||||||
import grabBackupData from "../utils/grab-backup-data";
|
import grabBackupData from "../utils/grab-backup-data";
|
||||||
import { AppData } from "../data/app-data";
|
|
||||||
import { restoreDatabase } from "../utils/mariadb-dump-restore";
|
import { restoreDatabase } from "../utils/mariadb-dump-restore";
|
||||||
import {
|
import {
|
||||||
isArchivePath,
|
isArchivePath,
|
||||||
isSqlPath,
|
isSqlPath,
|
||||||
readExportArchive,
|
readExportArchive,
|
||||||
} from "../utils/export-archive";
|
} from "../utils/export-archive";
|
||||||
|
import { resolveDataFile } from "../utils/resolve-and-load-data-file";
|
||||||
|
import { AppData } from "../data/app-data";
|
||||||
|
|
||||||
function formatChoice(name: string, index: number): string {
|
function formatChoice(name: string, index: number): string {
|
||||||
const { backup_date } = grabBackupData({ backup_name: name });
|
const { backup_date } = grabBackupData({ backup_name: name });
|
||||||
@@ -25,7 +26,7 @@ function formatChoice(name: string, index: number): string {
|
|||||||
export default function () {
|
export default function () {
|
||||||
return new Command("import")
|
return new Command("import")
|
||||||
.description(
|
.description(
|
||||||
"Import an SQL dump, or a full export archive (SQL + schema.ts)",
|
"Import an SQL dump, or a full export archive (SQL + schema)",
|
||||||
)
|
)
|
||||||
.argument(
|
.argument(
|
||||||
"[file]",
|
"[file]",
|
||||||
@@ -33,11 +34,11 @@ export default function () {
|
|||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"--sql-only",
|
"--sql-only",
|
||||||
"When importing an archive, restore SQL only (skip writing schema.ts)",
|
"When importing an archive, restore SQL only (skip writing schema)",
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"--schema-only",
|
"--schema-only",
|
||||||
"When importing an archive, write schema.ts only (skip SQL restore)",
|
"When importing an archive, write schema only (skip SQL restore)",
|
||||||
)
|
)
|
||||||
.action(async (fileArg: string | undefined, opts) => {
|
.action(async (fileArg: string | undefined, opts) => {
|
||||||
console.log(`Importing database ...`);
|
console.log(`Importing database ...`);
|
||||||
@@ -113,7 +114,8 @@ export default function () {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { sql, schemaTs } = await readExportArchive(filePath);
|
const { sql, schema, schemaFileName } =
|
||||||
|
await readExportArchive(filePath);
|
||||||
|
|
||||||
if (!opts.schemaOnly) {
|
if (!opts.schemaOnly) {
|
||||||
await restoreDatabase(config, sql);
|
await restoreDatabase(config, sql);
|
||||||
@@ -121,14 +123,27 @@ export default function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!opts.sqlOnly) {
|
if (!opts.sqlOnly) {
|
||||||
const schemaPath = path.join(
|
const existing = resolveDataFile(
|
||||||
db_dir,
|
db_dir,
|
||||||
AppData.DbSchemaFileName,
|
AppData.DbSchemaFileName,
|
||||||
);
|
);
|
||||||
fs.writeFileSync(schemaPath, schemaTs, "utf-8");
|
const schemaPath = path.join(db_dir, schemaFileName);
|
||||||
console.log(
|
|
||||||
chalk.green(`Schema written → ${schemaPath}`),
|
// Remove a differently-named schema so only one format remains
|
||||||
);
|
if (
|
||||||
|
existing &&
|
||||||
|
path.resolve(existing.path) !== path.resolve(schemaPath)
|
||||||
|
) {
|
||||||
|
fs.unlinkSync(existing.path);
|
||||||
|
console.log(
|
||||||
|
chalk.dim(
|
||||||
|
`Removed previous schema file: ${existing.basename}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(schemaPath, schema, "utf-8");
|
||||||
|
console.log(chalk.green(`Schema written → ${schemaPath}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default function () {
|
|||||||
|
|
||||||
if (!config.typedef_file_path) {
|
if (!config.typedef_file_path) {
|
||||||
console.error(
|
console.error(
|
||||||
`\`typedef_file_path\` is required in bun-mariadb.config.ts to generate types.`,
|
`\`typedef_file_path\` is required in bun-mariadb.config to generate types.`,
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
export const AppData = {
|
export const AppData = {
|
||||||
ConfigFileName: "bun-mariadb.config.ts",
|
ConfigFileName: "bun-mariadb.config",
|
||||||
MaxBackups: 10,
|
MaxBackups: 10,
|
||||||
MaxExports: 10,
|
MaxExports: 10,
|
||||||
DefaultBackupDirName: ".backups",
|
DefaultBackupDirName: ".backups",
|
||||||
DefaultExportDirName: ".exports",
|
DefaultExportDirName: ".exports",
|
||||||
DbSchemaManagerTableName: "__db_schema_manager__",
|
DbSchemaManagerTableName: "__db_schema_manager__",
|
||||||
DbSchemaFileName: "schema.ts",
|
DbSchemaFileName: "schema",
|
||||||
|
/** Priority order when resolving config/schema files */
|
||||||
|
SupportedDataFileExtensions: [".ts", ".js", ".json", ".yaml", ".yml"] as const,
|
||||||
MaxInitRetries: 50,
|
MaxInitRetries: 50,
|
||||||
InitRetryIntervalMilliseconds: 5000,
|
InitRetryIntervalMilliseconds: 5000,
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
+34
-19
@@ -8,6 +8,10 @@ import {
|
|||||||
RequiredENVs,
|
RequiredENVs,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import setMariaDBClient from "./set-mariadb-client";
|
import setMariaDBClient from "./set-mariadb-client";
|
||||||
|
import {
|
||||||
|
resolveAndLoadDataFile,
|
||||||
|
supportedDataFileNames,
|
||||||
|
} from "../utils/resolve-and-load-data-file";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* # Declare Global Variables
|
* # Declare Global Variables
|
||||||
@@ -16,6 +20,8 @@ declare global {
|
|||||||
var CONFIG: BunMariaDBConfig;
|
var CONFIG: BunMariaDBConfig;
|
||||||
var DB_SCHEMA: BUN_MARIADB_DatabaseSchemaType;
|
var DB_SCHEMA: BUN_MARIADB_DatabaseSchemaType;
|
||||||
var MARIADB_CLIENT: Bun.SQL;
|
var MARIADB_CLIENT: Bun.SQL;
|
||||||
|
var CONFIG_FILE_PATH: string;
|
||||||
|
var SCHEMA_FILE_PATH: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function init(): void {
|
export default function init(): void {
|
||||||
@@ -23,21 +29,23 @@ export default function init(): void {
|
|||||||
const { ROOT_DIR } = grabDirNames();
|
const { ROOT_DIR } = grabDirNames();
|
||||||
const { ConfigFileName } = AppData;
|
const { ConfigFileName } = AppData;
|
||||||
|
|
||||||
const ConfigFilePath = path.join(ROOT_DIR, ConfigFileName);
|
const loadedConfig = resolveAndLoadDataFile<BunMariaDBConfig>(
|
||||||
|
ROOT_DIR,
|
||||||
|
ConfigFileName,
|
||||||
|
);
|
||||||
|
|
||||||
if (!fs.existsSync(ConfigFilePath)) {
|
if (!loadedConfig) {
|
||||||
console.error(
|
console.error(
|
||||||
`Please create a \`${ConfigFileName}\` file at the root of your project.`,
|
`Please create a ${supportedDataFileNames(ConfigFileName)} file at the root of your project.`,
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConfigImport = require(ConfigFilePath);
|
const Config = loadedConfig.data;
|
||||||
const Config = ConfigImport["default"] as BunMariaDBConfig;
|
|
||||||
|
|
||||||
if (!Config) {
|
if (!Config || typeof Config !== "object") {
|
||||||
console.error(
|
console.error(
|
||||||
`No default export from \`${ConfigFilePath}\`. Please export a default module.`,
|
`Invalid config in \`${loadedConfig.path}\`. Expected a config object${loadedConfig.format === "ts" || loadedConfig.format === "js" ? " (export default)" : ""}.`,
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -63,7 +71,7 @@ export default function init(): void {
|
|||||||
|
|
||||||
if (!Config.db_dir) {
|
if (!Config.db_dir) {
|
||||||
console.error(
|
console.error(
|
||||||
`\`db_dir\` is required in your config. This directory holds all database related configuration. Also note that a \`${AppData["DbSchemaFileName"]}\` file is also required in this directory to define your database schema`,
|
`\`db_dir\` is required in your config. This directory holds all database related configuration. Also note that a ${supportedDataFileNames(AppData.DbSchemaFileName)} file is also required in this directory to define your database schema`,
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -73,19 +81,27 @@ export default function init(): void {
|
|||||||
fs.mkdirSync(db_dir, { recursive: true });
|
fs.mkdirSync(db_dir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const DBSchemaFilePath = path.join(db_dir, AppData["DbSchemaFileName"]);
|
const loadedSchema =
|
||||||
|
resolveAndLoadDataFile<BUN_MARIADB_DatabaseSchemaType>(
|
||||||
|
db_dir,
|
||||||
|
AppData.DbSchemaFileName,
|
||||||
|
);
|
||||||
|
|
||||||
if (!fs.existsSync(DBSchemaFilePath)) {
|
if (!loadedSchema) {
|
||||||
console.error(
|
console.error(
|
||||||
`Please create a schema file at \`${DBSchemaFilePath}\`. Don't forget to export a default module from this file.`,
|
`Please create a schema file (${supportedDataFileNames(AppData.DbSchemaFileName)}) in \`${db_dir}\`${loadedConfig.format === "ts" || loadedConfig.format === "js" ? ". Don't forget to export a default module from .ts/.js files." : "."}`,
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const DbSchemaImport = require(DBSchemaFilePath);
|
const DbSchema = loadedSchema.data;
|
||||||
const DbSchema = DbSchemaImport[
|
|
||||||
"default"
|
if (!DbSchema || typeof DbSchema !== "object") {
|
||||||
] as BUN_MARIADB_DatabaseSchemaType;
|
console.error(
|
||||||
|
`Invalid schema in \`${loadedSchema.path}\`. Expected a schema object${loadedSchema.format === "ts" || loadedSchema.format === "js" ? " (export default)" : ""}.`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
const backup_dir =
|
const backup_dir =
|
||||||
Config.db_backup_dir || AppData["DefaultBackupDirName"];
|
Config.db_backup_dir || AppData["DefaultBackupDirName"];
|
||||||
@@ -95,16 +111,15 @@ export default function init(): void {
|
|||||||
fs.mkdirSync(BackupDir, { recursive: true });
|
fs.mkdirSync(BackupDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const ExportDir = path.resolve(
|
const ExportDir = path.resolve(db_dir, AppData["DefaultExportDirName"]);
|
||||||
db_dir,
|
|
||||||
AppData["DefaultExportDirName"],
|
|
||||||
);
|
|
||||||
if (!fs.existsSync(ExportDir)) {
|
if (!fs.existsSync(ExportDir)) {
|
||||||
fs.mkdirSync(ExportDir, { recursive: true });
|
fs.mkdirSync(ExportDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
global.CONFIG = Config;
|
global.CONFIG = Config;
|
||||||
global.DB_SCHEMA = DbSchema;
|
global.DB_SCHEMA = DbSchema;
|
||||||
|
global.CONFIG_FILE_PATH = loadedConfig.path;
|
||||||
|
global.SCHEMA_FILE_PATH = loadedSchema.path;
|
||||||
|
|
||||||
if (!global.CONFIG) {
|
if (!global.CONFIG) {
|
||||||
console.error(`Couldn't grab global Config.`);
|
console.error(`Couldn't grab global Config.`);
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export default async function syncIndexes({
|
|||||||
protectedIndexNames.add(field.fieldName);
|
protectedIndexNames.add(field.fieldName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const constraint of table.uniqueConstraints || []) {
|
for (const constraint of table.uniqueConstraints || []) {
|
||||||
if (constraint.constraintName) {
|
if (constraint.constraintName) {
|
||||||
protectedIndexNames.add(constraint.constraintName);
|
protectedIndexNames.add(constraint.constraintName);
|
||||||
@@ -94,7 +95,9 @@ export default async function syncIndexes({
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const schemaIndex = table.indexes?.find((i) => i.indexName === indexName);
|
const schemaIndex = table.indexes?.find(
|
||||||
|
(i) => i.indexName === indexName,
|
||||||
|
);
|
||||||
|
|
||||||
if (!schemaIndex) {
|
if (!schemaIndex) {
|
||||||
console.log(`Dropping index: ${indexName}`);
|
console.log(`Dropping index: ${indexName}`);
|
||||||
|
|||||||
+64
-28
@@ -2,6 +2,7 @@ import fs from "fs";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { AppData } from "../data/app-data";
|
import { AppData } from "../data/app-data";
|
||||||
import grabDirNames from "../data/grab-dir-names";
|
import grabDirNames from "../data/grab-dir-names";
|
||||||
|
import { isSchemaFileName } from "./resolve-and-load-data-file";
|
||||||
|
|
||||||
export const ExportArchiveMembers = {
|
export const ExportArchiveMembers = {
|
||||||
SqlFileName: "dump.sql",
|
SqlFileName: "dump.sql",
|
||||||
@@ -10,7 +11,9 @@ export const ExportArchiveMembers = {
|
|||||||
|
|
||||||
export type ExportArchiveContents = {
|
export type ExportArchiveContents = {
|
||||||
sql: string;
|
sql: string;
|
||||||
schemaTs: string;
|
schema: string;
|
||||||
|
/** Archive member basename, e.g. schema.ts / schema.json / schema.yaml */
|
||||||
|
schemaFileName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ARCHIVE_EXTENSIONS = [".tar.gz", ".tgz", ".tar", ".zip"] as const;
|
const ARCHIVE_EXTENSIONS = [".tar.gz", ".tgz", ".tar", ".zip"] as const;
|
||||||
@@ -25,7 +28,7 @@ export 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 async function writeExportArchive({
|
export async function writeExportArchive({
|
||||||
contents,
|
contents,
|
||||||
@@ -42,7 +45,7 @@ export async function writeExportArchive({
|
|||||||
|
|
||||||
const members = {
|
const members = {
|
||||||
[ExportArchiveMembers.SqlFileName]: contents.sql,
|
[ExportArchiveMembers.SqlFileName]: contents.sql,
|
||||||
[ExportArchiveMembers.SchemaFileName]: contents.schemaTs,
|
[contents.schemaFileName]: contents.schema,
|
||||||
};
|
};
|
||||||
|
|
||||||
const gzip = lower.endsWith(".gz") || lower.endsWith(".tgz");
|
const gzip = lower.endsWith(".gz") || lower.endsWith(".tgz");
|
||||||
@@ -73,12 +76,7 @@ export async function readExportArchive(
|
|||||||
(await readArchiveMember(files, ExportArchiveMembers.SqlFileName)) ??
|
(await readArchiveMember(files, ExportArchiveMembers.SqlFileName)) ??
|
||||||
(await readFirstMatching(files, (name) => name.endsWith(".sql")));
|
(await readFirstMatching(files, (name) => name.endsWith(".sql")));
|
||||||
|
|
||||||
const schemaTs =
|
const schemaEntry = await readFirstSchemaMember(files);
|
||||||
(await readArchiveMember(
|
|
||||||
files,
|
|
||||||
ExportArchiveMembers.SchemaFileName,
|
|
||||||
)) ??
|
|
||||||
(await readFirstMatching(files, (name) => name.endsWith("schema.ts")));
|
|
||||||
|
|
||||||
if (!sql) {
|
if (!sql) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -86,20 +84,23 @@ export async function readExportArchive(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!schemaTs) {
|
if (!schemaEntry) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Archive is missing schema TypeScript (expected \`${ExportArchiveMembers.SchemaFileName}\`)`,
|
`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(
|
async function readArchiveMember(
|
||||||
files: Map<string, File>,
|
files: Map<string, File>,
|
||||||
name: string,
|
name: string,
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
// Exact match, or basename match for nested paths
|
|
||||||
for (const [entry, file] of files) {
|
for (const [entry, file] of files) {
|
||||||
if (entry === name || path.basename(entry) === name) {
|
if (entry === name || path.basename(entry) === name) {
|
||||||
return await file.text();
|
return await file.text();
|
||||||
@@ -120,6 +121,21 @@ async function readFirstMatching(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function readFirstSchemaMember(
|
||||||
|
files: Map<string, File>,
|
||||||
|
): Promise<{ content: string; fileName: string } | null> {
|
||||||
|
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({
|
async function writeZipArchive({
|
||||||
contents,
|
contents,
|
||||||
outPath,
|
outPath,
|
||||||
@@ -137,12 +153,9 @@ async function writeZipArchive({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const sqlPath = path.join(tempDir, ExportArchiveMembers.SqlFileName);
|
const sqlPath = path.join(tempDir, ExportArchiveMembers.SqlFileName);
|
||||||
const schemaPath = path.join(
|
const schemaPath = path.join(tempDir, contents.schemaFileName);
|
||||||
tempDir,
|
|
||||||
ExportArchiveMembers.SchemaFileName,
|
|
||||||
);
|
|
||||||
fs.writeFileSync(sqlPath, contents.sql, "utf-8");
|
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 absOut = path.resolve(outPath);
|
||||||
const proc = Bun.spawn(
|
const proc = Bun.spawn(
|
||||||
@@ -152,7 +165,7 @@ async function writeZipArchive({
|
|||||||
"-j",
|
"-j",
|
||||||
absOut,
|
absOut,
|
||||||
ExportArchiveMembers.SqlFileName,
|
ExportArchiveMembers.SqlFileName,
|
||||||
ExportArchiveMembers.SchemaFileName,
|
contents.schemaFileName,
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
cwd: tempDir,
|
cwd: tempDir,
|
||||||
@@ -208,25 +221,24 @@ async function readZipArchive(
|
|||||||
const sql = findFileContents(tempDir, (name) =>
|
const sql = findFileContents(tempDir, (name) =>
|
||||||
name === ExportArchiveMembers.SqlFileName || name.endsWith(".sql"),
|
name === ExportArchiveMembers.SqlFileName || name.endsWith(".sql"),
|
||||||
);
|
);
|
||||||
const schemaTs = findFileContents(
|
const schemaHit = findSchemaFile(tempDir);
|
||||||
tempDir,
|
|
||||||
(name) =>
|
|
||||||
name === ExportArchiveMembers.SchemaFileName ||
|
|
||||||
name.endsWith("schema.ts"),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!sql) {
|
if (!sql) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Archive is missing SQL dump (expected \`${ExportArchiveMembers.SqlFileName}\`)`,
|
`Archive is missing SQL dump (expected \`${ExportArchiveMembers.SqlFileName}\`)`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!schemaTs) {
|
if (!schemaHit) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Archive is missing schema TypeScript (expected \`${ExportArchiveMembers.SchemaFileName}\`)`,
|
`Archive is missing schema file (expected \`${AppData.DbSchemaFileName}.ts|json|yaml|yml\`)`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { sql, schemaTs };
|
return {
|
||||||
|
sql,
|
||||||
|
schema: schemaHit.content,
|
||||||
|
schemaFileName: schemaHit.fileName,
|
||||||
|
};
|
||||||
} finally {
|
} finally {
|
||||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
@@ -250,3 +262,27 @@ function findFileContents(
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findSchemaFile(
|
||||||
|
dir: string,
|
||||||
|
): { content: string; fileName: string } | null {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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