Update .gitignore, add dist directory
This commit is contained in:
parent
9f2db66760
commit
3bbf00cdb0
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,7 +3,6 @@ node_modules
|
|||||||
|
|
||||||
# output
|
# output
|
||||||
out
|
out
|
||||||
dist
|
|
||||||
*.tgz
|
*.tgz
|
||||||
|
|
||||||
# code coverage
|
# code coverage
|
||||||
|
|||||||
2
dist/commands/admin/index.d.ts
vendored
Normal file
2
dist/commands/admin/index.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import { Command } from "commander";
|
||||||
|
export default function (): Command;
|
||||||
37
dist/commands/admin/index.js
vendored
Normal file
37
dist/commands/admin/index.js
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { Command } from "commander";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { select } from "@inquirer/prompts";
|
||||||
|
import listTables from "./list-tables";
|
||||||
|
import runSQL from "./run-sql";
|
||||||
|
export default function () {
|
||||||
|
return new Command("admin")
|
||||||
|
.description("View Tables and Data, Run SQL Queries, Etc.")
|
||||||
|
.action(async () => {
|
||||||
|
console.log(chalk.bold(chalk.blue("\nBun MariaDB Admin\n")));
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const paradigm = await select({
|
||||||
|
message: "Choose an action:",
|
||||||
|
choices: [
|
||||||
|
{ name: "Tables", value: "list_tables" },
|
||||||
|
{ name: "SQL", value: "run_sql" },
|
||||||
|
{ name: chalk.dim("✕ Exit"), value: "exit" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
if (paradigm === "exit")
|
||||||
|
break;
|
||||||
|
if (paradigm === "list_tables") {
|
||||||
|
const result = await listTables();
|
||||||
|
if (result === "__exit__")
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (paradigm === "run_sql")
|
||||||
|
await runSQL();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error.message);
|
||||||
|
}
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
}
|
||||||
3
dist/commands/admin/list-tables.d.ts
vendored
Normal file
3
dist/commands/admin/list-tables.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
type Params = {};
|
||||||
|
export default function listTables(params?: Params): Promise<"__exit__" | void>;
|
||||||
|
export {};
|
||||||
81
dist/commands/admin/list-tables.js
vendored
Normal file
81
dist/commands/admin/list-tables.js
vendored
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import chalk from "chalk";
|
||||||
|
import { select } from "@inquirer/prompts";
|
||||||
|
import { AppData } from "../../data/app-data";
|
||||||
|
import showEntries from "./show-entries";
|
||||||
|
import showFields from "./show-fields";
|
||||||
|
import dbHandler from "../../lib/db-handler";
|
||||||
|
import MariaDBQuoteGen from "../../lib/schema/mariadb-quote-gen";
|
||||||
|
export default async function listTables(params) {
|
||||||
|
const tables = (await dbHandler({
|
||||||
|
query: `SELECT table_name FROM ${MariaDBQuoteGen(AppData["DbSchemaManagerTableName"])}`,
|
||||||
|
})).payload;
|
||||||
|
if (!tables?.length) {
|
||||||
|
console.log(chalk.yellow("\nNo tables found.\n"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
while (true) {
|
||||||
|
const tableName = await select({
|
||||||
|
message: "Select a table:",
|
||||||
|
choices: [
|
||||||
|
...tables.map((t) => ({
|
||||||
|
name: t.table_name,
|
||||||
|
value: t.table_name,
|
||||||
|
})),
|
||||||
|
{ name: chalk.dim("← Go Back"), value: "__back__" },
|
||||||
|
{ name: chalk.dim("✕ Exit"), value: "__exit__" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
if (tableName === "__back__")
|
||||||
|
break;
|
||||||
|
if (tableName === "__exit__")
|
||||||
|
return "__exit__";
|
||||||
|
while (true) {
|
||||||
|
const action = await select({
|
||||||
|
message: `"${tableName}" — choose an action:`,
|
||||||
|
choices: [
|
||||||
|
{ name: "Entries", value: "entries" },
|
||||||
|
{ name: "Fields", value: "fields" },
|
||||||
|
{ name: "Schema", value: "schema" },
|
||||||
|
{ name: chalk.dim("← Go Back"), value: "__back__" },
|
||||||
|
{ name: chalk.dim("✕ Exit"), value: "__exit__" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
if (action === "__back__")
|
||||||
|
break;
|
||||||
|
if (action === "__exit__")
|
||||||
|
return "__exit__";
|
||||||
|
if (action === "entries") {
|
||||||
|
const result = await showEntries({ tableName });
|
||||||
|
if (result === "__exit__")
|
||||||
|
return "__exit__";
|
||||||
|
}
|
||||||
|
if (action === "fields") {
|
||||||
|
const result = await showFields({ tableName });
|
||||||
|
if (result === "__exit__")
|
||||||
|
return "__exit__";
|
||||||
|
}
|
||||||
|
if (action === "schema") {
|
||||||
|
const columns = (await dbHandler({
|
||||||
|
query: `SELECT ORDINAL_POSITION, COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_KEY, EXTRA FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION`,
|
||||||
|
values: [tableName],
|
||||||
|
})).payload;
|
||||||
|
console.log(`\n${chalk.bold(`Schema for "${tableName}":`)} \n`);
|
||||||
|
if (columns?.length) {
|
||||||
|
console.table(columns.map((c) => ({
|
||||||
|
"#": c.ORDINAL_POSITION,
|
||||||
|
Name: c.COLUMN_NAME,
|
||||||
|
Type: c.COLUMN_TYPE,
|
||||||
|
Nullable: c.IS_NULLABLE,
|
||||||
|
Default: c.COLUMN_DEFAULT ?? "(none)",
|
||||||
|
Key: c.COLUMN_KEY || "",
|
||||||
|
Extra: c.EXTRA || "",
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log(chalk.yellow("No columns found."));
|
||||||
|
}
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
dist/commands/admin/run-sql.d.ts
vendored
Normal file
3
dist/commands/admin/run-sql.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
type Params = {};
|
||||||
|
export default function runSQL(params?: Params): Promise<void>;
|
||||||
|
export {};
|
||||||
25
dist/commands/admin/run-sql.js
vendored
Normal file
25
dist/commands/admin/run-sql.js
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { input } from "@inquirer/prompts";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import dbHandler from "../../lib/db-handler";
|
||||||
|
export default async function runSQL(params) {
|
||||||
|
const sql = await input({
|
||||||
|
message: "Enter SQL query:",
|
||||||
|
validate: (val) => val.trim().length > 0 || "Query cannot be empty",
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const res = await dbHandler({ query: sql });
|
||||||
|
if (res.payload?.length) {
|
||||||
|
console.log(`\n${chalk.bold(`Result (${res.payload.length} row${res.payload.length !== 1 ? "s" : ""}):`)} \n`);
|
||||||
|
console.table(res.payload);
|
||||||
|
}
|
||||||
|
else if (res.success) {
|
||||||
|
console.log(chalk.green(`\nSuccess! Affected rows: ${res.insert_return?.affected_rows ?? res.count ?? 0}, Last insert ID: ${res.insert_return?.last_insert_id ?? "—"}\n`));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.error(chalk.red(`\nSQL Error: ${res.error || res.msg}\n`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(chalk.red(`\nSQL Error: ${error.message}\n`));
|
||||||
|
}
|
||||||
|
}
|
||||||
5
dist/commands/admin/show-entries.d.ts
vendored
Normal file
5
dist/commands/admin/show-entries.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
type Params = {
|
||||||
|
tableName: string;
|
||||||
|
};
|
||||||
|
export default function showEntries({ tableName }: Params): Promise<"__exit__" | undefined>;
|
||||||
|
export {};
|
||||||
91
dist/commands/admin/show-entries.js
vendored
Normal file
91
dist/commands/admin/show-entries.js
vendored
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import chalk from "chalk";
|
||||||
|
import { select, input } from "@inquirer/prompts";
|
||||||
|
import dbHandler from "../../lib/db-handler";
|
||||||
|
import MariaDBQuoteGen from "../../lib/schema/mariadb-quote-gen";
|
||||||
|
const LIMIT = 50;
|
||||||
|
export default async function showEntries({ tableName }) {
|
||||||
|
let page = 0;
|
||||||
|
let searchField = null;
|
||||||
|
let searchTerm = null;
|
||||||
|
const quotedTable = MariaDBQuoteGen(tableName);
|
||||||
|
while (true) {
|
||||||
|
const offset = page * LIMIT;
|
||||||
|
const rows = searchTerm
|
||||||
|
? (await dbHandler({
|
||||||
|
query: `SELECT * FROM ${quotedTable} WHERE ${MariaDBQuoteGen(searchField)} LIKE ? LIMIT ${LIMIT} OFFSET ${offset}`,
|
||||||
|
values: [`%${searchTerm}%`],
|
||||||
|
})).payload
|
||||||
|
: (await dbHandler({
|
||||||
|
query: `SELECT * FROM ${quotedTable} LIMIT ${LIMIT} OFFSET ${offset}`,
|
||||||
|
})).payload;
|
||||||
|
const countRow = searchTerm
|
||||||
|
? (await dbHandler({
|
||||||
|
query: `SELECT COUNT(*) as count FROM ${quotedTable} WHERE ${MariaDBQuoteGen(searchField)} LIKE ?`,
|
||||||
|
values: [`%${searchTerm}%`],
|
||||||
|
})).payload
|
||||||
|
: (await dbHandler({
|
||||||
|
query: `SELECT COUNT(*) as count FROM ${quotedTable}`,
|
||||||
|
})).payload;
|
||||||
|
const total = Number(countRow?.[0]?.count || 0);
|
||||||
|
const searchInfo = searchTerm
|
||||||
|
? chalk.dim(` · searching "${searchField}" = "${searchTerm}"`)
|
||||||
|
: "";
|
||||||
|
if (!rows) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`\n${chalk.bold(tableName)} — Page ${page + 1}${searchInfo} (${rows.length} of ${total}):\n`);
|
||||||
|
if (rows.length) {
|
||||||
|
console.table(rows);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log(chalk.yellow("No rows found."));
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
const choices = [];
|
||||||
|
if (page > 0)
|
||||||
|
choices.push({ name: "← Previous Page", value: "prev" });
|
||||||
|
if (offset + rows.length < total)
|
||||||
|
choices.push({ name: "Next Page →", value: "next" });
|
||||||
|
choices.push({ name: "Search by Field", value: "search" });
|
||||||
|
if (searchTerm)
|
||||||
|
choices.push({ name: "Clear Search", value: "clear_search" });
|
||||||
|
choices.push({ name: chalk.dim("← Go Back"), value: "__back__" });
|
||||||
|
choices.push({ name: chalk.dim("✕ Exit"), value: "__exit__" });
|
||||||
|
const action = await select({ message: "Navigate:", choices });
|
||||||
|
if (action === "__back__")
|
||||||
|
break;
|
||||||
|
if (action === "__exit__")
|
||||||
|
return "__exit__";
|
||||||
|
if (action === "next")
|
||||||
|
page++;
|
||||||
|
if (action === "prev")
|
||||||
|
page--;
|
||||||
|
if (action === "clear_search") {
|
||||||
|
searchField = null;
|
||||||
|
searchTerm = null;
|
||||||
|
page = 0;
|
||||||
|
}
|
||||||
|
if (action === "search") {
|
||||||
|
const columns = (await dbHandler({
|
||||||
|
query: `SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION`,
|
||||||
|
values: [tableName],
|
||||||
|
})).payload;
|
||||||
|
if (!columns?.length) {
|
||||||
|
console.log(chalk.yellow("No columns found for search."));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
searchField = await select({
|
||||||
|
message: "Search by field:",
|
||||||
|
choices: columns.map((c) => ({
|
||||||
|
name: c.COLUMN_NAME,
|
||||||
|
value: c.COLUMN_NAME,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
searchTerm = await input({
|
||||||
|
message: `Search term for "${searchField}":`,
|
||||||
|
validate: (v) => v.trim().length > 0 || "Cannot be empty",
|
||||||
|
});
|
||||||
|
page = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
dist/commands/admin/show-fields.d.ts
vendored
Normal file
5
dist/commands/admin/show-fields.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
type Params = {
|
||||||
|
tableName: string;
|
||||||
|
};
|
||||||
|
export default function showFields({ tableName, }: Params): Promise<"__exit__" | void>;
|
||||||
|
export {};
|
||||||
60
dist/commands/admin/show-fields.js
vendored
Normal file
60
dist/commands/admin/show-fields.js
vendored
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import chalk from "chalk";
|
||||||
|
import { select } from "@inquirer/prompts";
|
||||||
|
import dbHandler from "../../lib/db-handler";
|
||||||
|
export default async function showFields({ tableName, }) {
|
||||||
|
const columns = (await dbHandler({
|
||||||
|
query: `SELECT ORDINAL_POSITION, COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_KEY, EXTRA, COLUMN_COMMENT FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION`,
|
||||||
|
values: [tableName],
|
||||||
|
})).payload;
|
||||||
|
if (!columns?.length) {
|
||||||
|
console.log(chalk.yellow(`\nNo columns found for "${tableName}".\n`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const indexes = (await dbHandler({
|
||||||
|
query: `SELECT INDEX_NAME, COLUMN_NAME, NON_UNIQUE, INDEX_TYPE FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? ORDER BY INDEX_NAME, SEQ_IN_INDEX`,
|
||||||
|
values: [tableName],
|
||||||
|
})).payload;
|
||||||
|
const foreignKeys = (await dbHandler({
|
||||||
|
query: `SELECT CONSTRAINT_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND REFERENCED_TABLE_NAME IS NOT NULL`,
|
||||||
|
values: [tableName],
|
||||||
|
})).payload;
|
||||||
|
while (true) {
|
||||||
|
const fieldName = await select({
|
||||||
|
message: `"${tableName}" — select a field:`,
|
||||||
|
choices: [
|
||||||
|
...columns.map((c) => ({
|
||||||
|
name: c.COLUMN_NAME,
|
||||||
|
value: c.COLUMN_NAME,
|
||||||
|
})),
|
||||||
|
{ name: chalk.dim("← Go Back"), value: "__back__" },
|
||||||
|
{ name: chalk.dim("✕ Exit"), value: "__exit__" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
if (fieldName === "__back__")
|
||||||
|
break;
|
||||||
|
if (fieldName === "__exit__")
|
||||||
|
return "__exit__";
|
||||||
|
const col = columns.find((c) => c.COLUMN_NAME === fieldName);
|
||||||
|
const colIndexes = indexes?.filter((i) => i.COLUMN_NAME === fieldName) || [];
|
||||||
|
const fk = foreignKeys?.find((f) => f.COLUMN_NAME === fieldName);
|
||||||
|
console.log(`\n${chalk.bold(`Field: "${fieldName}"`)}\n`);
|
||||||
|
console.log(` ${chalk.dim("Table")} ${tableName}`);
|
||||||
|
console.log(` ${chalk.dim("Column #")} ${col.ORDINAL_POSITION}`);
|
||||||
|
console.log(` ${chalk.dim("Type")} ${col.COLUMN_TYPE}`);
|
||||||
|
console.log(` ${chalk.dim("Primary Key")} ${col.COLUMN_KEY === "PRI" ? chalk.green("YES") : "NO"}`);
|
||||||
|
console.log(` ${chalk.dim("Not Null")} ${col.IS_NULLABLE === "NO" ? chalk.yellow("YES") : "NO"}`);
|
||||||
|
console.log(` ${chalk.dim("Default")} ${col.COLUMN_DEFAULT ?? chalk.italic("(none)")}`);
|
||||||
|
console.log(` ${chalk.dim("Extra")} ${col.EXTRA || chalk.italic("(none)")}`);
|
||||||
|
console.log(` ${chalk.dim("Indexed")} ${colIndexes.length ? chalk.cyan(colIndexes.map((i) => i.INDEX_NAME).join(", ")) : "NO"}`);
|
||||||
|
if (fk) {
|
||||||
|
console.log(` ${chalk.dim("Foreign Key")} ${chalk.magenta(`${fk.REFERENCED_TABLE_NAME}(${fk.REFERENCED_COLUMN_NAME})`)}`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log(` ${chalk.dim("Foreign Key")} NO`);
|
||||||
|
}
|
||||||
|
if (col.COLUMN_COMMENT) {
|
||||||
|
console.log(` ${chalk.dim("Comment")} ${col.COLUMN_COMMENT}`);
|
||||||
|
}
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
}
|
||||||
2
dist/commands/backup.d.ts
vendored
Normal file
2
dist/commands/backup.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import { Command } from "commander";
|
||||||
|
export default function (): Command;
|
||||||
33
dist/commands/backup.js
vendored
Normal file
33
dist/commands/backup.js
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { Command } from "commander";
|
||||||
|
import path from "path";
|
||||||
|
import fs from "fs";
|
||||||
|
import grabDBDir from "../utils/grab-db-dir";
|
||||||
|
import grabDBBackupFileName from "../utils/grab-db-backup-file-name";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import trimBackups from "../utils/trim-backups";
|
||||||
|
import { dumpDatabase } from "../utils/mariadb-dump-restore";
|
||||||
|
export default function () {
|
||||||
|
return new Command("backup")
|
||||||
|
.description("Backup MariaDB database (SQL dump)")
|
||||||
|
.action(async () => {
|
||||||
|
console.log(`Backing up database ...`);
|
||||||
|
const config = global.CONFIG;
|
||||||
|
const { backup_dir } = grabDBDir({ config });
|
||||||
|
if (!fs.existsSync(backup_dir)) {
|
||||||
|
fs.mkdirSync(backup_dir, { recursive: true });
|
||||||
|
}
|
||||||
|
const backup_file_name = grabDBBackupFileName({ config });
|
||||||
|
const backup_path = path.join(backup_dir, backup_file_name);
|
||||||
|
try {
|
||||||
|
const sql = await dumpDatabase(config);
|
||||||
|
fs.writeFileSync(backup_path, sql, "utf-8");
|
||||||
|
trimBackups({ config });
|
||||||
|
console.log(`${chalk.bold(chalk.green(`DB Backup Success!`))} → ${backup_path}`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(chalk.red(`Backup ERROR => ${error.message}`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
2
dist/commands/export.d.ts
vendored
Normal file
2
dist/commands/export.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import { Command } from "commander";
|
||||||
|
export default function (): Command;
|
||||||
62
dist/commands/export.js
vendored
Normal file
62
dist/commands/export.js
vendored
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { Command } from "commander";
|
||||||
|
import path from "path";
|
||||||
|
import fs from "fs";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import grabDBDir from "../utils/grab-db-dir";
|
||||||
|
import { AppData } from "../data/app-data";
|
||||||
|
import { dumpDatabase } from "../utils/mariadb-dump-restore";
|
||||||
|
import { writeExportArchive } from "../utils/export-archive";
|
||||||
|
import trimExports from "../utils/trim-exports";
|
||||||
|
function defaultExportFileName(dbName, format) {
|
||||||
|
return `${dbName}-${Date.now()}.${format}`;
|
||||||
|
}
|
||||||
|
export default function () {
|
||||||
|
return new Command("export")
|
||||||
|
.description("Export database SQL dump + schema.ts into a portable archive")
|
||||||
|
.option("-o, --output <path>", "Output archive path (.tar.gz or .zip). Defaults to db exports dir")
|
||||||
|
.option("-f, --format <format>", "Archive format when --output is omitted: tar.gz | zip", "tar.gz")
|
||||||
|
.action(async (opts) => {
|
||||||
|
console.log(`Exporting database ...`);
|
||||||
|
const config = global.CONFIG;
|
||||||
|
const { db_dir, export_dir } = grabDBDir({ config });
|
||||||
|
if (!fs.existsSync(export_dir)) {
|
||||||
|
fs.mkdirSync(export_dir, { recursive: true });
|
||||||
|
}
|
||||||
|
const schemaPath = path.join(db_dir, AppData.DbSchemaFileName);
|
||||||
|
if (!fs.existsSync(schemaPath)) {
|
||||||
|
console.error(chalk.red(`Schema file not found: ${schemaPath}`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const formatRaw = String(opts.format || "tar.gz").toLowerCase();
|
||||||
|
const format = formatRaw === "zip" ? "zip" : "tar.gz";
|
||||||
|
let outPath;
|
||||||
|
if (opts.output) {
|
||||||
|
outPath = path.resolve(opts.output);
|
||||||
|
const parent = path.dirname(outPath);
|
||||||
|
if (!fs.existsSync(parent)) {
|
||||||
|
fs.mkdirSync(parent, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
outPath = path.join(export_dir, defaultExportFileName(config.db_name, format));
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const sql = await dumpDatabase(config);
|
||||||
|
const schemaTs = fs.readFileSync(schemaPath, "utf-8");
|
||||||
|
await writeExportArchive({
|
||||||
|
contents: { sql, schemaTs },
|
||||||
|
outPath,
|
||||||
|
});
|
||||||
|
if (path.dirname(outPath) === export_dir) {
|
||||||
|
trimExports({ config });
|
||||||
|
}
|
||||||
|
console.log(`${chalk.bold(chalk.green(`DB Export Success!`))} → ${outPath}`);
|
||||||
|
console.log(chalk.dim(`Contains: dump.sql + ${AppData.DbSchemaFileName}`));
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(chalk.red(`Export ERROR => ${error.message}`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
2
dist/commands/import.d.ts
vendored
Normal file
2
dist/commands/import.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import { Command } from "commander";
|
||||||
|
export default function (): Command;
|
||||||
86
dist/commands/import.js
vendored
Normal file
86
dist/commands/import.js
vendored
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { Command } from "commander";
|
||||||
|
import path from "path";
|
||||||
|
import fs from "fs";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { select } from "@inquirer/prompts";
|
||||||
|
import grabDBDir from "../utils/grab-db-dir";
|
||||||
|
import grabSortedExports from "../utils/grab-sorted-exports";
|
||||||
|
import grabBackupData from "../utils/grab-backup-data";
|
||||||
|
import { AppData } from "../data/app-data";
|
||||||
|
import { restoreDatabase } from "../utils/mariadb-dump-restore";
|
||||||
|
import { isArchivePath, isSqlPath, readExportArchive, } from "../utils/export-archive";
|
||||||
|
function formatChoice(name, index) {
|
||||||
|
const { backup_date } = grabBackupData({ backup_name: name });
|
||||||
|
const time = Number.isNaN(backup_date.getTime())
|
||||||
|
? "unknown date"
|
||||||
|
: `${backup_date.toDateString()} ${backup_date.getHours()}:${backup_date.getMinutes()}:${backup_date.getSeconds().toString().padStart(2, "0")}`;
|
||||||
|
return `#${index + 1}: ${name} (${time})`;
|
||||||
|
}
|
||||||
|
export default function () {
|
||||||
|
return new Command("import")
|
||||||
|
.description("Import an SQL dump, or a full export archive (SQL + schema.ts)")
|
||||||
|
.argument("[file]", "Path to .sql file or export archive (.tar.gz / .tar / .zip)")
|
||||||
|
.option("--sql-only", "When importing an archive, restore SQL only (skip writing schema.ts)")
|
||||||
|
.option("--schema-only", "When importing an archive, write schema.ts only (skip SQL restore)")
|
||||||
|
.action(async (fileArg, opts) => {
|
||||||
|
console.log(`Importing database ...`);
|
||||||
|
const config = global.CONFIG;
|
||||||
|
const { db_dir, export_dir } = grabDBDir({ config });
|
||||||
|
try {
|
||||||
|
let filePath = fileArg ? path.resolve(fileArg) : "";
|
||||||
|
if (!filePath) {
|
||||||
|
const candidates = grabSortedExports({ config }).filter((b) => isArchivePath(b));
|
||||||
|
if (!candidates[0]) {
|
||||||
|
console.error(`No export archives found in \`${export_dir}\`. Use \`export\`, or pass a file path.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const selected = await select({
|
||||||
|
message: "Select an export to import:",
|
||||||
|
choices: candidates.map((b, i) => ({
|
||||||
|
name: formatChoice(b, i),
|
||||||
|
value: b,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
filePath = path.join(export_dir, selected);
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
console.error(chalk.red(`File not found: ${filePath}`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (isSqlPath(filePath)) {
|
||||||
|
if (opts.schemaOnly) {
|
||||||
|
console.error(chalk.red(`--schema-only requires an export archive, not a plain .sql file.`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const sql = fs.readFileSync(filePath, "utf-8");
|
||||||
|
await restoreDatabase(config, sql);
|
||||||
|
console.log(`${chalk.bold(chalk.green(`DB Import Success!`))} ← ${filePath}`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
if (!isArchivePath(filePath)) {
|
||||||
|
console.error(chalk.red(`Unsupported file type: ${filePath}. Use .sql, .tar.gz, .tar, or .zip.`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (opts.sqlOnly && opts.schemaOnly) {
|
||||||
|
console.error(chalk.red(`Cannot combine --sql-only and --schema-only.`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const { sql, schemaTs } = await readExportArchive(filePath);
|
||||||
|
if (!opts.schemaOnly) {
|
||||||
|
await restoreDatabase(config, sql);
|
||||||
|
console.log(chalk.green(`SQL restored from archive`));
|
||||||
|
}
|
||||||
|
if (!opts.sqlOnly) {
|
||||||
|
const schemaPath = path.join(db_dir, AppData.DbSchemaFileName);
|
||||||
|
fs.writeFileSync(schemaPath, schemaTs, "utf-8");
|
||||||
|
console.log(chalk.green(`Schema written → ${schemaPath}`));
|
||||||
|
}
|
||||||
|
console.log(`${chalk.bold(chalk.green(`DB Import Success!`))} ← ${filePath}`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(chalk.red(`Import ERROR => ${error.message}`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
2
dist/commands/index.d.ts
vendored
Normal file
2
dist/commands/index.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
export {};
|
||||||
39
dist/commands/index.js
vendored
Normal file
39
dist/commands/index.js
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
import { program } from "commander";
|
||||||
|
import schema from "./schema";
|
||||||
|
import typedef from "./typedef";
|
||||||
|
import backup from "./backup";
|
||||||
|
import restore from "./restore";
|
||||||
|
import exportCmd from "./export";
|
||||||
|
import importCmd from "./import";
|
||||||
|
import admin from "./admin";
|
||||||
|
import init from "../functions/init";
|
||||||
|
init();
|
||||||
|
/**
|
||||||
|
* # Describe Program
|
||||||
|
*/
|
||||||
|
program
|
||||||
|
.name(`bun-mariadb`)
|
||||||
|
.description(`MariaDB manager for Bun`)
|
||||||
|
.version(`1.0.0`);
|
||||||
|
/**
|
||||||
|
* # Declare Commands
|
||||||
|
*/
|
||||||
|
program.addCommand(schema());
|
||||||
|
program.addCommand(typedef());
|
||||||
|
program.addCommand(backup());
|
||||||
|
program.addCommand(restore());
|
||||||
|
program.addCommand(exportCmd());
|
||||||
|
program.addCommand(importCmd());
|
||||||
|
program.addCommand(admin());
|
||||||
|
/**
|
||||||
|
* # Handle Unavailable Commands
|
||||||
|
*/
|
||||||
|
program.on("command:*", () => {
|
||||||
|
console.error("Invalid command: %s\nSee --help for a list of available commands.", program.args.join(" "));
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* # Parse Arguments
|
||||||
|
*/
|
||||||
|
program.parse(Bun.argv);
|
||||||
2
dist/commands/restore.d.ts
vendored
Normal file
2
dist/commands/restore.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import { Command } from "commander";
|
||||||
|
export default function (): Command;
|
||||||
50
dist/commands/restore.js
vendored
Normal file
50
dist/commands/restore.js
vendored
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { Command } from "commander";
|
||||||
|
import grabDBDir from "../utils/grab-db-dir";
|
||||||
|
import fs from "fs";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import grabSortedBackups from "../utils/grab-sorted-backups";
|
||||||
|
import { select } from "@inquirer/prompts";
|
||||||
|
import grabBackupData from "../utils/grab-backup-data";
|
||||||
|
import path from "path";
|
||||||
|
import { restoreDatabase } from "../utils/mariadb-dump-restore";
|
||||||
|
export default function () {
|
||||||
|
return new Command("restore")
|
||||||
|
.description("Restore MariaDB database from an SQL dump")
|
||||||
|
.action(async () => {
|
||||||
|
console.log(`Restoring database ...`);
|
||||||
|
const config = global.CONFIG;
|
||||||
|
const { backup_dir } = grabDBDir({ config });
|
||||||
|
const backups = grabSortedBackups({ config }).filter((b) => b.endsWith(".sql"));
|
||||||
|
if (!backups?.[0]) {
|
||||||
|
console.error(`No SQL backups to restore. Use the \`backup\` command to create a backup.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const selected_backup = await select({
|
||||||
|
message: "Select a backup:",
|
||||||
|
choices: backups.map((b, i) => {
|
||||||
|
const { backup_date } = grabBackupData({
|
||||||
|
backup_name: b,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
name: `Backup #${i + 1}: ${backup_date.toDateString()} ${backup_date.getHours()}:${backup_date.getMinutes()}:${backup_date.getSeconds().toString().padStart(2, "0")}`,
|
||||||
|
value: b,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const backup_path = path.join(backup_dir, selected_backup);
|
||||||
|
if (!fs.existsSync(backup_path)) {
|
||||||
|
console.error(`Backup file not found: ${backup_path}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const sql = fs.readFileSync(backup_path, "utf-8");
|
||||||
|
await restoreDatabase(config, sql);
|
||||||
|
console.log(`${chalk.bold(chalk.green(`DB Restore Success!`))} ← ${selected_backup}`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(`Backup Restore ERROR => ${error.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
2
dist/commands/schema.d.ts
vendored
Normal file
2
dist/commands/schema.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import { Command } from "commander";
|
||||||
|
export default function (): Command;
|
||||||
44
dist/commands/schema.js
vendored
Normal file
44
dist/commands/schema.js
vendored
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { Command } from "commander";
|
||||||
|
import grabDirNames from "../data/grab-dir-names";
|
||||||
|
import path from "path";
|
||||||
|
import dbSchemaToTypeDef from "../lib/mariadb/schema-to-typedef";
|
||||||
|
import appendDefaultFieldsToDbSchema from "../utils/append-default-fields-to-db-schema";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { writeLiveSchema } from "../functions/live-schema";
|
||||||
|
import createDBSchema from "../lib/schema/create-db-schema";
|
||||||
|
export default function () {
|
||||||
|
return new Command("schema")
|
||||||
|
.description("Build DB From Schema")
|
||||||
|
.option("-t, --typedef", "Generate typescript type definitions")
|
||||||
|
.action(async (opts) => {
|
||||||
|
console.log(`Starting process ...`);
|
||||||
|
const config = global.CONFIG;
|
||||||
|
const dbSchema = global.DB_SCHEMA;
|
||||||
|
const { ROOT_DIR } = grabDirNames();
|
||||||
|
try {
|
||||||
|
const isTypeDef = Boolean(opts.typedef || opts.t);
|
||||||
|
const finaldbSchema = appendDefaultFieldsToDbSchema({
|
||||||
|
dbSchema,
|
||||||
|
});
|
||||||
|
await createDBSchema({
|
||||||
|
db_schema: finaldbSchema,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
if (isTypeDef && config.typedef_file_path) {
|
||||||
|
const out_file = path.resolve(ROOT_DIR, config.typedef_file_path);
|
||||||
|
dbSchemaToTypeDef({
|
||||||
|
dbSchema: finaldbSchema,
|
||||||
|
dst_file: out_file,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
writeLiveSchema({ schema: finaldbSchema });
|
||||||
|
console.log(`${chalk.bold(chalk.green(`DB Schema setup success!`))}`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
2
dist/commands/typedef.d.ts
vendored
Normal file
2
dist/commands/typedef.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import { Command } from "commander";
|
||||||
|
export default function (): Command;
|
||||||
29
dist/commands/typedef.js
vendored
Normal file
29
dist/commands/typedef.js
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { Command } from "commander";
|
||||||
|
import dbSchemaToTypeDef from "../lib/mariadb/schema-to-typedef";
|
||||||
|
import path from "path";
|
||||||
|
import grabDirNames from "../data/grab-dir-names";
|
||||||
|
import appendDefaultFieldsToDbSchema from "../utils/append-default-fields-to-db-schema";
|
||||||
|
import chalk from "chalk";
|
||||||
|
export default function () {
|
||||||
|
return new Command("typedef")
|
||||||
|
.description("Generate TypeScript type definitions from the schema")
|
||||||
|
.action(async () => {
|
||||||
|
console.log(`Creating Type Definition From DB Schema ...`);
|
||||||
|
const config = global.CONFIG;
|
||||||
|
const dbSchema = global.DB_SCHEMA;
|
||||||
|
const { ROOT_DIR } = grabDirNames();
|
||||||
|
const finaldbSchema = appendDefaultFieldsToDbSchema({ dbSchema });
|
||||||
|
if (!config.typedef_file_path) {
|
||||||
|
console.error(`\`typedef_file_path\` is required in bun-mariadb.config.ts to generate types.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const out_file = path.resolve(ROOT_DIR, config.typedef_file_path);
|
||||||
|
dbSchemaToTypeDef({
|
||||||
|
dbSchema: finaldbSchema,
|
||||||
|
dst_file: out_file,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
console.log(`${chalk.bold(chalk.green(`Typedef gen success!`))}`);
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
11
dist/data/app-data.d.ts
vendored
Normal file
11
dist/data/app-data.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export declare const AppData: {
|
||||||
|
readonly ConfigFileName: "bun-mariadb.config.ts";
|
||||||
|
readonly MaxBackups: 10;
|
||||||
|
readonly MaxExports: 10;
|
||||||
|
readonly DefaultBackupDirName: ".backups";
|
||||||
|
readonly DefaultExportDirName: ".exports";
|
||||||
|
readonly DbSchemaManagerTableName: "__db_schema_manager__";
|
||||||
|
readonly DbSchemaFileName: "schema.ts";
|
||||||
|
readonly MaxInitRetries: 50;
|
||||||
|
readonly InitRetryIntervalMilliseconds: 5000;
|
||||||
|
};
|
||||||
11
dist/data/app-data.js
vendored
Normal file
11
dist/data/app-data.js
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export const AppData = {
|
||||||
|
ConfigFileName: "bun-mariadb.config.ts",
|
||||||
|
MaxBackups: 10,
|
||||||
|
MaxExports: 10,
|
||||||
|
DefaultBackupDirName: ".backups",
|
||||||
|
DefaultExportDirName: ".exports",
|
||||||
|
DbSchemaManagerTableName: "__db_schema_manager__",
|
||||||
|
DbSchemaFileName: "schema.ts",
|
||||||
|
MaxInitRetries: 50,
|
||||||
|
InitRetryIntervalMilliseconds: 5000,
|
||||||
|
};
|
||||||
11
dist/data/grab-dir-names.d.ts
vendored
Normal file
11
dist/data/grab-dir-names.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import type { BunMariaDBConfig } from "../types";
|
||||||
|
type Params = {
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
};
|
||||||
|
export default function grabDirNames(params?: Params): {
|
||||||
|
ROOT_DIR: string;
|
||||||
|
BUN_MARIADB_DIR: string;
|
||||||
|
BUN_MARIADB_TEMP_DIR: string;
|
||||||
|
BUN_MARIADB_LIVE_SCHEMA: string;
|
||||||
|
};
|
||||||
|
export {};
|
||||||
13
dist/data/grab-dir-names.js
vendored
Normal file
13
dist/data/grab-dir-names.js
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import path from "path";
|
||||||
|
export default function grabDirNames(params) {
|
||||||
|
const ROOT_DIR = process.cwd();
|
||||||
|
const BUN_MARIADB_DIR = path.join(ROOT_DIR, ".bun-mariadb");
|
||||||
|
const BUN_MARIADB_TEMP_DIR = path.join(BUN_MARIADB_DIR, ".tmp");
|
||||||
|
const BUN_MARIADB_LIVE_SCHEMA = path.join(BUN_MARIADB_DIR, "live-schema.json");
|
||||||
|
return {
|
||||||
|
ROOT_DIR,
|
||||||
|
BUN_MARIADB_DIR,
|
||||||
|
BUN_MARIADB_TEMP_DIR,
|
||||||
|
BUN_MARIADB_LIVE_SCHEMA,
|
||||||
|
};
|
||||||
|
}
|
||||||
6
dist/functions/grab-mariadb-client.d.ts
vendored
Normal file
6
dist/functions/grab-mariadb-client.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { type BunMariaDBConfig } from "../types";
|
||||||
|
type Params = {
|
||||||
|
config: BunMariaDBConfig;
|
||||||
|
};
|
||||||
|
export default function grabMariaDBClient({ config, }: Params): Bun.SQL | undefined;
|
||||||
|
export {};
|
||||||
48
dist/functions/grab-mariadb-client.js
vendored
Normal file
48
dist/functions/grab-mariadb-client.js
vendored
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import path from "path";
|
||||||
|
import grabDirNames from "../data/grab-dir-names";
|
||||||
|
import {} from "../types";
|
||||||
|
import { SQL } from "bun";
|
||||||
|
/** Reuse one client per database name to avoid exhausting MariaDB connections. */
|
||||||
|
const clientCache = new Map();
|
||||||
|
export default function grabMariaDBClient({ config, }) {
|
||||||
|
const cacheKey = config.db_name || "__default__";
|
||||||
|
const cached = clientCache.get(cacheKey);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
// Prefer the process-wide client when it matches the requested database
|
||||||
|
if (global.MARIADB_CLIENT &&
|
||||||
|
(!global.CONFIG?.db_name || global.CONFIG.db_name === config.db_name)) {
|
||||||
|
clientCache.set(cacheKey, global.MARIADB_CLIENT);
|
||||||
|
return global.MARIADB_CLIENT;
|
||||||
|
}
|
||||||
|
const { ROOT_DIR } = grabDirNames();
|
||||||
|
try {
|
||||||
|
const tls = config.ssl_ca
|
||||||
|
? {
|
||||||
|
ca: Bun.file(path.resolve(ROOT_DIR, config.ssl_ca)),
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
};
|
||||||
|
const MariaDBClient = new SQL({
|
||||||
|
hostname: process.env.BUN_MARIADB_SERVER_HOST,
|
||||||
|
username: process.env.BUN_MARIADB_SERVER_USERNAME,
|
||||||
|
password: process.env.BUN_MARIADB_SERVER_PASSWORD,
|
||||||
|
database: config.db_name,
|
||||||
|
port: process.env.BUN_MARIADB_SERVER_PORT
|
||||||
|
? Number(process.env.BUN_MARIADB_SERVER_PORT)
|
||||||
|
: undefined,
|
||||||
|
...config.db_config,
|
||||||
|
tls,
|
||||||
|
adapter: "mariadb",
|
||||||
|
});
|
||||||
|
clientCache.set(cacheKey, MariaDBClient);
|
||||||
|
return MariaDBClient;
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(`Couldn't grab MariaDB Client => ` + error.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
dist/functions/init.d.ts
vendored
Normal file
10
dist/functions/init.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { type BunMariaDBConfig, type BUN_MARIADB_DatabaseSchemaType } from "../types";
|
||||||
|
/**
|
||||||
|
* # Declare Global Variables
|
||||||
|
*/
|
||||||
|
declare global {
|
||||||
|
var CONFIG: BunMariaDBConfig;
|
||||||
|
var DB_SCHEMA: BUN_MARIADB_DatabaseSchemaType;
|
||||||
|
var MARIADB_CLIENT: Bun.SQL;
|
||||||
|
}
|
||||||
|
export default function init(): void;
|
||||||
80
dist/functions/init.js
vendored
Normal file
80
dist/functions/init.js
vendored
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import path from "path";
|
||||||
|
import fs from "fs";
|
||||||
|
import { AppData } from "../data/app-data";
|
||||||
|
import grabDirNames from "../data/grab-dir-names";
|
||||||
|
import { RequiredENVs, } from "../types";
|
||||||
|
import setMariaDBClient from "./set-mariadb-client";
|
||||||
|
export default function init() {
|
||||||
|
try {
|
||||||
|
const { ROOT_DIR } = grabDirNames();
|
||||||
|
const { ConfigFileName } = AppData;
|
||||||
|
const ConfigFilePath = path.join(ROOT_DIR, ConfigFileName);
|
||||||
|
if (!fs.existsSync(ConfigFilePath)) {
|
||||||
|
console.error(`Please create a \`${ConfigFileName}\` file at the root of your project.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const ConfigImport = require(ConfigFilePath);
|
||||||
|
const Config = ConfigImport["default"];
|
||||||
|
if (!Config) {
|
||||||
|
console.error(`No default export from \`${ConfigFilePath}\`. Please export a default module.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (!Config.db_name) {
|
||||||
|
console.error(`\`db_name\` is required in your config`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
RequiredENVs.forEach((env_key) => {
|
||||||
|
if (env_key == "BUN_MARIADB_SERVER_PORT" ||
|
||||||
|
env_key == "BUN_MARIADB_SERVER_SSL_KEY_PATH") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!process.env[env_key]) {
|
||||||
|
console.error(`\`${env_key}\` env variable is required.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!Config.db_dir) {
|
||||||
|
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`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const db_dir = path.resolve(ROOT_DIR, Config.db_dir);
|
||||||
|
if (!fs.existsSync(db_dir)) {
|
||||||
|
fs.mkdirSync(db_dir, { recursive: true });
|
||||||
|
}
|
||||||
|
const DBSchemaFilePath = path.join(db_dir, AppData["DbSchemaFileName"]);
|
||||||
|
if (!fs.existsSync(DBSchemaFilePath)) {
|
||||||
|
console.error(`Please create a schema file at \`${DBSchemaFilePath}\`. Don't forget to export a default module from this file.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const DbSchemaImport = require(DBSchemaFilePath);
|
||||||
|
const DbSchema = DbSchemaImport["default"];
|
||||||
|
const backup_dir = Config.db_backup_dir || AppData["DefaultBackupDirName"];
|
||||||
|
const BackupDir = path.resolve(db_dir, backup_dir);
|
||||||
|
if (!fs.existsSync(BackupDir)) {
|
||||||
|
fs.mkdirSync(BackupDir, { recursive: true });
|
||||||
|
}
|
||||||
|
const ExportDir = path.resolve(db_dir, AppData["DefaultExportDirName"]);
|
||||||
|
if (!fs.existsSync(ExportDir)) {
|
||||||
|
fs.mkdirSync(ExportDir, { recursive: true });
|
||||||
|
}
|
||||||
|
global.CONFIG = Config;
|
||||||
|
global.DB_SCHEMA = DbSchema;
|
||||||
|
if (!global.CONFIG) {
|
||||||
|
console.error(`Couldn't grab global Config.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (!global.DB_SCHEMA) {
|
||||||
|
console.error(`Couldn't grab Database Schema.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
setMariaDBClient({ config: Config });
|
||||||
|
if (!global.MARIADB_CLIENT) {
|
||||||
|
console.error(`Couldn't set MariaDB Client.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(`Initialization ERROR => ` + error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
dist/functions/live-schema.d.ts
vendored
Normal file
7
dist/functions/live-schema.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import type { BUN_MARIADB_DatabaseSchemaType } from "../types";
|
||||||
|
type Params = {
|
||||||
|
schema: BUN_MARIADB_DatabaseSchemaType;
|
||||||
|
};
|
||||||
|
export declare function writeLiveSchema({ schema }: Params): void;
|
||||||
|
export declare function readLiveSchema(): BUN_MARIADB_DatabaseSchemaType | undefined;
|
||||||
|
export {};
|
||||||
15
dist/functions/live-schema.js
vendored
Normal file
15
dist/functions/live-schema.js
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
||||||
|
import grabDirNames from "../data/grab-dir-names";
|
||||||
|
import path from "path";
|
||||||
|
const { BUN_MARIADB_LIVE_SCHEMA } = grabDirNames();
|
||||||
|
export function writeLiveSchema({ schema }) {
|
||||||
|
mkdirSync(path.dirname(BUN_MARIADB_LIVE_SCHEMA), { recursive: true });
|
||||||
|
writeFileSync(BUN_MARIADB_LIVE_SCHEMA, JSON.stringify(schema));
|
||||||
|
}
|
||||||
|
export function readLiveSchema() {
|
||||||
|
if (!existsSync(BUN_MARIADB_LIVE_SCHEMA)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const live_schema = readFileSync(BUN_MARIADB_LIVE_SCHEMA, "utf-8");
|
||||||
|
return JSON.parse(live_schema);
|
||||||
|
}
|
||||||
6
dist/functions/set-mariadb-client.d.ts
vendored
Normal file
6
dist/functions/set-mariadb-client.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { type BunMariaDBConfig } from "../types";
|
||||||
|
type Params = {
|
||||||
|
config: BunMariaDBConfig;
|
||||||
|
};
|
||||||
|
export default function setMariaDBClient({ config }: Params): void;
|
||||||
|
export {};
|
||||||
21
dist/functions/set-mariadb-client.js
vendored
Normal file
21
dist/functions/set-mariadb-client.js
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import {} from "../types";
|
||||||
|
import grabMariaDBClient from "./grab-mariadb-client";
|
||||||
|
export default function setMariaDBClient({ config }) {
|
||||||
|
try {
|
||||||
|
const MariaDBClient = grabMariaDBClient({ config });
|
||||||
|
if (!MariaDBClient) {
|
||||||
|
throw new Error(`Couldn't grab MariaDB Client`);
|
||||||
|
}
|
||||||
|
global.MARIADB_CLIENT = MariaDBClient;
|
||||||
|
// const test = await MariaDBClient.unsafe(`SHOW DATABASES`);
|
||||||
|
// if (!test.count) {
|
||||||
|
// console.error(`MariaDBClient Error: Database not ready.`);
|
||||||
|
// console.log(test);
|
||||||
|
// process.exit(1);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(`ERROR setting MariaDB Client => ` + error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
dist/index.d.ts
vendored
Normal file
20
dist/index.d.ts
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import DbDelete from "./lib/mariadb/db-delete";
|
||||||
|
import DbInsert from "./lib/mariadb/db-insert";
|
||||||
|
import DbSelect from "./lib/mariadb/db-select";
|
||||||
|
import DbSQL from "./lib/mariadb/db-sql";
|
||||||
|
import DbUpdate from "./lib/mariadb/db-update";
|
||||||
|
import grabDbSchema from "./utils/grab-db-schema";
|
||||||
|
import grabJoinFieldsFromQueryObject from "./utils/grab-join-fields-from-query-object";
|
||||||
|
declare const BunMariaDB: {
|
||||||
|
readonly select: typeof DbSelect;
|
||||||
|
readonly insert: typeof DbInsert;
|
||||||
|
readonly update: typeof DbUpdate;
|
||||||
|
readonly delete: typeof DbDelete;
|
||||||
|
readonly sql: typeof DbSQL;
|
||||||
|
readonly utils: {
|
||||||
|
readonly grab_db_schema: typeof grabDbSchema;
|
||||||
|
readonly grab_join_fields_from_query_object: typeof grabJoinFieldsFromQueryObject;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export default BunMariaDB;
|
||||||
|
export type { BunMariaDBConfig, BUN_MARIADB_DatabaseSchemaType, BUN_MARIADB_TableSchemaType, BUN_MARIADB_FieldSchemaType, BUN_MARIADB_IndexSchemaType, BUN_MARIADB_UniqueConstraintSchemaType, BUN_MARIADB_ForeignKeyType, DBResponseObject, ServerQueryParam, } from "./types";
|
||||||
21
dist/index.js
vendored
Normal file
21
dist/index.js
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import init from "./functions/init";
|
||||||
|
import DbDelete from "./lib/mariadb/db-delete";
|
||||||
|
import DbInsert from "./lib/mariadb/db-insert";
|
||||||
|
import DbSelect from "./lib/mariadb/db-select";
|
||||||
|
import DbSQL from "./lib/mariadb/db-sql";
|
||||||
|
import DbUpdate from "./lib/mariadb/db-update";
|
||||||
|
import grabDbSchema from "./utils/grab-db-schema";
|
||||||
|
import grabJoinFieldsFromQueryObject from "./utils/grab-join-fields-from-query-object";
|
||||||
|
init();
|
||||||
|
const BunMariaDB = {
|
||||||
|
select: DbSelect,
|
||||||
|
insert: DbInsert,
|
||||||
|
update: DbUpdate,
|
||||||
|
delete: DbDelete,
|
||||||
|
sql: DbSQL,
|
||||||
|
utils: {
|
||||||
|
grab_db_schema: grabDbSchema,
|
||||||
|
grab_join_fields_from_query_object: grabJoinFieldsFromQueryObject,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default BunMariaDB;
|
||||||
15
dist/lib/db-handler.d.ts
vendored
Normal file
15
dist/lib/db-handler.d.ts
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import type { BunMariaDBConfig, DBResponseObject } from "../types";
|
||||||
|
type Param = {
|
||||||
|
query: string;
|
||||||
|
values?: any[];
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* # Main DB Handler Function
|
||||||
|
*/
|
||||||
|
export default function dbHandler<T extends {
|
||||||
|
[k: string]: any;
|
||||||
|
} = {
|
||||||
|
[k: string]: any;
|
||||||
|
}>({ query, values, config }: Param): Promise<DBResponseObject<T>>;
|
||||||
|
export {};
|
||||||
61
dist/lib/db-handler.js
vendored
Normal file
61
dist/lib/db-handler.js
vendored
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import grabMariaDBClient from "../functions/grab-mariadb-client";
|
||||||
|
/**
|
||||||
|
* Resolve a shared MariaDB client. Prefer the global client so schema sync
|
||||||
|
* and CRUD do not open a new connection per query.
|
||||||
|
*/
|
||||||
|
function resolveClient(config) {
|
||||||
|
const resolvedConfig = config || global.CONFIG;
|
||||||
|
// Same database as the global client → reuse it
|
||||||
|
if (global.MARIADB_CLIENT &&
|
||||||
|
(!resolvedConfig?.db_name ||
|
||||||
|
!global.CONFIG?.db_name ||
|
||||||
|
resolvedConfig.db_name === global.CONFIG.db_name)) {
|
||||||
|
return global.MARIADB_CLIENT;
|
||||||
|
}
|
||||||
|
if (resolvedConfig) {
|
||||||
|
return grabMariaDBClient({ config: resolvedConfig });
|
||||||
|
}
|
||||||
|
return global.MARIADB_CLIENT;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* # Main DB Handler Function
|
||||||
|
*/
|
||||||
|
export default async function dbHandler({ query, values, config }) {
|
||||||
|
try {
|
||||||
|
const CLIENT = resolveClient(config);
|
||||||
|
if (!CLIENT) {
|
||||||
|
throw new Error(`Couldn't grab MariaDB Client.`);
|
||||||
|
}
|
||||||
|
const res = await CLIENT.unsafe(query, values);
|
||||||
|
const count = res.count;
|
||||||
|
const res_array = (() => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
const last_insert_id = res_array?.[0] ? res_array[0]?.id : undefined;
|
||||||
|
const affected_rows = res_array?.[0] ? res_array.length : undefined;
|
||||||
|
const insert_return = {
|
||||||
|
count,
|
||||||
|
last_insert_id,
|
||||||
|
affected_rows,
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
payload: res_array,
|
||||||
|
single_res: res_array?.[0],
|
||||||
|
insert_return,
|
||||||
|
count,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "DB Handler Error => " + error.message,
|
||||||
|
msg: "DB Handler Error => " + error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
7
dist/lib/grab-duplicate-safe-insert-sql.d.ts
vendored
Normal file
7
dist/lib/grab-duplicate-safe-insert-sql.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
type Params = {
|
||||||
|
sql: string;
|
||||||
|
table: string;
|
||||||
|
data: any | any[];
|
||||||
|
};
|
||||||
|
export default function ({ sql: passedSql, table, data }: Params): Promise<string>;
|
||||||
|
export {};
|
||||||
25
dist/lib/grab-duplicate-safe-insert-sql.js
vendored
Normal file
25
dist/lib/grab-duplicate-safe-insert-sql.js
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
function quoteIdentifier(identifier) {
|
||||||
|
return `\`${identifier.replace(/`/g, "``")}\``;
|
||||||
|
}
|
||||||
|
export default async function ({ sql: passedSql, table, data }) {
|
||||||
|
const config = global.CONFIG;
|
||||||
|
const dbSchema = global.DB_SCHEMA;
|
||||||
|
const tableSchema = dbSchema?.tables.find((t) => t.tableName == table);
|
||||||
|
if (!tableSchema?.tableName) {
|
||||||
|
return passedSql;
|
||||||
|
}
|
||||||
|
const dataObject = Array.isArray(data) ? data[0] : data;
|
||||||
|
const dataKeys = Object.keys(dataObject || {});
|
||||||
|
if (dataKeys.length === 0) {
|
||||||
|
return passedSql;
|
||||||
|
}
|
||||||
|
const hasUniqueField = tableSchema.fields.some((field) => field.unique) || Boolean(tableSchema.uniqueConstraints?.length);
|
||||||
|
if (!hasUniqueField) {
|
||||||
|
return passedSql;
|
||||||
|
}
|
||||||
|
const setSql = dataKeys
|
||||||
|
.filter((field) => field !== "updated_at")
|
||||||
|
.map((field) => `${quoteIdentifier(field)} = VALUES(${quoteIdentifier(field)})`);
|
||||||
|
setSql.push(`updated_at = ${Date.now()}`);
|
||||||
|
return `${passedSql} ON DUPLICATE KEY UPDATE ${setSql.join(", ")}`;
|
||||||
|
}
|
||||||
17
dist/lib/mariadb/db-delete.d.ts
vendored
Normal file
17
dist/lib/mariadb/db-delete.d.ts
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import type { BunMariaDBConfig, DBResponseObject, ServerQueryParam } from "../../types";
|
||||||
|
type Params<Schema extends {
|
||||||
|
[k: string]: any;
|
||||||
|
} = {
|
||||||
|
[k: string]: any;
|
||||||
|
}, Table extends string = string> = {
|
||||||
|
table: Table;
|
||||||
|
query?: ServerQueryParam<Schema>;
|
||||||
|
targetId?: number | string;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
};
|
||||||
|
export default function DbDelete<Schema extends {
|
||||||
|
[k: string]: any;
|
||||||
|
} = {
|
||||||
|
[k: string]: any;
|
||||||
|
}, Table extends string = string>({ table, query, targetId, config, }: Params<Schema, Table>): Promise<DBResponseObject>;
|
||||||
|
export {};
|
||||||
68
dist/lib/mariadb/db-delete.js
vendored
Normal file
68
dist/lib/mariadb/db-delete.js
vendored
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import dbHandler from "../db-handler";
|
||||||
|
import _ from "lodash";
|
||||||
|
import sqlGenerator from "../../utils/sql-generator";
|
||||||
|
function quoteIdentifier(identifier) {
|
||||||
|
return `\`${identifier.replace(/`/g, "``")}\``;
|
||||||
|
}
|
||||||
|
export default async function DbDelete({ table, query, targetId, config, }) {
|
||||||
|
let sqlObj = null;
|
||||||
|
try {
|
||||||
|
let finalQuery = query || {};
|
||||||
|
if (targetId) {
|
||||||
|
finalQuery = _.merge(finalQuery, {
|
||||||
|
query: {
|
||||||
|
id: {
|
||||||
|
value: String(targetId),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
sqlObj = sqlGenerator({
|
||||||
|
tableName: quoteIdentifier(table),
|
||||||
|
genObject: finalQuery,
|
||||||
|
});
|
||||||
|
const whereClause = sqlObj.string.match(/WHERE .*/)?.[0];
|
||||||
|
if (!whereClause) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: `No WHERE clause`,
|
||||||
|
debug: {
|
||||||
|
sqlObj,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let sql = `DELETE FROM ${quoteIdentifier(table)} ${whereClause}`;
|
||||||
|
sql += ` RETURNING *`;
|
||||||
|
sqlObj.string = sql;
|
||||||
|
const res = await dbHandler({
|
||||||
|
query: sql,
|
||||||
|
values: sqlObj.values,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
if (!res.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: "Database delete failed",
|
||||||
|
debug: {
|
||||||
|
sqlObj,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
..._.omit(res, ["payload"]),
|
||||||
|
success: Boolean(res.insert_return?.last_insert_id),
|
||||||
|
debug: {
|
||||||
|
sqlObj,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
debug: {
|
||||||
|
sqlObj,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
15
dist/lib/mariadb/db-generate-type-defs.d.ts
vendored
Normal file
15
dist/lib/mariadb/db-generate-type-defs.d.ts
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import type { BUN_MARIADB_TableSchemaType } from "../../types";
|
||||||
|
type Param = {
|
||||||
|
paradigm: "JavaScript" | "TypeScript" | undefined;
|
||||||
|
table: BUN_MARIADB_TableSchemaType;
|
||||||
|
query?: any;
|
||||||
|
typeDefName?: string;
|
||||||
|
allValuesOptional?: boolean;
|
||||||
|
addExport?: boolean;
|
||||||
|
dbName?: string;
|
||||||
|
};
|
||||||
|
export default function generateTypeDefinition({ paradigm, table, query, typeDefName, allValuesOptional, addExport, dbName, }: Param): {
|
||||||
|
typeDefinition: string | null;
|
||||||
|
tdName: string;
|
||||||
|
};
|
||||||
|
export {};
|
||||||
63
dist/lib/mariadb/db-generate-type-defs.js
vendored
Normal file
63
dist/lib/mariadb/db-generate-type-defs.js
vendored
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
export default function generateTypeDefinition({ paradigm, table, query, typeDefName, allValuesOptional, addExport, dbName, }) {
|
||||||
|
let typeDefinition = ``;
|
||||||
|
let tdName = ``;
|
||||||
|
try {
|
||||||
|
tdName = typeDefName
|
||||||
|
? typeDefName
|
||||||
|
: dbName
|
||||||
|
? `BUN_MARIADB_${dbName}_${table.tableName}`.toUpperCase()
|
||||||
|
: `BUN_MARIADB_${query.single}_${query.single_table}`.toUpperCase();
|
||||||
|
const fields = table.fields;
|
||||||
|
function typeMap(schemaType) {
|
||||||
|
if (schemaType.options && schemaType.options.length > 0) {
|
||||||
|
let opts = schemaType.options.map((opt) => schemaType.dataType?.match(/int/i) || typeof opt == "number"
|
||||||
|
? `${opt}`
|
||||||
|
: `"${opt}"`);
|
||||||
|
opts.push(`""`);
|
||||||
|
return opts.join(" | ");
|
||||||
|
}
|
||||||
|
if (schemaType.dataType?.match(/blob/i)) {
|
||||||
|
return `Float32Array<ArrayBuffer> | Buffer<ArrayBuffer> | null`;
|
||||||
|
}
|
||||||
|
if (schemaType.dataType?.match(/int|double|decimal|real/i)) {
|
||||||
|
return `number | ""`;
|
||||||
|
}
|
||||||
|
if (schemaType.dataType?.match(/text|varchar|timestamp/i)) {
|
||||||
|
return `string`;
|
||||||
|
}
|
||||||
|
if (schemaType.dataType?.match(/boolean/i)) {
|
||||||
|
return "0 | 1";
|
||||||
|
}
|
||||||
|
return "string";
|
||||||
|
}
|
||||||
|
const typesArrayTypeScript = [];
|
||||||
|
const typesArrayJavascript = [];
|
||||||
|
typesArrayTypeScript.push(`${addExport ? "export " : ""}type ${tdName} = {`);
|
||||||
|
typesArrayJavascript.push(`/**\n * @typedef {object} ${tdName}`);
|
||||||
|
fields.forEach((field) => {
|
||||||
|
if (field.fieldDescription) {
|
||||||
|
typesArrayTypeScript.push(` /** \n * ${field.fieldDescription}\n */`);
|
||||||
|
}
|
||||||
|
const nullValue = allValuesOptional
|
||||||
|
? "?"
|
||||||
|
: field.notNullValue
|
||||||
|
? ""
|
||||||
|
: "?";
|
||||||
|
typesArrayTypeScript.push(` ${field.fieldName}${nullValue}: ${typeMap(field)};`);
|
||||||
|
typesArrayJavascript.push(` * @property {${typeMap(field)}${nullValue}} ${field.fieldName}`);
|
||||||
|
});
|
||||||
|
typesArrayTypeScript.push(`}`);
|
||||||
|
typesArrayJavascript.push(` */`);
|
||||||
|
if (paradigm?.match(/javascript/i)) {
|
||||||
|
typeDefinition = typesArrayJavascript.join("\n");
|
||||||
|
}
|
||||||
|
if (paradigm?.match(/typescript/i)) {
|
||||||
|
typeDefinition = typesArrayTypeScript.join("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(error.message);
|
||||||
|
typeDefinition = null;
|
||||||
|
}
|
||||||
|
return { typeDefinition, tdName };
|
||||||
|
}
|
||||||
17
dist/lib/mariadb/db-insert.d.ts
vendored
Normal file
17
dist/lib/mariadb/db-insert.d.ts
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import type { BunMariaDBConfig, DBResponseObject } from "../../types";
|
||||||
|
type Params<Schema extends {
|
||||||
|
[k: string]: any;
|
||||||
|
} = {
|
||||||
|
[k: string]: any;
|
||||||
|
}, Table extends string = string> = {
|
||||||
|
table: Table;
|
||||||
|
data: Schema[];
|
||||||
|
update_on_duplicate?: boolean;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
};
|
||||||
|
export default function DbInsert<Schema extends {
|
||||||
|
[k: string]: any;
|
||||||
|
} = {
|
||||||
|
[k: string]: any;
|
||||||
|
}, Table extends string = string>({ table, data, update_on_duplicate, config, }: Params<Schema, Table>): Promise<DBResponseObject>;
|
||||||
|
export {};
|
||||||
66
dist/lib/mariadb/db-insert.js
vendored
Normal file
66
dist/lib/mariadb/db-insert.js
vendored
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import dbHandler from "../db-handler";
|
||||||
|
import sqlInsertGenerator from "../../utils/sql-insert-generator";
|
||||||
|
import { sanitizeHtmlFieldsBatch } from "../../utils/sanitize-html-fields";
|
||||||
|
import grabDuplicateSafeInsertSql from "../grab-duplicate-safe-insert-sql";
|
||||||
|
export default async function DbInsert({ table, data, update_on_duplicate, config, }) {
|
||||||
|
let sqlObj = null;
|
||||||
|
try {
|
||||||
|
const sanitizedData = sanitizeHtmlFieldsBatch({
|
||||||
|
table,
|
||||||
|
data,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
const finalData = sanitizedData.map((d) => ({
|
||||||
|
created_at: Date.now(),
|
||||||
|
updated_at: Date.now(),
|
||||||
|
...d,
|
||||||
|
}));
|
||||||
|
sqlObj =
|
||||||
|
sqlInsertGenerator({
|
||||||
|
tableName: table,
|
||||||
|
data: finalData,
|
||||||
|
}) || null;
|
||||||
|
if (!sqlObj) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: "No insert SQL generated",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let sql = sqlObj.query;
|
||||||
|
if (update_on_duplicate && data[0]) {
|
||||||
|
sql = await grabDuplicateSafeInsertSql({ data, table, sql });
|
||||||
|
}
|
||||||
|
sql += ` RETURNING *`;
|
||||||
|
const res = await dbHandler({
|
||||||
|
query: sql,
|
||||||
|
values: sqlObj.values,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
if (!res.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: "Database insert failed",
|
||||||
|
debug: {
|
||||||
|
sqlObj,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...res,
|
||||||
|
success: Boolean(res?.insert_return?.affected_rows ||
|
||||||
|
res.insert_return?.last_insert_id),
|
||||||
|
debug: {
|
||||||
|
sqlObj,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
debug: {
|
||||||
|
sqlObj,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
7
dist/lib/mariadb/db-schema-to-typedef.d.ts
vendored
Normal file
7
dist/lib/mariadb/db-schema-to-typedef.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import type { BUN_MARIADB_DatabaseSchemaType, BunMariaDBConfig } from "../../types";
|
||||||
|
type Params = {
|
||||||
|
dbSchema: BUN_MARIADB_DatabaseSchemaType;
|
||||||
|
config: BunMariaDBConfig;
|
||||||
|
};
|
||||||
|
export default function dbSchemaToType({ config, dbSchema, }: Params): string[] | undefined;
|
||||||
|
export {};
|
||||||
44
dist/lib/mariadb/db-schema-to-typedef.js
vendored
Normal file
44
dist/lib/mariadb/db-schema-to-typedef.js
vendored
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import _ from "lodash";
|
||||||
|
import generateTypeDefinition from "./db-generate-type-defs";
|
||||||
|
export default function dbSchemaToType({ config, dbSchema, }) {
|
||||||
|
let datasquirelSchema = dbSchema;
|
||||||
|
if (!datasquirelSchema)
|
||||||
|
return;
|
||||||
|
let tableNames = `export const BunMariaDBTables = [\n${datasquirelSchema.tables
|
||||||
|
.map((tbl) => ` "${tbl.tableName}",`)
|
||||||
|
.join("\n")}\n] as const`;
|
||||||
|
const dbTablesSchemas = datasquirelSchema.tables;
|
||||||
|
const defDbName = config.db_name
|
||||||
|
?.toUpperCase()
|
||||||
|
.replace(/[^a-zA-Z0-9]/g, "_");
|
||||||
|
const defNames = [];
|
||||||
|
const schemas = dbTablesSchemas
|
||||||
|
.map((table) => {
|
||||||
|
let final_table = _.cloneDeep(table);
|
||||||
|
if (final_table.parentTableName) {
|
||||||
|
const parent_table = dbTablesSchemas.find((t) => t.tableName === final_table.parentTableName);
|
||||||
|
if (parent_table) {
|
||||||
|
final_table = _.merge(parent_table, {
|
||||||
|
tableName: final_table.tableName,
|
||||||
|
tableDescription: final_table.tableDescription,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const defObj = generateTypeDefinition({
|
||||||
|
paradigm: "TypeScript",
|
||||||
|
table: final_table,
|
||||||
|
typeDefName: `BUN_MARIADB_${defDbName}_${final_table.tableName.toUpperCase()}`,
|
||||||
|
allValuesOptional: true,
|
||||||
|
addExport: true,
|
||||||
|
});
|
||||||
|
if (defObj.tdName?.match(/./)) {
|
||||||
|
defNames.push(defObj.tdName);
|
||||||
|
}
|
||||||
|
return defObj.typeDefinition;
|
||||||
|
})
|
||||||
|
.filter((schm) => typeof schm == "string");
|
||||||
|
const allTd = defNames?.[0]
|
||||||
|
? `export type BUN_MARIADB_${defDbName}_ALL_TYPEDEFS = ${defNames.join(` & `)}`
|
||||||
|
: ``;
|
||||||
|
return [tableNames, ...schemas, allTd];
|
||||||
|
}
|
||||||
18
dist/lib/mariadb/db-select.d.ts
vendored
Normal file
18
dist/lib/mariadb/db-select.d.ts
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import type { BunMariaDBConfig, DBResponseObject, ServerQueryParam } from "../../types";
|
||||||
|
type Params<Schema extends {
|
||||||
|
[k: string]: any;
|
||||||
|
} = {
|
||||||
|
[k: string]: any;
|
||||||
|
}, Table extends string = string> = {
|
||||||
|
query?: ServerQueryParam<Schema>;
|
||||||
|
table: Table;
|
||||||
|
count?: boolean;
|
||||||
|
targetId?: number | string;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
};
|
||||||
|
export default function DbSelect<Schema extends {
|
||||||
|
[k: string]: any;
|
||||||
|
} = {
|
||||||
|
[k: string]: any;
|
||||||
|
}, Table extends string = string>({ table, query, count, targetId, config, }: Params<Schema, Table>): Promise<DBResponseObject<Schema>>;
|
||||||
|
export {};
|
||||||
94
dist/lib/mariadb/db-select.js
vendored
Normal file
94
dist/lib/mariadb/db-select.js
vendored
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import dbHandler from "../db-handler";
|
||||||
|
import _ from "lodash";
|
||||||
|
import sqlGenerator from "../../utils/sql-generator";
|
||||||
|
function quoteIdentifier(identifier) {
|
||||||
|
return `\`${identifier.replace(/`/g, "``")}\``;
|
||||||
|
}
|
||||||
|
export default async function DbSelect({ table, query, count, targetId, config, }) {
|
||||||
|
let sqlObj = null;
|
||||||
|
try {
|
||||||
|
let finalQuery = query || {};
|
||||||
|
if (targetId) {
|
||||||
|
finalQuery = _.merge(finalQuery, {
|
||||||
|
query: {
|
||||||
|
id: {
|
||||||
|
value: String(targetId),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
sqlObj = sqlGenerator({
|
||||||
|
tableName: quoteIdentifier(table),
|
||||||
|
genObject: finalQuery,
|
||||||
|
});
|
||||||
|
const res = await dbHandler({
|
||||||
|
query: sqlObj.string,
|
||||||
|
values: sqlObj.values,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
if (!res.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: "Database select failed",
|
||||||
|
debug: {
|
||||||
|
sqlObj,
|
||||||
|
sql: sqlObj.string,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const batchRes = (res.payload || []);
|
||||||
|
let resp = {
|
||||||
|
success: true,
|
||||||
|
payload: batchRes,
|
||||||
|
single_res: batchRes[0],
|
||||||
|
debug: {
|
||||||
|
sqlObj,
|
||||||
|
sql: sqlObj.string,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (count) {
|
||||||
|
const countSqlObject = sqlGenerator({
|
||||||
|
tableName: quoteIdentifier(table),
|
||||||
|
genObject: finalQuery,
|
||||||
|
count,
|
||||||
|
});
|
||||||
|
const countSql = `SELECT COUNT(*) AS count FROM (${countSqlObject.string}) AS c`;
|
||||||
|
const countRes = await dbHandler({
|
||||||
|
query: countSql,
|
||||||
|
values: countSqlObject.values,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
if (!countRes.success) {
|
||||||
|
return {
|
||||||
|
...resp,
|
||||||
|
success: false,
|
||||||
|
msg: "Database count failed",
|
||||||
|
debug: {
|
||||||
|
...resp.debug,
|
||||||
|
count_sql: countSql,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const countRows = countRes.payload || [];
|
||||||
|
const countVal = countRows[0]?.count ?? countRows[0]?.["count"];
|
||||||
|
resp = {
|
||||||
|
...resp,
|
||||||
|
count: Number(countVal),
|
||||||
|
debug: {
|
||||||
|
...resp.debug,
|
||||||
|
count_sql: countSql,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
debug: {
|
||||||
|
sqlObj,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
11
dist/lib/mariadb/db-sql.d.ts
vendored
Normal file
11
dist/lib/mariadb/db-sql.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import type { DBResponseObject, SQLInsertGenValueType } from "../../types";
|
||||||
|
type Params = {
|
||||||
|
sql: string;
|
||||||
|
values?: SQLInsertGenValueType[];
|
||||||
|
};
|
||||||
|
export default function DbSQL<T extends {
|
||||||
|
[k: string]: any;
|
||||||
|
} = {
|
||||||
|
[k: string]: any;
|
||||||
|
}>({ sql, values }: Params): Promise<DBResponseObject<T>>;
|
||||||
|
export {};
|
||||||
45
dist/lib/mariadb/db-sql.js
vendored
Normal file
45
dist/lib/mariadb/db-sql.js
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import dbHandler from "../db-handler";
|
||||||
|
export default async function DbSQL({ sql, values }) {
|
||||||
|
try {
|
||||||
|
const trimmedSql = sql.trim();
|
||||||
|
const isSelect = trimmedSql.match(/^select/i);
|
||||||
|
const res = await dbHandler({
|
||||||
|
query: trimmedSql,
|
||||||
|
values: values,
|
||||||
|
});
|
||||||
|
if (!res.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: "Database query failed",
|
||||||
|
debug: {
|
||||||
|
sqlObj: {
|
||||||
|
sql: trimmedSql,
|
||||||
|
values,
|
||||||
|
},
|
||||||
|
sql,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const payload = isSelect ? (res.payload || []) : undefined;
|
||||||
|
const single_res = isSelect ? payload?.[0] : res.single_res;
|
||||||
|
const singleRaw = res.single_res;
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
payload,
|
||||||
|
single_res,
|
||||||
|
debug: {
|
||||||
|
sqlObj: {
|
||||||
|
sql: trimmedSql,
|
||||||
|
values,
|
||||||
|
},
|
||||||
|
sql,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
18
dist/lib/mariadb/db-update.d.ts
vendored
Normal file
18
dist/lib/mariadb/db-update.d.ts
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import type { BunMariaDBConfig, DBResponseObject, ServerQueryParam } from "../../types";
|
||||||
|
type Params<Schema extends {
|
||||||
|
[k: string]: any;
|
||||||
|
} = {
|
||||||
|
[k: string]: any;
|
||||||
|
}, Table extends string = string> = {
|
||||||
|
table: Table;
|
||||||
|
data: Schema;
|
||||||
|
query?: ServerQueryParam<Schema>;
|
||||||
|
targetId?: number | string;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
};
|
||||||
|
export default function DbUpdate<Schema extends {
|
||||||
|
[k: string]: any;
|
||||||
|
} = {
|
||||||
|
[k: string]: any;
|
||||||
|
}, Table extends string = string>({ table, data, query, targetId, config, }: Params<Schema, Table>): Promise<DBResponseObject>;
|
||||||
|
export {};
|
||||||
109
dist/lib/mariadb/db-update.js
vendored
Normal file
109
dist/lib/mariadb/db-update.js
vendored
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import dbHandler from "../db-handler";
|
||||||
|
import _ from "lodash";
|
||||||
|
import sqlGenerator from "../../utils/sql-generator";
|
||||||
|
import sanitizeHtmlFields from "../../utils/sanitize-html-fields";
|
||||||
|
function quoteIdentifier(identifier) {
|
||||||
|
return `\`${identifier.replace(/`/g, "``")}\``;
|
||||||
|
}
|
||||||
|
export default async function DbUpdate({ table, data, query, targetId, config, }) {
|
||||||
|
let sqlObj = { string: "", values: [] };
|
||||||
|
try {
|
||||||
|
let finalQuery = query || {};
|
||||||
|
if (targetId) {
|
||||||
|
finalQuery = _.merge(finalQuery, {
|
||||||
|
query: {
|
||||||
|
id: {
|
||||||
|
value: String(targetId),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const sqlQueryObj = sqlGenerator({
|
||||||
|
tableName: quoteIdentifier(table),
|
||||||
|
genObject: finalQuery,
|
||||||
|
});
|
||||||
|
const whereClause = sqlQueryObj.string.match(/WHERE .*/)?.[0];
|
||||||
|
if (!whereClause) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
msg: `No WHERE clause`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let values = [];
|
||||||
|
let sql = ``;
|
||||||
|
sql += `UPDATE ${quoteIdentifier(table)} SET`;
|
||||||
|
const sanitizedData = sanitizeHtmlFields({
|
||||||
|
table,
|
||||||
|
data,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
const finalData = {
|
||||||
|
updated_at: Date.now(),
|
||||||
|
...sanitizedData,
|
||||||
|
};
|
||||||
|
const keys = Object.keys(finalData);
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
const key = keys[i];
|
||||||
|
if (!key)
|
||||||
|
continue;
|
||||||
|
const isLast = i == keys.length - 1;
|
||||||
|
sql += ` ${quoteIdentifier(key)}=?`;
|
||||||
|
values.push(finalData[key] ?? null);
|
||||||
|
if (!isLast) {
|
||||||
|
sql += `,`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sql += ` ${whereClause}`;
|
||||||
|
values = [...values, ...sqlQueryObj.values];
|
||||||
|
const res = await dbHandler({
|
||||||
|
query: sql,
|
||||||
|
values: values,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
sqlObj.string = sql;
|
||||||
|
sqlObj.values = values;
|
||||||
|
let updated_sql = ``;
|
||||||
|
let updated_sql_values = [];
|
||||||
|
updated_sql += `SELECT * FROM ${quoteIdentifier(table)} ${whereClause}`;
|
||||||
|
updated_sql_values = [...updated_sql_values, ...sqlQueryObj.values];
|
||||||
|
updated_sql += ` AND `;
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
const key = keys[i];
|
||||||
|
if (!key)
|
||||||
|
continue;
|
||||||
|
if (key == "updated_at")
|
||||||
|
continue;
|
||||||
|
const isLast = i == keys.length - 1;
|
||||||
|
updated_sql += ` ${quoteIdentifier(key)}=?`;
|
||||||
|
updated_sql_values.push(finalData[key] ?? null);
|
||||||
|
if (!isLast) {
|
||||||
|
updated_sql += ` AND `;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const updated_res = await dbHandler({
|
||||||
|
query: updated_sql,
|
||||||
|
values: updated_sql_values,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
const affected_rows = updated_res.payload?.length;
|
||||||
|
return {
|
||||||
|
...res,
|
||||||
|
success: Boolean(affected_rows),
|
||||||
|
insert_return: {
|
||||||
|
affected_rows,
|
||||||
|
},
|
||||||
|
debug: {
|
||||||
|
sqlObj,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
debug: {
|
||||||
|
sqlObj,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
8
dist/lib/mariadb/schema-to-typedef.d.ts
vendored
Normal file
8
dist/lib/mariadb/schema-to-typedef.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import type { BUN_MARIADB_DatabaseSchemaType, BunMariaDBConfig } from "../../types";
|
||||||
|
type Params = {
|
||||||
|
dbSchema: BUN_MARIADB_DatabaseSchemaType;
|
||||||
|
dst_file: string;
|
||||||
|
config: BunMariaDBConfig;
|
||||||
|
};
|
||||||
|
export default function dbSchemaToTypeDef({ dbSchema, dst_file, config, }: Params): void;
|
||||||
|
export {};
|
||||||
18
dist/lib/mariadb/schema-to-typedef.js
vendored
Normal file
18
dist/lib/mariadb/schema-to-typedef.js
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||||
|
import dbSchemaToType from "./db-schema-to-typedef";
|
||||||
|
export default function dbSchemaToTypeDef({ dbSchema, dst_file, config, }) {
|
||||||
|
try {
|
||||||
|
if (!dbSchema)
|
||||||
|
throw new Error("No schema found");
|
||||||
|
const definitions = dbSchemaToType({ dbSchema, config });
|
||||||
|
const ourfileDir = path.dirname(dst_file);
|
||||||
|
if (!existsSync(ourfileDir)) {
|
||||||
|
mkdirSync(ourfileDir, { recursive: true });
|
||||||
|
}
|
||||||
|
writeFileSync(dst_file, definitions?.join("\n\n") || "", "utf-8");
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(`Schema to Typedef Error =>`, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
dist/lib/schema/build-column-definition.d.ts
vendored
Normal file
2
dist/lib/schema/build-column-definition.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import type { BUN_MARIADB_FieldSchemaType } from "../../types";
|
||||||
|
export default function buildColumnDefinition(field: BUN_MARIADB_FieldSchemaType): string;
|
||||||
43
dist/lib/schema/build-column-definition.js
vendored
Normal file
43
dist/lib/schema/build-column-definition.js
vendored
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import isVectorField from "./is-vector-field";
|
||||||
|
import mapDataType from "./map-data-types";
|
||||||
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
|
export default function buildColumnDefinition(field) {
|
||||||
|
if (!field.fieldName) {
|
||||||
|
throw new Error("Field name is required");
|
||||||
|
}
|
||||||
|
const parts = [MariaDBQuoteGen(field.fieldName)];
|
||||||
|
parts.push(mapDataType(field));
|
||||||
|
if (field.autoIncrement) {
|
||||||
|
parts.push("AUTO_INCREMENT");
|
||||||
|
}
|
||||||
|
// Vector columns used in VECTOR INDEX must be NOT NULL
|
||||||
|
if (field.notNullValue ||
|
||||||
|
field.primaryKey ||
|
||||||
|
isVectorField(field)) {
|
||||||
|
if (!field.primaryKey) {
|
||||||
|
parts.push("NOT NULL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// VECTOR columns cannot be UNIQUE in the usual sense
|
||||||
|
if (field.unique && !field.primaryKey && !isVectorField(field)) {
|
||||||
|
parts.push("UNIQUE");
|
||||||
|
}
|
||||||
|
if (field.defaultValue !== undefined) {
|
||||||
|
if (typeof field.defaultValue === "string") {
|
||||||
|
parts.push(`DEFAULT '${field.defaultValue.replace(/'/g, "''")}'`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
parts.push(`DEFAULT ${field.defaultValue}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (field.defaultValueLiteral) {
|
||||||
|
parts.push(`DEFAULT ${field.defaultValueLiteral}`);
|
||||||
|
}
|
||||||
|
if (field.onUpdate) {
|
||||||
|
parts.push(`ON UPDATE ${field.onUpdate}`);
|
||||||
|
}
|
||||||
|
else if (field.onUpdateLiteral) {
|
||||||
|
parts.push(`ON UPDATE ${field.onUpdateLiteral}`);
|
||||||
|
}
|
||||||
|
return parts.join(" ");
|
||||||
|
}
|
||||||
2
dist/lib/schema/build-foreign-key-constraint.d.ts
vendored
Normal file
2
dist/lib/schema/build-foreign-key-constraint.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import type { BUN_MARIADB_FieldSchemaType } from "../../types";
|
||||||
|
export default function buildForeignKeyConstraint(field: BUN_MARIADB_FieldSchemaType): string;
|
||||||
15
dist/lib/schema/build-foreign-key-constraint.js
vendored
Normal file
15
dist/lib/schema/build-foreign-key-constraint.js
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
|
export default function buildForeignKeyConstraint(field) {
|
||||||
|
const fk = field.foreignKey;
|
||||||
|
const constraintName = fk.foreignKeyName
|
||||||
|
? `CONSTRAINT ${MariaDBQuoteGen(fk.foreignKeyName)} `
|
||||||
|
: "";
|
||||||
|
let constraint = `${constraintName}FOREIGN KEY (${MariaDBQuoteGen(field.fieldName)}) REFERENCES ${MariaDBQuoteGen(fk.destinationTableName)}(${MariaDBQuoteGen(fk.destinationTableColumnName)})`;
|
||||||
|
if (fk.cascadeDelete) {
|
||||||
|
constraint += " ON DELETE CASCADE";
|
||||||
|
}
|
||||||
|
if (fk.cascadeUpdate) {
|
||||||
|
constraint += " ON UPDATE CASCADE";
|
||||||
|
}
|
||||||
|
return constraint;
|
||||||
|
}
|
||||||
2
dist/lib/schema/build-table-options.d.ts
vendored
Normal file
2
dist/lib/schema/build-table-options.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import type { BUN_MARIADB_TableSchemaType } from "../../types";
|
||||||
|
export default function buildTableOptions(table: BUN_MARIADB_TableSchemaType): string;
|
||||||
7
dist/lib/schema/build-table-options.js
vendored
Normal file
7
dist/lib/schema/build-table-options.js
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export default function buildTableOptions(table) {
|
||||||
|
const options = ["ENGINE=InnoDB"];
|
||||||
|
if (table.collation) {
|
||||||
|
options.push("DEFAULT CHARSET=utf8mb4", `COLLATE ${table.collation}`);
|
||||||
|
}
|
||||||
|
return ` ${options.join(" ")}`;
|
||||||
|
}
|
||||||
2
dist/lib/schema/create-db-manager-table.d.ts
vendored
Normal file
2
dist/lib/schema/create-db-manager-table.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import type { CreateDBSchemaParams } from "../../types";
|
||||||
|
export default function createDBManagerTable({ db_schema, config, }: CreateDBSchemaParams): Promise<void>;
|
||||||
15
dist/lib/schema/create-db-manager-table.js
vendored
Normal file
15
dist/lib/schema/create-db-manager-table.js
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { AppData } from "../../data/app-data";
|
||||||
|
import dbHandler from "../db-handler";
|
||||||
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
|
export default async function createDBManagerTable({ db_schema, config, }) {
|
||||||
|
let sql = ``;
|
||||||
|
sql += `CREATE TABLE IF NOT EXISTS ${MariaDBQuoteGen(AppData["DbSchemaManagerTableName"])} (\n`;
|
||||||
|
sql += ` table_name VARCHAR(255) NOT NULL PRIMARY KEY,\n`;
|
||||||
|
sql += ` created_at BIGINT NOT NULL,\n`;
|
||||||
|
sql += ` updated_at BIGINT NOT NULL\n`;
|
||||||
|
sql += `) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci\n`;
|
||||||
|
await dbHandler({
|
||||||
|
query: sql,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
2
dist/lib/schema/create-db-schema.d.ts
vendored
Normal file
2
dist/lib/schema/create-db-schema.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import type { CreateDBSchemaParams } from "../../types";
|
||||||
|
export default function createDBSchema(params: CreateDBSchemaParams): Promise<void>;
|
||||||
17
dist/lib/schema/create-db-schema.js
vendored
Normal file
17
dist/lib/schema/create-db-schema.js
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import createDBManagerTable from "./create-db-manager-table";
|
||||||
|
import handleDBSchemaTables from "./handle-db-schema-tables";
|
||||||
|
import orderDBSchema from "./order-db-schema";
|
||||||
|
export default async function createDBSchema(params) {
|
||||||
|
/**
|
||||||
|
* Create Schema Manager Table
|
||||||
|
*/
|
||||||
|
await createDBManagerTable(params);
|
||||||
|
/**
|
||||||
|
* Reorder Tables (parents before children with FKs)
|
||||||
|
*/
|
||||||
|
const ordered_db_schema = await orderDBSchema(params);
|
||||||
|
/**
|
||||||
|
* Handle Tables (create / update / drop)
|
||||||
|
*/
|
||||||
|
await handleDBSchemaTables({ ...params, db_schema: ordered_db_schema });
|
||||||
|
}
|
||||||
5
dist/lib/schema/create-table.d.ts
vendored
Normal file
5
dist/lib/schema/create-table.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import type { BUN_MARIADB_TableSchemaType, BunMariaDBConfig } from "../../types";
|
||||||
|
export default function createTable({ table, config, }: {
|
||||||
|
table: BUN_MARIADB_TableSchemaType;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void>;
|
||||||
41
dist/lib/schema/create-table.js
vendored
Normal file
41
dist/lib/schema/create-table.js
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import buildColumnDefinition from "./build-column-definition";
|
||||||
|
import buildForeignKeyConstraint from "./build-foreign-key-constraint";
|
||||||
|
import buildTableOptions from "./build-table-options";
|
||||||
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
|
import runSchemaQuery from "./run-schema-query";
|
||||||
|
export default async function createTable({ table, config, }) {
|
||||||
|
if (!table.tableName.match(/_temp_\d+$/)) {
|
||||||
|
console.log(`Creating table: ${table.tableName}`);
|
||||||
|
}
|
||||||
|
const columnDefinitions = [];
|
||||||
|
const foreignKeys = [];
|
||||||
|
const primaryKeys = [];
|
||||||
|
for (const field of table.fields || []) {
|
||||||
|
columnDefinitions.push(buildColumnDefinition(field));
|
||||||
|
if (field.primaryKey && field.fieldName) {
|
||||||
|
primaryKeys.push(field.fieldName);
|
||||||
|
}
|
||||||
|
if (field.foreignKey && !table.isVector) {
|
||||||
|
foreignKeys.push(buildForeignKeyConstraint(field));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (primaryKeys.length > 0) {
|
||||||
|
const pkCols = primaryKeys.map((k) => MariaDBQuoteGen(k)).join(", ");
|
||||||
|
columnDefinitions.push(`PRIMARY KEY (${pkCols})`);
|
||||||
|
}
|
||||||
|
if (table.uniqueConstraints) {
|
||||||
|
for (const constraint of table.uniqueConstraints) {
|
||||||
|
if (constraint.constraintTableFields &&
|
||||||
|
constraint.constraintTableFields.length > 0) {
|
||||||
|
const fields = constraint.constraintTableFields
|
||||||
|
.map((field) => MariaDBQuoteGen(field.value))
|
||||||
|
.join(", ");
|
||||||
|
const constraintName = constraint.constraintName ||
|
||||||
|
`unique_${fields.replace(/`/g, "")}`;
|
||||||
|
columnDefinitions.push(`CONSTRAINT ${MariaDBQuoteGen(constraintName)} UNIQUE (${fields})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const sql = `CREATE TABLE IF NOT EXISTS ${MariaDBQuoteGen(table.tableName)} (${[...columnDefinitions, ...foreignKeys].join(", ")})${buildTableOptions(table)}`;
|
||||||
|
await runSchemaQuery({ query: sql, config });
|
||||||
|
}
|
||||||
2
dist/lib/schema/get-existing-tables-from-tables-manager-table.d.ts
vendored
Normal file
2
dist/lib/schema/get-existing-tables-from-tables-manager-table.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import type { CreateDBSchemaParams } from "../../types";
|
||||||
|
export default function getExistingTablesFromTablesManagerTable({ config, }: CreateDBSchemaParams): Promise<(string | undefined)[]>;
|
||||||
10
dist/lib/schema/get-existing-tables-from-tables-manager-table.js
vendored
Normal file
10
dist/lib/schema/get-existing-tables-from-tables-manager-table.js
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { AppData } from "../../data/app-data";
|
||||||
|
import dbHandler from "../db-handler";
|
||||||
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
|
export default async function getExistingTablesFromTablesManagerTable({ config, }) {
|
||||||
|
const rows = await dbHandler({
|
||||||
|
query: `SELECT table_name FROM ${MariaDBQuoteGen(AppData["DbSchemaManagerTableName"])}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
return rows.payload?.map((row) => row.table_name) || [];
|
||||||
|
}
|
||||||
10
dist/lib/schema/get-table-columns.d.ts
vendored
Normal file
10
dist/lib/schema/get-table-columns.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import type { BunMariaDBConfig } from "../../types";
|
||||||
|
export type ColumnInfoRow = {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
comment?: string;
|
||||||
|
};
|
||||||
|
export default function getTableColumns({ tableName, config, }: {
|
||||||
|
tableName: string;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<ColumnInfoRow[]>;
|
||||||
15
dist/lib/schema/get-table-columns.js
vendored
Normal file
15
dist/lib/schema/get-table-columns.js
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { querySchemaRows } from "./run-schema-query";
|
||||||
|
import schemaCondition from "./schema-condition";
|
||||||
|
export default async function getTableColumns({ tableName, config, }) {
|
||||||
|
const schemaCond = schemaCondition(config);
|
||||||
|
const rows = await querySchemaRows({
|
||||||
|
query: `SELECT COLUMN_NAME, COLUMN_TYPE, COLUMN_COMMENT FROM information_schema.COLUMNS WHERE ${schemaCond.where} AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION`,
|
||||||
|
values: [...schemaCond.values, tableName],
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
return rows.map((row) => ({
|
||||||
|
name: row.COLUMN_NAME,
|
||||||
|
type: row.COLUMN_TYPE,
|
||||||
|
comment: row.COLUMN_COMMENT,
|
||||||
|
}));
|
||||||
|
}
|
||||||
2
dist/lib/schema/handle-db-schema-table.d.ts
vendored
Normal file
2
dist/lib/schema/handle-db-schema-table.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import type { CreateDBSchemaTableHandlerParams } from "../../types";
|
||||||
|
export default function handleDBSchemaTable({ db_schema, config, table, db_manager_table_name, existing_live_table, }: CreateDBSchemaTableHandlerParams): Promise<void>;
|
||||||
62
dist/lib/schema/handle-db-schema-table.js
vendored
Normal file
62
dist/lib/schema/handle-db-schema-table.js
vendored
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import createTable from "./create-table";
|
||||||
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
|
import resolveTable from "./resolve-table";
|
||||||
|
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
||||||
|
import schemaCondition from "./schema-condition";
|
||||||
|
import syncIndexes from "./sync-indexes";
|
||||||
|
import updateTable from "./update-table";
|
||||||
|
import upsertDbManagerTable, { removeDbManagerTable, } from "./upsert-db-manager-table";
|
||||||
|
export default async function handleDBSchemaTable({ db_schema, config, table, db_manager_table_name, existing_live_table, }) {
|
||||||
|
const resolvedTable = resolveTable(table, db_schema);
|
||||||
|
let tableExistsTracked = Boolean(db_manager_table_name);
|
||||||
|
let tableExistsLive = Boolean(existing_live_table?.TABLE_NAME);
|
||||||
|
let wasRenamed = false;
|
||||||
|
if (resolvedTable.tableNameOld &&
|
||||||
|
resolvedTable.tableNameOld !== resolvedTable.tableName) {
|
||||||
|
// Only hit information_schema when a rename is declared
|
||||||
|
const schemaCond = schemaCondition(config);
|
||||||
|
const liveTables = await querySchemaRows({
|
||||||
|
query: `SELECT TABLE_NAME FROM information_schema.TABLES WHERE ${schemaCond.where} AND TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME = ?`,
|
||||||
|
values: [...schemaCond.values, resolvedTable.tableNameOld],
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
if (liveTables.length > 0) {
|
||||||
|
console.log(`Renaming table: ${resolvedTable.tableNameOld} -> ${resolvedTable.tableName}`);
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `RENAME TABLE ${MariaDBQuoteGen(resolvedTable.tableNameOld)} TO ${MariaDBQuoteGen(resolvedTable.tableName)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
await upsertDbManagerTable({
|
||||||
|
tableName: resolvedTable.tableName,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
await removeDbManagerTable({
|
||||||
|
tableName: resolvedTable.tableNameOld,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
tableExistsTracked = true;
|
||||||
|
tableExistsLive = true;
|
||||||
|
wasRenamed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!tableExistsTracked && !tableExistsLive) {
|
||||||
|
await createTable({ table: resolvedTable, config });
|
||||||
|
await upsertDbManagerTable({
|
||||||
|
tableName: resolvedTable.tableName,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (!wasRenamed) {
|
||||||
|
await updateTable({
|
||||||
|
table: resolvedTable,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await upsertDbManagerTable({
|
||||||
|
tableName: resolvedTable.tableName,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await syncIndexes({ table: resolvedTable, config });
|
||||||
|
}
|
||||||
2
dist/lib/schema/handle-db-schema-tables.d.ts
vendored
Normal file
2
dist/lib/schema/handle-db-schema-tables.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import type { CreateDBSchemaParams } from "../../types";
|
||||||
|
export default function handleDBSchemaTables(params: CreateDBSchemaParams): Promise<void>;
|
||||||
93
dist/lib/schema/handle-db-schema-tables.js
vendored
Normal file
93
dist/lib/schema/handle-db-schema-tables.js
vendored
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import _ from "lodash";
|
||||||
|
import { AppData } from "../../data/app-data";
|
||||||
|
import handleDBSchemaTable from "./handle-db-schema-table";
|
||||||
|
import getExistingTablesFromTablesManagerTable from "./get-existing-tables-from-tables-manager-table";
|
||||||
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
|
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
||||||
|
import schemaCondition from "./schema-condition";
|
||||||
|
import { removeDbManagerTable } from "./upsert-db-manager-table";
|
||||||
|
export default async function handleDBSchemaTables(params) {
|
||||||
|
const { db_schema, config } = params;
|
||||||
|
/**
|
||||||
|
* Grab Tables that have been recorded in the Schema
|
||||||
|
* Manager Table
|
||||||
|
*/
|
||||||
|
const existing_schema_tables = await getExistingTablesFromTablesManagerTable(params);
|
||||||
|
const schemaCond = schemaCondition(config);
|
||||||
|
const existing_live_tables = await querySchemaRows({
|
||||||
|
query: `SELECT TABLE_NAME FROM information_schema.TABLES WHERE ${schemaCond.where} AND TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME != ?`,
|
||||||
|
values: [...schemaCond.values, AppData["DbSchemaManagerTableName"]],
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
for (let i = 0; i < db_schema.tables.length; i++) {
|
||||||
|
const table = db_schema.tables[i];
|
||||||
|
if (!table)
|
||||||
|
continue;
|
||||||
|
const existing_table = existing_schema_tables.find((t) => t == table.tableName);
|
||||||
|
const existing_live_table = existing_live_tables.find((t) => t.TABLE_NAME == table.tableName);
|
||||||
|
await handleDBSchemaTable({
|
||||||
|
...params,
|
||||||
|
table,
|
||||||
|
db_manager_table_name: existing_table,
|
||||||
|
existing_live_table,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Drop tables tracked by the manager but no longer in the schema.
|
||||||
|
* Skip drops when remaining schema tables still reference the table via FK
|
||||||
|
* (e.g. external tables like `users` that are referenced but not managed).
|
||||||
|
*/
|
||||||
|
const schemaTableNames = db_schema.tables.map((t) => t.tableName);
|
||||||
|
const tablesToDrop = _.uniq(existing_schema_tables.filter((tableName) => Boolean(tableName) && !schemaTableNames.includes(tableName)));
|
||||||
|
if (tablesToDrop.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fkRows = await querySchemaRows({
|
||||||
|
query: `SELECT TABLE_NAME, REFERENCED_TABLE_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE ${schemaCond.where} AND REFERENCED_TABLE_NAME IS NOT NULL`,
|
||||||
|
values: schemaCond.values,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
const referencedByRemaining = new Map();
|
||||||
|
for (const row of fkRows) {
|
||||||
|
if (!row.REFERENCED_TABLE_NAME ||
|
||||||
|
!tablesToDrop.includes(row.REFERENCED_TABLE_NAME)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Referenced by a table we are keeping
|
||||||
|
if (schemaTableNames.includes(row.TABLE_NAME) ||
|
||||||
|
!tablesToDrop.includes(row.TABLE_NAME)) {
|
||||||
|
const list = referencedByRemaining.get(row.REFERENCED_TABLE_NAME) || [];
|
||||||
|
if (!list.includes(row.TABLE_NAME)) {
|
||||||
|
list.push(row.TABLE_NAME);
|
||||||
|
}
|
||||||
|
referencedByRemaining.set(row.REFERENCED_TABLE_NAME, list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const safeToDrop = [];
|
||||||
|
for (const tableName of tablesToDrop) {
|
||||||
|
const dependents = referencedByRemaining.get(tableName);
|
||||||
|
if (dependents && dependents.length > 0) {
|
||||||
|
console.warn(`Skipping drop of table \`${tableName}\`: still referenced by ${dependents.join(", ")}. Removing from schema manager tracking only.`);
|
||||||
|
await removeDbManagerTable({ tableName, config });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
safeToDrop.push(tableName);
|
||||||
|
}
|
||||||
|
if (safeToDrop.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await runSchemaQuery({ query: `SET FOREIGN_KEY_CHECKS = 0`, config });
|
||||||
|
try {
|
||||||
|
for (const tableName of safeToDrop) {
|
||||||
|
console.log(`Dropping table: ${tableName}`);
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `DROP TABLE IF EXISTS ${MariaDBQuoteGen(tableName)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
await removeDbManagerTable({ tableName, config });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
await runSchemaQuery({ query: `SET FOREIGN_KEY_CHECKS = 1`, config });
|
||||||
|
}
|
||||||
|
}
|
||||||
5
dist/lib/schema/is-vector-field.d.ts
vendored
Normal file
5
dist/lib/schema/is-vector-field.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import type { BUN_MARIADB_FieldSchemaType } from "../../types";
|
||||||
|
/**
|
||||||
|
* True when a field is a MariaDB native vector column.
|
||||||
|
*/
|
||||||
|
export default function isVectorField(field?: BUN_MARIADB_FieldSchemaType): boolean;
|
||||||
8
dist/lib/schema/is-vector-field.js
vendored
Normal file
8
dist/lib/schema/is-vector-field.js
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* True when a field is a MariaDB native vector column.
|
||||||
|
*/
|
||||||
|
export default function isVectorField(field) {
|
||||||
|
if (!field)
|
||||||
|
return false;
|
||||||
|
return field.isVector === true || field.dataType === "VECTOR";
|
||||||
|
}
|
||||||
2
dist/lib/schema/map-data-types.d.ts
vendored
Normal file
2
dist/lib/schema/map-data-types.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import type { BUN_MARIADB_FieldSchemaType } from "../../types";
|
||||||
|
export default function mapDataType(field: BUN_MARIADB_FieldSchemaType): string;
|
||||||
93
dist/lib/schema/map-data-types.js
vendored
Normal file
93
dist/lib/schema/map-data-types.js
vendored
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
export default function mapDataType(field) {
|
||||||
|
const dataType = field.dataType?.toUpperCase() || "TEXT";
|
||||||
|
const vectorSize = field.vectorSize || 1536;
|
||||||
|
// Native MariaDB VECTOR type (11.7+). Prefer this over LONGTEXT storage.
|
||||||
|
if (field.isVector || dataType === "VECTOR") {
|
||||||
|
return `VECTOR(${vectorSize})`;
|
||||||
|
}
|
||||||
|
switch (dataType) {
|
||||||
|
case "CHAR":
|
||||||
|
return `CHAR(${field.integerLength || 255})`;
|
||||||
|
case "VARCHAR":
|
||||||
|
return `VARCHAR(${field.integerLength || 255})`;
|
||||||
|
case "TEXT":
|
||||||
|
return "TEXT";
|
||||||
|
case "TINYTEXT":
|
||||||
|
return "TINYTEXT";
|
||||||
|
case "MEDIUMTEXT":
|
||||||
|
return "MEDIUMTEXT";
|
||||||
|
case "LONGTEXT":
|
||||||
|
return "LONGTEXT";
|
||||||
|
case "TINYINT":
|
||||||
|
return field.integerLength
|
||||||
|
? `TINYINT(${field.integerLength})`
|
||||||
|
: "TINYINT";
|
||||||
|
case "SMALLINT":
|
||||||
|
return field.integerLength
|
||||||
|
? `SMALLINT(${field.integerLength})`
|
||||||
|
: "SMALLINT";
|
||||||
|
case "MEDIUMINT":
|
||||||
|
return field.integerLength
|
||||||
|
? `MEDIUMINT(${field.integerLength})`
|
||||||
|
: "MEDIUMINT";
|
||||||
|
case "INT":
|
||||||
|
return field.integerLength ? `INT(${field.integerLength})` : "INT";
|
||||||
|
case "BIGINT":
|
||||||
|
return field.integerLength
|
||||||
|
? `BIGINT(${field.integerLength})`
|
||||||
|
: "BIGINT";
|
||||||
|
case "FLOAT":
|
||||||
|
return "FLOAT";
|
||||||
|
case "DOUBLE":
|
||||||
|
return "DOUBLE";
|
||||||
|
case "DECIMAL":
|
||||||
|
if (field.integerLength && field.decimals) {
|
||||||
|
return `DECIMAL(${field.integerLength}, ${field.decimals})`;
|
||||||
|
}
|
||||||
|
return "DECIMAL(10,2)";
|
||||||
|
case "BINARY":
|
||||||
|
return `BINARY(${field.integerLength || 1})`;
|
||||||
|
case "VARBINARY":
|
||||||
|
return `VARBINARY(${field.integerLength || 255})`;
|
||||||
|
case "BLOB":
|
||||||
|
return "BLOB";
|
||||||
|
case "TINYBLOB":
|
||||||
|
return "TINYBLOB";
|
||||||
|
case "MEDIUMBLOB":
|
||||||
|
return "MEDIUMBLOB";
|
||||||
|
case "LONGBLOB":
|
||||||
|
return "LONGBLOB";
|
||||||
|
case "DATE":
|
||||||
|
return "DATE";
|
||||||
|
case "TIME":
|
||||||
|
return "TIME";
|
||||||
|
case "DATETIME":
|
||||||
|
return "DATETIME";
|
||||||
|
case "TIMESTAMP":
|
||||||
|
return "TIMESTAMP";
|
||||||
|
case "YEAR":
|
||||||
|
return "YEAR";
|
||||||
|
case "UUID":
|
||||||
|
return "CHAR(36)"; // MariaDB does not have a native UUID type
|
||||||
|
case "JSON":
|
||||||
|
return "JSON";
|
||||||
|
case "INET6":
|
||||||
|
return "INET6";
|
||||||
|
case "BOOLEAN":
|
||||||
|
return "TINYINT(1)";
|
||||||
|
case "ENUM": {
|
||||||
|
const enumVals = (field.options || [])
|
||||||
|
.map((v) => `'${String(v).replace(/'/g, "''")}'`)
|
||||||
|
.join(", ");
|
||||||
|
return `ENUM(${enumVals || "''"})`;
|
||||||
|
}
|
||||||
|
case "SET": {
|
||||||
|
const setVals = (field.options || [])
|
||||||
|
.map((v) => `'${String(v).replace(/'/g, "''")}'`)
|
||||||
|
.join(", ");
|
||||||
|
return `SET(${setVals || "''"})`;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return "TEXT";
|
||||||
|
}
|
||||||
|
}
|
||||||
1
dist/lib/schema/mariadb-quote-gen.d.ts
vendored
Normal file
1
dist/lib/schema/mariadb-quote-gen.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default function MariaDBQuoteGen(str: string): string;
|
||||||
3
dist/lib/schema/mariadb-quote-gen.js
vendored
Normal file
3
dist/lib/schema/mariadb-quote-gen.js
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default function MariaDBQuoteGen(str) {
|
||||||
|
return `\`${str.replace(/`/g, "``")}\``;
|
||||||
|
}
|
||||||
2
dist/lib/schema/order-db-schema.d.ts
vendored
Normal file
2
dist/lib/schema/order-db-schema.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import type { BUN_MARIADB_DatabaseSchemaType, CreateDBSchemaParams } from "../../types";
|
||||||
|
export default function orderDBSchema({ db_schema, }: CreateDBSchemaParams): Promise<BUN_MARIADB_DatabaseSchemaType>;
|
||||||
57
dist/lib/schema/order-db-schema.js
vendored
Normal file
57
dist/lib/schema/order-db-schema.js
vendored
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import _ from "lodash";
|
||||||
|
export default async function orderDBSchema({ db_schema, }) {
|
||||||
|
let new_db_schema = _.cloneDeep(db_schema);
|
||||||
|
const tables = new_db_schema.tables;
|
||||||
|
let new_tables_set = new Set();
|
||||||
|
let new_tables_start_set = new Set();
|
||||||
|
let new_tables_end_set = new Set();
|
||||||
|
function setParentTable(table) {
|
||||||
|
const fields = table.fields;
|
||||||
|
let does_table_have_foreign_keys = false;
|
||||||
|
for (let f = 0; f < fields.length; f++) {
|
||||||
|
const field = fields[f];
|
||||||
|
if (!field)
|
||||||
|
continue;
|
||||||
|
const dst_table_name = field.foreignKey?.destinationTableName;
|
||||||
|
if (dst_table_name) {
|
||||||
|
const fk_table = tables.find((tb) => tb.tableName == dst_table_name);
|
||||||
|
if (fk_table &&
|
||||||
|
!new_tables_start_set.has(fk_table) &&
|
||||||
|
!new_tables_end_set.has(fk_table)) {
|
||||||
|
setParentTable(fk_table);
|
||||||
|
}
|
||||||
|
new_tables_end_set.add(table);
|
||||||
|
if (fk_table) {
|
||||||
|
new_tables_start_set.add(fk_table);
|
||||||
|
}
|
||||||
|
does_table_have_foreign_keys = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { does_table_have_foreign_keys };
|
||||||
|
}
|
||||||
|
for (let i = 0; i < tables.length; i++) {
|
||||||
|
const table = tables[i];
|
||||||
|
if (!table) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let { does_table_have_foreign_keys } = setParentTable(table);
|
||||||
|
if (does_table_have_foreign_keys) {
|
||||||
|
new_tables_end_set.add(table);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
new_tables_start_set.add(table);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const parsed_tables = [
|
||||||
|
...Array.from(new_tables_start_set),
|
||||||
|
...Array.from(new_tables_end_set),
|
||||||
|
];
|
||||||
|
for (let nt = 0; nt < parsed_tables.length; nt++) {
|
||||||
|
const new_table = parsed_tables[nt];
|
||||||
|
if (new_table) {
|
||||||
|
new_tables_set.add(new_table);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
new_db_schema.tables = Array.from(new_tables_set);
|
||||||
|
return new_db_schema;
|
||||||
|
}
|
||||||
9
dist/lib/schema/recreate-table.d.ts
vendored
Normal file
9
dist/lib/schema/recreate-table.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import type { BUN_MARIADB_TableSchemaType, BunMariaDBConfig } from "../../types";
|
||||||
|
/**
|
||||||
|
* Full table rebuild. For `isVector` tables this drops and recreates in place
|
||||||
|
* (preserving rows when possible). For regular tables it uses a temp-table swap.
|
||||||
|
*/
|
||||||
|
export default function recreateTable({ table, config, }: {
|
||||||
|
table: BUN_MARIADB_TableSchemaType;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void>;
|
||||||
111
dist/lib/schema/recreate-table.js
vendored
Normal file
111
dist/lib/schema/recreate-table.js
vendored
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import createTable from "./create-table";
|
||||||
|
import getTableColumns from "./get-table-columns";
|
||||||
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
|
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
||||||
|
import schemaCondition from "./schema-condition";
|
||||||
|
async function checkIfTableExists({ tableName, config, }) {
|
||||||
|
const schemaCond = schemaCondition(config);
|
||||||
|
const rows = await querySchemaRows({
|
||||||
|
query: `SELECT 1 AS \`table_exists\` FROM information_schema.TABLES WHERE ${schemaCond.where} AND TABLE_NAME = ? LIMIT 1`,
|
||||||
|
values: [...schemaCond.values, tableName],
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
return Boolean(rows[0]?.table_exists);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Full table rebuild. For `isVector` tables this drops and recreates in place
|
||||||
|
* (preserving rows when possible). For regular tables it uses a temp-table swap.
|
||||||
|
*/
|
||||||
|
export default async function recreateTable({ table, config, }) {
|
||||||
|
const doesTableExist = await checkIfTableExists({
|
||||||
|
tableName: table.tableName,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
if (!doesTableExist) {
|
||||||
|
await createTable({ table, config });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Vector tables: drop + recreate + reinsert (MariaDB VECTOR INDEX / dim
|
||||||
|
* changes are not reliably alterable in place).
|
||||||
|
*/
|
||||||
|
if (table.isVector) {
|
||||||
|
console.log(`Recreating vector table: ${table.tableName}`);
|
||||||
|
const existingRows = await querySchemaRows({
|
||||||
|
query: `SELECT * FROM ${MariaDBQuoteGen(table.tableName)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
await runSchemaQuery({ query: `SET FOREIGN_KEY_CHECKS = 0`, config });
|
||||||
|
try {
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `DROP TABLE IF EXISTS ${MariaDBQuoteGen(table.tableName)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
await createTable({ table, config });
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
await runSchemaQuery({ query: `SET FOREIGN_KEY_CHECKS = 1`, config });
|
||||||
|
}
|
||||||
|
if (existingRows.length > 0) {
|
||||||
|
const schemaFieldNames = new Set((table.fields || [])
|
||||||
|
.map((f) => f.fieldName)
|
||||||
|
.filter((n) => Boolean(n)));
|
||||||
|
for (const row of existingRows) {
|
||||||
|
const columns = Object.keys(row).filter((c) => schemaFieldNames.has(c));
|
||||||
|
if (columns.length === 0)
|
||||||
|
continue;
|
||||||
|
const placeholders = columns.map(() => "?").join(", ");
|
||||||
|
const columnList = columns
|
||||||
|
.map((c) => MariaDBQuoteGen(c))
|
||||||
|
.join(", ");
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `INSERT INTO ${MariaDBQuoteGen(table.tableName)} (${columnList}) VALUES (${placeholders})`,
|
||||||
|
values: columns.map((c) => row[c] ?? null),
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tempTableName = `${table.tableName}_temp_${Date.now()}`;
|
||||||
|
const backupOldTableName = `${table.tableName}_old_${Date.now()}`;
|
||||||
|
const existingColumns = await getTableColumns({
|
||||||
|
tableName: table.tableName,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
const columnsToKeep = (table.fields || [])
|
||||||
|
.filter((field) => existingColumns.some((column) => column.name === field.fieldName))
|
||||||
|
.map((field) => field.fieldName)
|
||||||
|
.filter((fieldName) => Boolean(fieldName));
|
||||||
|
await createTable({
|
||||||
|
table: { ...table, tableName: tempTableName },
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
if (columnsToKeep.length > 0) {
|
||||||
|
const columnList = columnsToKeep
|
||||||
|
.map((column) => MariaDBQuoteGen(column))
|
||||||
|
.join(", ");
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `INSERT INTO ${MariaDBQuoteGen(tempTableName)} (${columnList}) SELECT ${columnList} FROM ${MariaDBQuoteGen(table.tableName)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await runSchemaQuery({ query: `SET FOREIGN_KEY_CHECKS = 0`, config });
|
||||||
|
try {
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `RENAME TABLE ${MariaDBQuoteGen(table.tableName)} TO ${MariaDBQuoteGen(backupOldTableName)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `RENAME TABLE ${MariaDBQuoteGen(tempTableName)} TO ${MariaDBQuoteGen(table.tableName)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `DROP TABLE ${MariaDBQuoteGen(backupOldTableName)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
await runSchemaQuery({ query: `SET FOREIGN_KEY_CHECKS = 1`, config });
|
||||||
|
}
|
||||||
|
}
|
||||||
2
dist/lib/schema/resolve-table.d.ts
vendored
Normal file
2
dist/lib/schema/resolve-table.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import type { BUN_MARIADB_DatabaseSchemaType, BUN_MARIADB_TableSchemaType } from "../../types";
|
||||||
|
export default function resolveTable(table: BUN_MARIADB_TableSchemaType, db_schema: BUN_MARIADB_DatabaseSchemaType): BUN_MARIADB_TableSchemaType;
|
||||||
34
dist/lib/schema/resolve-table.js
vendored
Normal file
34
dist/lib/schema/resolve-table.js
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import _ from "lodash";
|
||||||
|
export default function resolveTable(table, db_schema) {
|
||||||
|
if (!table.parentTableName) {
|
||||||
|
return _.cloneDeep(table);
|
||||||
|
}
|
||||||
|
const parentTable = db_schema.tables.find((schemaTable) => schemaTable.tableName === table.parentTableName);
|
||||||
|
if (!parentTable) {
|
||||||
|
throw new Error(`Parent table \`${table.parentTableName}\` not found for \`${table.tableName}\``);
|
||||||
|
}
|
||||||
|
const mergedFieldsMap = new Map();
|
||||||
|
(parentTable.fields || []).forEach((f) => {
|
||||||
|
if (f.fieldName)
|
||||||
|
mergedFieldsMap.set(f.fieldName, _.cloneDeep(f));
|
||||||
|
});
|
||||||
|
(table.fields || []).forEach((f) => {
|
||||||
|
if (f.fieldName) {
|
||||||
|
const existing = mergedFieldsMap.get(f.fieldName) || {};
|
||||||
|
mergedFieldsMap.set(f.fieldName, _.merge({}, existing, f));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
..._.cloneDeep(parentTable),
|
||||||
|
tableName: table.tableName,
|
||||||
|
tableDescription: table.tableDescription || parentTable.tableDescription,
|
||||||
|
collation: table.collation || parentTable.collation,
|
||||||
|
isVector: table.isVector !== undefined ? table.isVector : parentTable.isVector,
|
||||||
|
fields: Array.from(mergedFieldsMap.values()),
|
||||||
|
indexes: _.uniqBy([...(parentTable.indexes || []), ...(table.indexes || [])], "indexName"),
|
||||||
|
uniqueConstraints: [
|
||||||
|
...(parentTable.uniqueConstraints || []),
|
||||||
|
...(table.uniqueConstraints || []),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
11
dist/lib/schema/run-schema-query.d.ts
vendored
Normal file
11
dist/lib/schema/run-schema-query.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import type { BunMariaDBConfig } from "../../types";
|
||||||
|
export default function runSchemaQuery({ query, values, config, }: {
|
||||||
|
query: string;
|
||||||
|
values?: any[];
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void>;
|
||||||
|
export declare function querySchemaRows<T extends Record<string, any>>({ query, values, config, }: {
|
||||||
|
query: string;
|
||||||
|
values?: any[];
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<T[]>;
|
||||||
22
dist/lib/schema/run-schema-query.js
vendored
Normal file
22
dist/lib/schema/run-schema-query.js
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import dbHandler from "../db-handler";
|
||||||
|
export default async function runSchemaQuery({ query, values, config, }) {
|
||||||
|
const res = await dbHandler({
|
||||||
|
query,
|
||||||
|
values,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
if (!res.success) {
|
||||||
|
throw new Error(`Database query failed: ${query} ... ERROR: ${res.error || res.msg}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function querySchemaRows({ query, values, config, }) {
|
||||||
|
const res = await dbHandler({
|
||||||
|
query,
|
||||||
|
values,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
if (!res.success) {
|
||||||
|
throw new Error(`Database query failed: ${query} ... ERROR: ${res.error || res.msg}`);
|
||||||
|
}
|
||||||
|
return (res.payload || []);
|
||||||
|
}
|
||||||
5
dist/lib/schema/schema-condition.d.ts
vendored
Normal file
5
dist/lib/schema/schema-condition.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import type { BunMariaDBConfig } from "../../types";
|
||||||
|
export default function schemaCondition(config?: BunMariaDBConfig): {
|
||||||
|
where: string;
|
||||||
|
values: string[];
|
||||||
|
};
|
||||||
13
dist/lib/schema/schema-condition.js
vendored
Normal file
13
dist/lib/schema/schema-condition.js
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export default function schemaCondition(config) {
|
||||||
|
const databaseName = config?.db_name || global.CONFIG?.db_name;
|
||||||
|
if (databaseName) {
|
||||||
|
return {
|
||||||
|
where: "TABLE_SCHEMA = ?",
|
||||||
|
values: [databaseName],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
where: "TABLE_SCHEMA = DATABASE()",
|
||||||
|
values: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
5
dist/lib/schema/sync-indexes.d.ts
vendored
Normal file
5
dist/lib/schema/sync-indexes.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import type { BUN_MARIADB_TableSchemaType, BunMariaDBConfig } from "../../types";
|
||||||
|
export default function syncIndexes({ table, config, }: {
|
||||||
|
table: BUN_MARIADB_TableSchemaType;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void>;
|
||||||
131
dist/lib/schema/sync-indexes.js
vendored
Normal file
131
dist/lib/schema/sync-indexes.js
vendored
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import isVectorField from "./is-vector-field";
|
||||||
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
|
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
||||||
|
import schemaCondition from "./schema-condition";
|
||||||
|
function isVectorIndexDef(index, table) {
|
||||||
|
if (index.indexType === "VECTOR")
|
||||||
|
return true;
|
||||||
|
if (table.isVector)
|
||||||
|
return true;
|
||||||
|
const firstFieldName = index.indexTableFields?.[0];
|
||||||
|
if (!firstFieldName)
|
||||||
|
return false;
|
||||||
|
const field = table.fields?.find((f) => f.fieldName === firstFieldName);
|
||||||
|
return isVectorField(field);
|
||||||
|
}
|
||||||
|
function vectorDistanceMetric(index) {
|
||||||
|
if (index.vectorDistanceMetric === "cosine")
|
||||||
|
return "cosine";
|
||||||
|
if (index.vectorDistanceMetric === "euclidean")
|
||||||
|
return "euclidean";
|
||||||
|
return "euclidean";
|
||||||
|
}
|
||||||
|
export default async function syncIndexes({ table, config, }) {
|
||||||
|
const schemaCond = schemaCondition(config);
|
||||||
|
const rows = await querySchemaRows({
|
||||||
|
query: `SELECT INDEX_NAME, COLUMN_NAME, INDEX_TYPE FROM information_schema.STATISTICS WHERE ${schemaCond.where} AND TABLE_NAME = ? AND INDEX_NAME <> 'PRIMARY' ORDER BY INDEX_NAME, SEQ_IN_INDEX`,
|
||||||
|
values: [...schemaCond.values, table.tableName],
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* Indexes required by foreign keys / unique constraints cannot be dropped
|
||||||
|
* freely. Skip those when cleaning up schema indexes.
|
||||||
|
*/
|
||||||
|
const protectedConstraintRows = await querySchemaRows({
|
||||||
|
query: `SELECT CONSTRAINT_NAME, CONSTRAINT_TYPE FROM information_schema.TABLE_CONSTRAINTS WHERE ${schemaCond.where} AND TABLE_NAME = ? AND CONSTRAINT_TYPE IN ('FOREIGN KEY', 'UNIQUE')`,
|
||||||
|
values: [...schemaCond.values, table.tableName],
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
const protectedIndexNames = new Set(protectedConstraintRows.map((r) => r.CONSTRAINT_NAME));
|
||||||
|
// Column-level UNIQUE creates an index often named after the column
|
||||||
|
for (const field of table.fields || []) {
|
||||||
|
if (field.unique && field.fieldName) {
|
||||||
|
protectedIndexNames.add(field.fieldName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const constraint of table.uniqueConstraints || []) {
|
||||||
|
if (constraint.constraintName) {
|
||||||
|
protectedIndexNames.add(constraint.constraintName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const existingIndexesMap = new Map();
|
||||||
|
for (const row of rows) {
|
||||||
|
if (!existingIndexesMap.has(row.INDEX_NAME)) {
|
||||||
|
existingIndexesMap.set(row.INDEX_NAME, {
|
||||||
|
columns: [],
|
||||||
|
type: row.INDEX_TYPE,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
existingIndexesMap.get(row.INDEX_NAME).columns.push(row.COLUMN_NAME);
|
||||||
|
}
|
||||||
|
for (const [indexName, details] of existingIndexesMap.entries()) {
|
||||||
|
if (protectedIndexNames.has(indexName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const schemaIndex = table.indexes?.find((i) => i.indexName === indexName);
|
||||||
|
if (!schemaIndex) {
|
||||||
|
console.log(`Dropping index: ${indexName}`);
|
||||||
|
try {
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `DROP INDEX ${MariaDBQuoteGen(indexName)} ON ${MariaDBQuoteGen(table.tableName)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
if (String(err?.message || "").includes("needed in a foreign key constraint")) {
|
||||||
|
console.warn(`Skipping drop of index ${indexName}: required by a foreign key constraint`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const schemaColumns = schemaIndex.indexTableFields || [];
|
||||||
|
const columnsMatch = details.columns.length === schemaColumns.length &&
|
||||||
|
details.columns.every((col, idx) => col === schemaColumns[idx]);
|
||||||
|
if (!columnsMatch) {
|
||||||
|
console.log(`Recreating changed index: ${indexName}`);
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `DROP INDEX ${MariaDBQuoteGen(indexName)} ON ${MariaDBQuoteGen(table.tableName)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
existingIndexesMap.delete(indexName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const index of table.indexes || []) {
|
||||||
|
if (!index.indexName ||
|
||||||
|
!index.indexTableFields ||
|
||||||
|
index.indexTableFields.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!existingIndexesMap.has(index.indexName)) {
|
||||||
|
if (isVectorIndexDef(index, table)) {
|
||||||
|
console.log(`Creating Vector index: ${index.indexName}`);
|
||||||
|
const targetField = MariaDBQuoteGen(index.indexTableFields[0]);
|
||||||
|
const distanceMetric = vectorDistanceMetric(index);
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} ADD VECTOR INDEX ${MariaDBQuoteGen(index.indexName)} (${targetField}) M=8 DISTANCE=${distanceMetric}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log(`Creating standard index: ${index.indexName}`);
|
||||||
|
const fields = index.indexTableFields
|
||||||
|
.map((field) => MariaDBQuoteGen(field))
|
||||||
|
.join(", ");
|
||||||
|
const typeUpper = index.indexType?.toUpperCase();
|
||||||
|
const isSpecialType = typeUpper === "FULLTEXT" || typeUpper === "SPATIAL";
|
||||||
|
const indexPrefix = isSpecialType ? `${typeUpper} ` : "";
|
||||||
|
const indexSuffix = !isSpecialType &&
|
||||||
|
(typeUpper === "BTREE" || typeUpper === "HASH")
|
||||||
|
? ` USING ${typeUpper}`
|
||||||
|
: "";
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `CREATE ${indexPrefix}INDEX ${MariaDBQuoteGen(index.indexName)} ON ${MariaDBQuoteGen(table.tableName)} (${fields})${indexSuffix}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
dist/lib/schema/update-table.d.ts
vendored
Normal file
5
dist/lib/schema/update-table.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import type { BUN_MARIADB_TableSchemaType, BunMariaDBConfig } from "../../types";
|
||||||
|
export default function updateTable({ table, config, }: {
|
||||||
|
table: BUN_MARIADB_TableSchemaType;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void>;
|
||||||
177
dist/lib/schema/update-table.js
vendored
Normal file
177
dist/lib/schema/update-table.js
vendored
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
import buildColumnDefinition from "./build-column-definition";
|
||||||
|
import createTable from "./create-table";
|
||||||
|
import getTableColumns from "./get-table-columns";
|
||||||
|
import isVectorField from "./is-vector-field";
|
||||||
|
import mapDataType from "./map-data-types";
|
||||||
|
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||||
|
import recreateTable from "./recreate-table";
|
||||||
|
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
||||||
|
import schemaCondition from "./schema-condition";
|
||||||
|
/**
|
||||||
|
* Compare live COLUMN_TYPE with schema-mapped type.
|
||||||
|
* Live types often include display widths (e.g. bigint(20) vs BIGINT).
|
||||||
|
*/
|
||||||
|
function columnTypesMatch(liveType, expectedType) {
|
||||||
|
const live = liveType.toLowerCase().replace(/\s+/g, "");
|
||||||
|
const expected = expectedType.toLowerCase().replace(/\s+/g, "");
|
||||||
|
if (live === expected)
|
||||||
|
return true;
|
||||||
|
// live may include display width: bigint(20) vs bigint
|
||||||
|
if (live.startsWith(`${expected}(`))
|
||||||
|
return true;
|
||||||
|
// expected may include length live omits in some versions
|
||||||
|
if (expected.startsWith(`${live}(`))
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function vectorTypeDiverged(liveType, liveComment, field) {
|
||||||
|
const dimensions = field.vectorSize || 1536;
|
||||||
|
const expectedNative = `vector(${dimensions})`;
|
||||||
|
const live = liveType.toLowerCase().replace(/\s+/g, "");
|
||||||
|
if (live === expectedNative)
|
||||||
|
return false;
|
||||||
|
// Legacy LONGTEXT storage with vector_size comment
|
||||||
|
if (live.startsWith("longtext") || live.startsWith("text")) {
|
||||||
|
const match = liveComment.match(/vector_size\s*=\s*(\d+)/i);
|
||||||
|
if (match && Number(match[1]) === dimensions) {
|
||||||
|
// Still legacy storage — treat as diverged so we can migrate to VECTOR
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
async function addColumn({ tableName, field, config, }) {
|
||||||
|
console.log(`Adding column: ${tableName}.${field.fieldName}`);
|
||||||
|
const columnDef = buildColumnDefinition(field).trim();
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `ALTER TABLE ${MariaDBQuoteGen(tableName)} ADD COLUMN IF NOT EXISTS ${columnDef}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function modifyColumn({ tableName, field, config, }) {
|
||||||
|
console.log(`Modifying column: ${tableName}.${field.fieldName}`);
|
||||||
|
const columnDef = buildColumnDefinition(field).trim();
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `ALTER TABLE ${MariaDBQuoteGen(tableName)} MODIFY COLUMN ${columnDef}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function dropColumn({ tableName, fieldName, config, }) {
|
||||||
|
console.log(`Dropping column: ${tableName}.${fieldName}`);
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `ALTER TABLE ${MariaDBQuoteGen(tableName)} DROP COLUMN ${MariaDBQuoteGen(fieldName)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export default async function updateTable({ table, config, }) {
|
||||||
|
const existingColumns = await getTableColumns({
|
||||||
|
tableName: table.tableName,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
if (existingColumns.length === 0) {
|
||||||
|
await createTable({ table, config });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const liveFieldsMap = new Map(existingColumns.map((col) => [
|
||||||
|
col.name,
|
||||||
|
{ type: col.type.toLowerCase(), comment: col.comment || "" },
|
||||||
|
]));
|
||||||
|
const codeFieldsMap = new Map((table.fields || []).map((f) => [f.fieldName, f]));
|
||||||
|
const fieldsToAdd = [];
|
||||||
|
const fieldsToModify = [];
|
||||||
|
const fieldsToDrop = [];
|
||||||
|
let needsVectorRecreate = false;
|
||||||
|
for (const field of table.fields || []) {
|
||||||
|
if (!field.fieldName)
|
||||||
|
continue;
|
||||||
|
const liveField = liveFieldsMap.get(field.fieldName);
|
||||||
|
if (!liveField) {
|
||||||
|
// Adding a new vector column can require rebuild if VECTOR INDEX
|
||||||
|
// constraints conflict; still try surgical add first.
|
||||||
|
fieldsToAdd.push(field);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let typeDiverged = !columnTypesMatch(liveField.type, mapDataType(field));
|
||||||
|
if (isVectorField(field)) {
|
||||||
|
typeDiverged = vectorTypeDiverged(liveField.type, liveField.comment, field);
|
||||||
|
if (typeDiverged) {
|
||||||
|
needsVectorRecreate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeDiverged) {
|
||||||
|
fieldsToModify.push(field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Vector dimension / storage type changes → full rebuild automatically
|
||||||
|
if (needsVectorRecreate) {
|
||||||
|
console.log(`Vector column change detected on \`${table.tableName}\`; recreating table`);
|
||||||
|
await recreateTable({ table, config });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const col of existingColumns) {
|
||||||
|
if (!codeFieldsMap.has(col.name)) {
|
||||||
|
fieldsToDrop.push(col.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fieldsToAdd.length === 0 &&
|
||||||
|
fieldsToModify.length === 0 &&
|
||||||
|
fieldsToDrop.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`Surgically updating table structure from database layout: ${table.tableName}`);
|
||||||
|
if (fieldsToDrop.length > 0) {
|
||||||
|
const schemaCond = schemaCondition(config);
|
||||||
|
const pkRows = await querySchemaRows({
|
||||||
|
query: `SELECT COLUMN_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE ${schemaCond.where} AND TABLE_NAME = ? AND CONSTRAINT_NAME = 'PRIMARY'`,
|
||||||
|
values: [...schemaCond.values, table.tableName],
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
const pkColumnNames = pkRows.map((r) => r.COLUMN_NAME);
|
||||||
|
if (fieldsToDrop.some((f) => pkColumnNames.includes(f))) {
|
||||||
|
console.log(`Dropping primary key because a PK column is being dropped`);
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} DROP PRIMARY KEY`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const indexRows = await querySchemaRows({
|
||||||
|
query: `SELECT INDEX_NAME, COLUMN_NAME FROM information_schema.STATISTICS WHERE ${schemaCond.where} AND TABLE_NAME = ? AND INDEX_NAME <> 'PRIMARY'`,
|
||||||
|
values: [...schemaCond.values, table.tableName],
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
const indexesToDrop = new Set();
|
||||||
|
for (const row of indexRows) {
|
||||||
|
if (fieldsToDrop.includes(row.COLUMN_NAME)) {
|
||||||
|
indexesToDrop.add(row.INDEX_NAME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const indexName of indexesToDrop) {
|
||||||
|
console.log(`Dropping index ${indexName} because it contains a dropped column`);
|
||||||
|
await runSchemaQuery({
|
||||||
|
query: `DROP INDEX ${MariaDBQuoteGen(indexName)} ON ${MariaDBQuoteGen(table.tableName)}`,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const field of fieldsToAdd) {
|
||||||
|
await addColumn({ tableName: table.tableName, field, config });
|
||||||
|
}
|
||||||
|
for (const field of fieldsToModify) {
|
||||||
|
try {
|
||||||
|
await modifyColumn({ tableName: table.tableName, field, config });
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
if (isVectorField(field)) {
|
||||||
|
console.warn(`[Vector Resize] Re-aligning dimension spaces natively via safe migration schema rebuild.`);
|
||||||
|
await recreateTable({ table, config });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const fieldName of fieldsToDrop) {
|
||||||
|
await dropColumn({ tableName: table.tableName, fieldName, config });
|
||||||
|
}
|
||||||
|
}
|
||||||
9
dist/lib/schema/upsert-db-manager-table.d.ts
vendored
Normal file
9
dist/lib/schema/upsert-db-manager-table.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import type { BunMariaDBConfig } from "../../types";
|
||||||
|
export default function upsertDbManagerTable({ tableName, config, }: {
|
||||||
|
tableName: string;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void>;
|
||||||
|
export declare function removeDbManagerTable({ tableName, config, }: {
|
||||||
|
tableName: string;
|
||||||
|
config?: BunMariaDBConfig;
|
||||||
|
}): Promise<void>;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user