From 3bbf00cdb04930603dbfb110ebd2cca64b45c5a5 Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Mon, 20 Jul 2026 21:43:05 +0100 Subject: [PATCH] Update .gitignore, add dist directory --- .gitignore | 1 - dist/commands/admin/index.d.ts | 2 + dist/commands/admin/index.js | 37 + dist/commands/admin/list-tables.d.ts | 3 + dist/commands/admin/list-tables.js | 81 + dist/commands/admin/run-sql.d.ts | 3 + dist/commands/admin/run-sql.js | 25 + dist/commands/admin/show-entries.d.ts | 5 + dist/commands/admin/show-entries.js | 91 ++ dist/commands/admin/show-fields.d.ts | 5 + dist/commands/admin/show-fields.js | 60 + dist/commands/backup.d.ts | 2 + dist/commands/backup.js | 33 + dist/commands/export.d.ts | 2 + dist/commands/export.js | 62 + dist/commands/import.d.ts | 2 + dist/commands/import.js | 86 + dist/commands/index.d.ts | 2 + dist/commands/index.js | 39 + dist/commands/restore.d.ts | 2 + dist/commands/restore.js | 50 + dist/commands/schema.d.ts | 2 + dist/commands/schema.js | 44 + dist/commands/typedef.d.ts | 2 + dist/commands/typedef.js | 29 + dist/data/app-data.d.ts | 11 + dist/data/app-data.js | 11 + dist/data/grab-dir-names.d.ts | 11 + dist/data/grab-dir-names.js | 13 + dist/functions/grab-mariadb-client.d.ts | 6 + dist/functions/grab-mariadb-client.js | 48 + dist/functions/init.d.ts | 10 + dist/functions/init.js | 80 + dist/functions/live-schema.d.ts | 7 + dist/functions/live-schema.js | 15 + dist/functions/set-mariadb-client.d.ts | 6 + dist/functions/set-mariadb-client.js | 21 + dist/index.d.ts | 20 + dist/index.js | 21 + dist/lib/db-handler.d.ts | 15 + dist/lib/db-handler.js | 61 + dist/lib/grab-duplicate-safe-insert-sql.d.ts | 7 + dist/lib/grab-duplicate-safe-insert-sql.js | 25 + dist/lib/mariadb/db-delete.d.ts | 17 + dist/lib/mariadb/db-delete.js | 68 + dist/lib/mariadb/db-generate-type-defs.d.ts | 15 + dist/lib/mariadb/db-generate-type-defs.js | 63 + dist/lib/mariadb/db-insert.d.ts | 17 + dist/lib/mariadb/db-insert.js | 66 + dist/lib/mariadb/db-schema-to-typedef.d.ts | 7 + dist/lib/mariadb/db-schema-to-typedef.js | 44 + dist/lib/mariadb/db-select.d.ts | 18 + dist/lib/mariadb/db-select.js | 94 ++ dist/lib/mariadb/db-sql.d.ts | 11 + dist/lib/mariadb/db-sql.js | 45 + dist/lib/mariadb/db-update.d.ts | 18 + dist/lib/mariadb/db-update.js | 109 ++ dist/lib/mariadb/schema-to-typedef.d.ts | 8 + dist/lib/mariadb/schema-to-typedef.js | 18 + dist/lib/schema/build-column-definition.d.ts | 2 + dist/lib/schema/build-column-definition.js | 43 + .../schema/build-foreign-key-constraint.d.ts | 2 + .../schema/build-foreign-key-constraint.js | 15 + dist/lib/schema/build-table-options.d.ts | 2 + dist/lib/schema/build-table-options.js | 7 + dist/lib/schema/create-db-manager-table.d.ts | 2 + dist/lib/schema/create-db-manager-table.js | 15 + dist/lib/schema/create-db-schema.d.ts | 2 + dist/lib/schema/create-db-schema.js | 17 + dist/lib/schema/create-table.d.ts | 5 + dist/lib/schema/create-table.js | 41 + ...ting-tables-from-tables-manager-table.d.ts | 2 + ...isting-tables-from-tables-manager-table.js | 10 + dist/lib/schema/get-table-columns.d.ts | 10 + dist/lib/schema/get-table-columns.js | 15 + dist/lib/schema/handle-db-schema-table.d.ts | 2 + dist/lib/schema/handle-db-schema-table.js | 62 + dist/lib/schema/handle-db-schema-tables.d.ts | 2 + dist/lib/schema/handle-db-schema-tables.js | 93 ++ dist/lib/schema/is-vector-field.d.ts | 5 + dist/lib/schema/is-vector-field.js | 8 + dist/lib/schema/map-data-types.d.ts | 2 + dist/lib/schema/map-data-types.js | 93 ++ dist/lib/schema/mariadb-quote-gen.d.ts | 1 + dist/lib/schema/mariadb-quote-gen.js | 3 + dist/lib/schema/order-db-schema.d.ts | 2 + dist/lib/schema/order-db-schema.js | 57 + dist/lib/schema/recreate-table.d.ts | 9 + dist/lib/schema/recreate-table.js | 111 ++ dist/lib/schema/resolve-table.d.ts | 2 + dist/lib/schema/resolve-table.js | 34 + dist/lib/schema/run-schema-query.d.ts | 11 + dist/lib/schema/run-schema-query.js | 22 + dist/lib/schema/schema-condition.d.ts | 5 + dist/lib/schema/schema-condition.js | 13 + dist/lib/schema/sync-indexes.d.ts | 5 + dist/lib/schema/sync-indexes.js | 131 ++ dist/lib/schema/update-table.d.ts | 5 + dist/lib/schema/update-table.js | 177 ++ dist/lib/schema/upsert-db-manager-table.d.ts | 9 + dist/lib/schema/upsert-db-manager-table.js | 18 + dist/types/index.d.ts | 1440 +++++++++++++++++ dist/types/index.js | 235 +++ .../append-default-fields-to-db-schema.d.ts | 6 + .../append-default-fields-to-db-schema.js | 12 + dist/utils/export-archive.d.ts | 21 + dist/utils/export-archive.js | 159 ++ dist/utils/grab-backup-data.d.ts | 12 + dist/utils/grab-backup-data.js | 33 + dist/utils/grab-db-backup-file-name.d.ts | 6 + dist/utils/grab-db-backup-file-name.js | 3 + dist/utils/grab-db-dir.d.ts | 10 + dist/utils/grab-db-dir.js | 13 + dist/utils/grab-db-schema.d.ts | 1 + dist/utils/grab-db-schema.js | 5 + .../grab-join-fields-from-query-object.d.ts | 7 + .../grab-join-fields-from-query-object.js | 55 + dist/utils/grab-sorted-backups.d.ts | 6 + dist/utils/grab-sorted-backups.js | 32 + dist/utils/grab-sorted-exports.d.ts | 6 + dist/utils/grab-sorted-exports.js | 27 + dist/utils/mariadb-cli-env.d.ts | 6 + dist/utils/mariadb-cli-env.js | 24 + dist/utils/mariadb-dump-restore.d.ts | 17 + dist/utils/mariadb-dump-restore.js | 92 ++ dist/utils/query-value-parser.d.ts | 6 + dist/utils/query-value-parser.js | 21 + dist/utils/sanitize-html-fields.d.ts | 18 + dist/utils/sanitize-html-fields.js | 54 + dist/utils/sanitize-html-options.d.ts | 7 + dist/utils/sanitize-html-options.js | 72 + dist/utils/sql-equality-parser.d.ts | 2 + dist/utils/sql-equality-parser.js | 41 + dist/utils/sql-gen-operator-gen.d.ts | 20 + dist/utils/sql-gen-operator-gen.js | 133 ++ dist/utils/sql-generator-gen-join-str.d.ts | 11 + dist/utils/sql-generator-gen-join-str.js | 65 + dist/utils/sql-generator-gen-query-str.d.ts | 22 + dist/utils/sql-generator-gen-query-str.js | 193 +++ dist/utils/sql-generator-gen-search-str.d.ts | 12 + dist/utils/sql-generator-gen-search-str.js | 92 ++ dist/utils/sql-generator-grab-concat-str.d.ts | 8 + dist/utils/sql-generator-grab-concat-str.js | 13 + .../sql-generator-grab-select-field-sql.d.ts | 16 + .../sql-generator-grab-select-field-sql.js | 55 + dist/utils/sql-generator.d.ts | 25 + dist/utils/sql-generator.js | 303 ++++ dist/utils/sql-insert-generator.d.ts | 5 + dist/utils/sql-insert-generator.js | 62 + dist/utils/trim-backups.d.ts | 6 + dist/utils/trim-backups.js | 19 + dist/utils/trim-exports.d.ts | 6 + dist/utils/trim-exports.js | 18 + 153 files changed, 6280 insertions(+), 1 deletion(-) create mode 100644 dist/commands/admin/index.d.ts create mode 100644 dist/commands/admin/index.js create mode 100644 dist/commands/admin/list-tables.d.ts create mode 100644 dist/commands/admin/list-tables.js create mode 100644 dist/commands/admin/run-sql.d.ts create mode 100644 dist/commands/admin/run-sql.js create mode 100644 dist/commands/admin/show-entries.d.ts create mode 100644 dist/commands/admin/show-entries.js create mode 100644 dist/commands/admin/show-fields.d.ts create mode 100644 dist/commands/admin/show-fields.js create mode 100644 dist/commands/backup.d.ts create mode 100644 dist/commands/backup.js create mode 100644 dist/commands/export.d.ts create mode 100644 dist/commands/export.js create mode 100644 dist/commands/import.d.ts create mode 100644 dist/commands/import.js create mode 100644 dist/commands/index.d.ts create mode 100644 dist/commands/index.js create mode 100644 dist/commands/restore.d.ts create mode 100644 dist/commands/restore.js create mode 100644 dist/commands/schema.d.ts create mode 100644 dist/commands/schema.js create mode 100644 dist/commands/typedef.d.ts create mode 100644 dist/commands/typedef.js create mode 100644 dist/data/app-data.d.ts create mode 100644 dist/data/app-data.js create mode 100644 dist/data/grab-dir-names.d.ts create mode 100644 dist/data/grab-dir-names.js create mode 100644 dist/functions/grab-mariadb-client.d.ts create mode 100644 dist/functions/grab-mariadb-client.js create mode 100644 dist/functions/init.d.ts create mode 100644 dist/functions/init.js create mode 100644 dist/functions/live-schema.d.ts create mode 100644 dist/functions/live-schema.js create mode 100644 dist/functions/set-mariadb-client.d.ts create mode 100644 dist/functions/set-mariadb-client.js create mode 100644 dist/index.d.ts create mode 100644 dist/index.js create mode 100644 dist/lib/db-handler.d.ts create mode 100644 dist/lib/db-handler.js create mode 100644 dist/lib/grab-duplicate-safe-insert-sql.d.ts create mode 100644 dist/lib/grab-duplicate-safe-insert-sql.js create mode 100644 dist/lib/mariadb/db-delete.d.ts create mode 100644 dist/lib/mariadb/db-delete.js create mode 100644 dist/lib/mariadb/db-generate-type-defs.d.ts create mode 100644 dist/lib/mariadb/db-generate-type-defs.js create mode 100644 dist/lib/mariadb/db-insert.d.ts create mode 100644 dist/lib/mariadb/db-insert.js create mode 100644 dist/lib/mariadb/db-schema-to-typedef.d.ts create mode 100644 dist/lib/mariadb/db-schema-to-typedef.js create mode 100644 dist/lib/mariadb/db-select.d.ts create mode 100644 dist/lib/mariadb/db-select.js create mode 100644 dist/lib/mariadb/db-sql.d.ts create mode 100644 dist/lib/mariadb/db-sql.js create mode 100644 dist/lib/mariadb/db-update.d.ts create mode 100644 dist/lib/mariadb/db-update.js create mode 100644 dist/lib/mariadb/schema-to-typedef.d.ts create mode 100644 dist/lib/mariadb/schema-to-typedef.js create mode 100644 dist/lib/schema/build-column-definition.d.ts create mode 100644 dist/lib/schema/build-column-definition.js create mode 100644 dist/lib/schema/build-foreign-key-constraint.d.ts create mode 100644 dist/lib/schema/build-foreign-key-constraint.js create mode 100644 dist/lib/schema/build-table-options.d.ts create mode 100644 dist/lib/schema/build-table-options.js create mode 100644 dist/lib/schema/create-db-manager-table.d.ts create mode 100644 dist/lib/schema/create-db-manager-table.js create mode 100644 dist/lib/schema/create-db-schema.d.ts create mode 100644 dist/lib/schema/create-db-schema.js create mode 100644 dist/lib/schema/create-table.d.ts create mode 100644 dist/lib/schema/create-table.js create mode 100644 dist/lib/schema/get-existing-tables-from-tables-manager-table.d.ts create mode 100644 dist/lib/schema/get-existing-tables-from-tables-manager-table.js create mode 100644 dist/lib/schema/get-table-columns.d.ts create mode 100644 dist/lib/schema/get-table-columns.js create mode 100644 dist/lib/schema/handle-db-schema-table.d.ts create mode 100644 dist/lib/schema/handle-db-schema-table.js create mode 100644 dist/lib/schema/handle-db-schema-tables.d.ts create mode 100644 dist/lib/schema/handle-db-schema-tables.js create mode 100644 dist/lib/schema/is-vector-field.d.ts create mode 100644 dist/lib/schema/is-vector-field.js create mode 100644 dist/lib/schema/map-data-types.d.ts create mode 100644 dist/lib/schema/map-data-types.js create mode 100644 dist/lib/schema/mariadb-quote-gen.d.ts create mode 100644 dist/lib/schema/mariadb-quote-gen.js create mode 100644 dist/lib/schema/order-db-schema.d.ts create mode 100644 dist/lib/schema/order-db-schema.js create mode 100644 dist/lib/schema/recreate-table.d.ts create mode 100644 dist/lib/schema/recreate-table.js create mode 100644 dist/lib/schema/resolve-table.d.ts create mode 100644 dist/lib/schema/resolve-table.js create mode 100644 dist/lib/schema/run-schema-query.d.ts create mode 100644 dist/lib/schema/run-schema-query.js create mode 100644 dist/lib/schema/schema-condition.d.ts create mode 100644 dist/lib/schema/schema-condition.js create mode 100644 dist/lib/schema/sync-indexes.d.ts create mode 100644 dist/lib/schema/sync-indexes.js create mode 100644 dist/lib/schema/update-table.d.ts create mode 100644 dist/lib/schema/update-table.js create mode 100644 dist/lib/schema/upsert-db-manager-table.d.ts create mode 100644 dist/lib/schema/upsert-db-manager-table.js create mode 100644 dist/types/index.d.ts create mode 100644 dist/types/index.js create mode 100644 dist/utils/append-default-fields-to-db-schema.d.ts create mode 100644 dist/utils/append-default-fields-to-db-schema.js create mode 100644 dist/utils/export-archive.d.ts create mode 100644 dist/utils/export-archive.js create mode 100644 dist/utils/grab-backup-data.d.ts create mode 100644 dist/utils/grab-backup-data.js create mode 100644 dist/utils/grab-db-backup-file-name.d.ts create mode 100644 dist/utils/grab-db-backup-file-name.js create mode 100644 dist/utils/grab-db-dir.d.ts create mode 100644 dist/utils/grab-db-dir.js create mode 100644 dist/utils/grab-db-schema.d.ts create mode 100644 dist/utils/grab-db-schema.js create mode 100644 dist/utils/grab-join-fields-from-query-object.d.ts create mode 100644 dist/utils/grab-join-fields-from-query-object.js create mode 100644 dist/utils/grab-sorted-backups.d.ts create mode 100644 dist/utils/grab-sorted-backups.js create mode 100644 dist/utils/grab-sorted-exports.d.ts create mode 100644 dist/utils/grab-sorted-exports.js create mode 100644 dist/utils/mariadb-cli-env.d.ts create mode 100644 dist/utils/mariadb-cli-env.js create mode 100644 dist/utils/mariadb-dump-restore.d.ts create mode 100644 dist/utils/mariadb-dump-restore.js create mode 100644 dist/utils/query-value-parser.d.ts create mode 100644 dist/utils/query-value-parser.js create mode 100644 dist/utils/sanitize-html-fields.d.ts create mode 100644 dist/utils/sanitize-html-fields.js create mode 100644 dist/utils/sanitize-html-options.d.ts create mode 100644 dist/utils/sanitize-html-options.js create mode 100644 dist/utils/sql-equality-parser.d.ts create mode 100644 dist/utils/sql-equality-parser.js create mode 100644 dist/utils/sql-gen-operator-gen.d.ts create mode 100644 dist/utils/sql-gen-operator-gen.js create mode 100644 dist/utils/sql-generator-gen-join-str.d.ts create mode 100644 dist/utils/sql-generator-gen-join-str.js create mode 100644 dist/utils/sql-generator-gen-query-str.d.ts create mode 100644 dist/utils/sql-generator-gen-query-str.js create mode 100644 dist/utils/sql-generator-gen-search-str.d.ts create mode 100644 dist/utils/sql-generator-gen-search-str.js create mode 100644 dist/utils/sql-generator-grab-concat-str.d.ts create mode 100644 dist/utils/sql-generator-grab-concat-str.js create mode 100644 dist/utils/sql-generator-grab-select-field-sql.d.ts create mode 100644 dist/utils/sql-generator-grab-select-field-sql.js create mode 100644 dist/utils/sql-generator.d.ts create mode 100644 dist/utils/sql-generator.js create mode 100644 dist/utils/sql-insert-generator.d.ts create mode 100644 dist/utils/sql-insert-generator.js create mode 100644 dist/utils/trim-backups.d.ts create mode 100644 dist/utils/trim-backups.js create mode 100644 dist/utils/trim-exports.d.ts create mode 100644 dist/utils/trim-exports.js diff --git a/.gitignore b/.gitignore index 65b3bcc..1968394 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ node_modules # output out -dist *.tgz # code coverage diff --git a/dist/commands/admin/index.d.ts b/dist/commands/admin/index.d.ts new file mode 100644 index 0000000..f202e7d --- /dev/null +++ b/dist/commands/admin/index.d.ts @@ -0,0 +1,2 @@ +import { Command } from "commander"; +export default function (): Command; diff --git a/dist/commands/admin/index.js b/dist/commands/admin/index.js new file mode 100644 index 0000000..7a3403a --- /dev/null +++ b/dist/commands/admin/index.js @@ -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(); + }); +} diff --git a/dist/commands/admin/list-tables.d.ts b/dist/commands/admin/list-tables.d.ts new file mode 100644 index 0000000..32a5441 --- /dev/null +++ b/dist/commands/admin/list-tables.d.ts @@ -0,0 +1,3 @@ +type Params = {}; +export default function listTables(params?: Params): Promise<"__exit__" | void>; +export {}; diff --git a/dist/commands/admin/list-tables.js b/dist/commands/admin/list-tables.js new file mode 100644 index 0000000..eea07fb --- /dev/null +++ b/dist/commands/admin/list-tables.js @@ -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(); + } + } + } +} diff --git a/dist/commands/admin/run-sql.d.ts b/dist/commands/admin/run-sql.d.ts new file mode 100644 index 0000000..b6f72fd --- /dev/null +++ b/dist/commands/admin/run-sql.d.ts @@ -0,0 +1,3 @@ +type Params = {}; +export default function runSQL(params?: Params): Promise; +export {}; diff --git a/dist/commands/admin/run-sql.js b/dist/commands/admin/run-sql.js new file mode 100644 index 0000000..fa1039b --- /dev/null +++ b/dist/commands/admin/run-sql.js @@ -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`)); + } +} diff --git a/dist/commands/admin/show-entries.d.ts b/dist/commands/admin/show-entries.d.ts new file mode 100644 index 0000000..64206e5 --- /dev/null +++ b/dist/commands/admin/show-entries.d.ts @@ -0,0 +1,5 @@ +type Params = { + tableName: string; +}; +export default function showEntries({ tableName }: Params): Promise<"__exit__" | undefined>; +export {}; diff --git a/dist/commands/admin/show-entries.js b/dist/commands/admin/show-entries.js new file mode 100644 index 0000000..7dafd42 --- /dev/null +++ b/dist/commands/admin/show-entries.js @@ -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; + } + } +} diff --git a/dist/commands/admin/show-fields.d.ts b/dist/commands/admin/show-fields.d.ts new file mode 100644 index 0000000..34beb3b --- /dev/null +++ b/dist/commands/admin/show-fields.d.ts @@ -0,0 +1,5 @@ +type Params = { + tableName: string; +}; +export default function showFields({ tableName, }: Params): Promise<"__exit__" | void>; +export {}; diff --git a/dist/commands/admin/show-fields.js b/dist/commands/admin/show-fields.js new file mode 100644 index 0000000..7ba7617 --- /dev/null +++ b/dist/commands/admin/show-fields.js @@ -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(); + } +} diff --git a/dist/commands/backup.d.ts b/dist/commands/backup.d.ts new file mode 100644 index 0000000..f202e7d --- /dev/null +++ b/dist/commands/backup.d.ts @@ -0,0 +1,2 @@ +import { Command } from "commander"; +export default function (): Command; diff --git a/dist/commands/backup.js b/dist/commands/backup.js new file mode 100644 index 0000000..934e43b --- /dev/null +++ b/dist/commands/backup.js @@ -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); + } + }); +} diff --git a/dist/commands/export.d.ts b/dist/commands/export.d.ts new file mode 100644 index 0000000..f202e7d --- /dev/null +++ b/dist/commands/export.d.ts @@ -0,0 +1,2 @@ +import { Command } from "commander"; +export default function (): Command; diff --git a/dist/commands/export.js b/dist/commands/export.js new file mode 100644 index 0000000..0aeeb34 --- /dev/null +++ b/dist/commands/export.js @@ -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 ", "Output archive path (.tar.gz or .zip). Defaults to db exports dir") + .option("-f, --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); + } + }); +} diff --git a/dist/commands/import.d.ts b/dist/commands/import.d.ts new file mode 100644 index 0000000..f202e7d --- /dev/null +++ b/dist/commands/import.d.ts @@ -0,0 +1,2 @@ +import { Command } from "commander"; +export default function (): Command; diff --git a/dist/commands/import.js b/dist/commands/import.js new file mode 100644 index 0000000..b19bd8f --- /dev/null +++ b/dist/commands/import.js @@ -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); + } + }); +} diff --git a/dist/commands/index.d.ts b/dist/commands/index.d.ts new file mode 100644 index 0000000..c1cf237 --- /dev/null +++ b/dist/commands/index.d.ts @@ -0,0 +1,2 @@ +#!/usr/bin/env bun +export {}; diff --git a/dist/commands/index.js b/dist/commands/index.js new file mode 100644 index 0000000..e648b1b --- /dev/null +++ b/dist/commands/index.js @@ -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); diff --git a/dist/commands/restore.d.ts b/dist/commands/restore.d.ts new file mode 100644 index 0000000..f202e7d --- /dev/null +++ b/dist/commands/restore.d.ts @@ -0,0 +1,2 @@ +import { Command } from "commander"; +export default function (): Command; diff --git a/dist/commands/restore.js b/dist/commands/restore.js new file mode 100644 index 0000000..0873406 --- /dev/null +++ b/dist/commands/restore.js @@ -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); + } + }); +} diff --git a/dist/commands/schema.d.ts b/dist/commands/schema.d.ts new file mode 100644 index 0000000..f202e7d --- /dev/null +++ b/dist/commands/schema.d.ts @@ -0,0 +1,2 @@ +import { Command } from "commander"; +export default function (): Command; diff --git a/dist/commands/schema.js b/dist/commands/schema.js new file mode 100644 index 0000000..d2417df --- /dev/null +++ b/dist/commands/schema.js @@ -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); + } + }); +} diff --git a/dist/commands/typedef.d.ts b/dist/commands/typedef.d.ts new file mode 100644 index 0000000..f202e7d --- /dev/null +++ b/dist/commands/typedef.d.ts @@ -0,0 +1,2 @@ +import { Command } from "commander"; +export default function (): Command; diff --git a/dist/commands/typedef.js b/dist/commands/typedef.js new file mode 100644 index 0000000..fe13a6f --- /dev/null +++ b/dist/commands/typedef.js @@ -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); + }); +} diff --git a/dist/data/app-data.d.ts b/dist/data/app-data.d.ts new file mode 100644 index 0000000..33f97f9 --- /dev/null +++ b/dist/data/app-data.d.ts @@ -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; +}; diff --git a/dist/data/app-data.js b/dist/data/app-data.js new file mode 100644 index 0000000..b591ea9 --- /dev/null +++ b/dist/data/app-data.js @@ -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, +}; diff --git a/dist/data/grab-dir-names.d.ts b/dist/data/grab-dir-names.d.ts new file mode 100644 index 0000000..5cdef68 --- /dev/null +++ b/dist/data/grab-dir-names.d.ts @@ -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 {}; diff --git a/dist/data/grab-dir-names.js b/dist/data/grab-dir-names.js new file mode 100644 index 0000000..82e50ef --- /dev/null +++ b/dist/data/grab-dir-names.js @@ -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, + }; +} diff --git a/dist/functions/grab-mariadb-client.d.ts b/dist/functions/grab-mariadb-client.d.ts new file mode 100644 index 0000000..c48b525 --- /dev/null +++ b/dist/functions/grab-mariadb-client.d.ts @@ -0,0 +1,6 @@ +import { type BunMariaDBConfig } from "../types"; +type Params = { + config: BunMariaDBConfig; +}; +export default function grabMariaDBClient({ config, }: Params): Bun.SQL | undefined; +export {}; diff --git a/dist/functions/grab-mariadb-client.js b/dist/functions/grab-mariadb-client.js new file mode 100644 index 0000000..ba50638 --- /dev/null +++ b/dist/functions/grab-mariadb-client.js @@ -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; + } +} diff --git a/dist/functions/init.d.ts b/dist/functions/init.d.ts new file mode 100644 index 0000000..949cd12 --- /dev/null +++ b/dist/functions/init.d.ts @@ -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; diff --git a/dist/functions/init.js b/dist/functions/init.js new file mode 100644 index 0000000..62da169 --- /dev/null +++ b/dist/functions/init.js @@ -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); + } +} diff --git a/dist/functions/live-schema.d.ts b/dist/functions/live-schema.d.ts new file mode 100644 index 0000000..5e9fcc9 --- /dev/null +++ b/dist/functions/live-schema.d.ts @@ -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 {}; diff --git a/dist/functions/live-schema.js b/dist/functions/live-schema.js new file mode 100644 index 0000000..13c282b --- /dev/null +++ b/dist/functions/live-schema.js @@ -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); +} diff --git a/dist/functions/set-mariadb-client.d.ts b/dist/functions/set-mariadb-client.d.ts new file mode 100644 index 0000000..7e3d391 --- /dev/null +++ b/dist/functions/set-mariadb-client.d.ts @@ -0,0 +1,6 @@ +import { type BunMariaDBConfig } from "../types"; +type Params = { + config: BunMariaDBConfig; +}; +export default function setMariaDBClient({ config }: Params): void; +export {}; diff --git a/dist/functions/set-mariadb-client.js b/dist/functions/set-mariadb-client.js new file mode 100644 index 0000000..54fe132 --- /dev/null +++ b/dist/functions/set-mariadb-client.js @@ -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); + } +} diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..4311b43 --- /dev/null +++ b/dist/index.d.ts @@ -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"; diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..825af04 --- /dev/null +++ b/dist/index.js @@ -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; diff --git a/dist/lib/db-handler.d.ts b/dist/lib/db-handler.d.ts new file mode 100644 index 0000000..dacda5b --- /dev/null +++ b/dist/lib/db-handler.d.ts @@ -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({ query, values, config }: Param): Promise>; +export {}; diff --git a/dist/lib/db-handler.js b/dist/lib/db-handler.js new file mode 100644 index 0000000..8343fb7 --- /dev/null +++ b/dist/lib/db-handler.js @@ -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, + }; + } +} diff --git a/dist/lib/grab-duplicate-safe-insert-sql.d.ts b/dist/lib/grab-duplicate-safe-insert-sql.d.ts new file mode 100644 index 0000000..03a70d0 --- /dev/null +++ b/dist/lib/grab-duplicate-safe-insert-sql.d.ts @@ -0,0 +1,7 @@ +type Params = { + sql: string; + table: string; + data: any | any[]; +}; +export default function ({ sql: passedSql, table, data }: Params): Promise; +export {}; diff --git a/dist/lib/grab-duplicate-safe-insert-sql.js b/dist/lib/grab-duplicate-safe-insert-sql.js new file mode 100644 index 0000000..5b92e7c --- /dev/null +++ b/dist/lib/grab-duplicate-safe-insert-sql.js @@ -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(", ")}`; +} diff --git a/dist/lib/mariadb/db-delete.d.ts b/dist/lib/mariadb/db-delete.d.ts new file mode 100644 index 0000000..672e2ca --- /dev/null +++ b/dist/lib/mariadb/db-delete.d.ts @@ -0,0 +1,17 @@ +import type { BunMariaDBConfig, DBResponseObject, ServerQueryParam } from "../../types"; +type Params = { + table: Table; + query?: ServerQueryParam; + targetId?: number | string; + config?: BunMariaDBConfig; +}; +export default function DbDelete({ table, query, targetId, config, }: Params): Promise; +export {}; diff --git a/dist/lib/mariadb/db-delete.js b/dist/lib/mariadb/db-delete.js new file mode 100644 index 0000000..359d585 --- /dev/null +++ b/dist/lib/mariadb/db-delete.js @@ -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, + }, + }; + } +} diff --git a/dist/lib/mariadb/db-generate-type-defs.d.ts b/dist/lib/mariadb/db-generate-type-defs.d.ts new file mode 100644 index 0000000..81892ca --- /dev/null +++ b/dist/lib/mariadb/db-generate-type-defs.d.ts @@ -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 {}; diff --git a/dist/lib/mariadb/db-generate-type-defs.js b/dist/lib/mariadb/db-generate-type-defs.js new file mode 100644 index 0000000..decabea --- /dev/null +++ b/dist/lib/mariadb/db-generate-type-defs.js @@ -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 | Buffer | 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 }; +} diff --git a/dist/lib/mariadb/db-insert.d.ts b/dist/lib/mariadb/db-insert.d.ts new file mode 100644 index 0000000..5f7d5df --- /dev/null +++ b/dist/lib/mariadb/db-insert.d.ts @@ -0,0 +1,17 @@ +import type { BunMariaDBConfig, DBResponseObject } from "../../types"; +type Params = { + table: Table; + data: Schema[]; + update_on_duplicate?: boolean; + config?: BunMariaDBConfig; +}; +export default function DbInsert({ table, data, update_on_duplicate, config, }: Params): Promise; +export {}; diff --git a/dist/lib/mariadb/db-insert.js b/dist/lib/mariadb/db-insert.js new file mode 100644 index 0000000..a9a653c --- /dev/null +++ b/dist/lib/mariadb/db-insert.js @@ -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, + }, + }; + } +} diff --git a/dist/lib/mariadb/db-schema-to-typedef.d.ts b/dist/lib/mariadb/db-schema-to-typedef.d.ts new file mode 100644 index 0000000..992e47f --- /dev/null +++ b/dist/lib/mariadb/db-schema-to-typedef.d.ts @@ -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 {}; diff --git a/dist/lib/mariadb/db-schema-to-typedef.js b/dist/lib/mariadb/db-schema-to-typedef.js new file mode 100644 index 0000000..93df003 --- /dev/null +++ b/dist/lib/mariadb/db-schema-to-typedef.js @@ -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]; +} diff --git a/dist/lib/mariadb/db-select.d.ts b/dist/lib/mariadb/db-select.d.ts new file mode 100644 index 0000000..9965a9c --- /dev/null +++ b/dist/lib/mariadb/db-select.d.ts @@ -0,0 +1,18 @@ +import type { BunMariaDBConfig, DBResponseObject, ServerQueryParam } from "../../types"; +type Params = { + query?: ServerQueryParam; + table: Table; + count?: boolean; + targetId?: number | string; + config?: BunMariaDBConfig; +}; +export default function DbSelect({ table, query, count, targetId, config, }: Params): Promise>; +export {}; diff --git a/dist/lib/mariadb/db-select.js b/dist/lib/mariadb/db-select.js new file mode 100644 index 0000000..5d62586 --- /dev/null +++ b/dist/lib/mariadb/db-select.js @@ -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, + }, + }; + } +} diff --git a/dist/lib/mariadb/db-sql.d.ts b/dist/lib/mariadb/db-sql.d.ts new file mode 100644 index 0000000..3984d51 --- /dev/null +++ b/dist/lib/mariadb/db-sql.d.ts @@ -0,0 +1,11 @@ +import type { DBResponseObject, SQLInsertGenValueType } from "../../types"; +type Params = { + sql: string; + values?: SQLInsertGenValueType[]; +}; +export default function DbSQL({ sql, values }: Params): Promise>; +export {}; diff --git a/dist/lib/mariadb/db-sql.js b/dist/lib/mariadb/db-sql.js new file mode 100644 index 0000000..725ca57 --- /dev/null +++ b/dist/lib/mariadb/db-sql.js @@ -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, + }; + } +} diff --git a/dist/lib/mariadb/db-update.d.ts b/dist/lib/mariadb/db-update.d.ts new file mode 100644 index 0000000..4c3a08f --- /dev/null +++ b/dist/lib/mariadb/db-update.d.ts @@ -0,0 +1,18 @@ +import type { BunMariaDBConfig, DBResponseObject, ServerQueryParam } from "../../types"; +type Params = { + table: Table; + data: Schema; + query?: ServerQueryParam; + targetId?: number | string; + config?: BunMariaDBConfig; +}; +export default function DbUpdate({ table, data, query, targetId, config, }: Params): Promise; +export {}; diff --git a/dist/lib/mariadb/db-update.js b/dist/lib/mariadb/db-update.js new file mode 100644 index 0000000..5fde08a --- /dev/null +++ b/dist/lib/mariadb/db-update.js @@ -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, + }, + }; + } +} diff --git a/dist/lib/mariadb/schema-to-typedef.d.ts b/dist/lib/mariadb/schema-to-typedef.d.ts new file mode 100644 index 0000000..4b75693 --- /dev/null +++ b/dist/lib/mariadb/schema-to-typedef.d.ts @@ -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 {}; diff --git a/dist/lib/mariadb/schema-to-typedef.js b/dist/lib/mariadb/schema-to-typedef.js new file mode 100644 index 0000000..f0c5ac3 --- /dev/null +++ b/dist/lib/mariadb/schema-to-typedef.js @@ -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); + } +} diff --git a/dist/lib/schema/build-column-definition.d.ts b/dist/lib/schema/build-column-definition.d.ts new file mode 100644 index 0000000..00bc713 --- /dev/null +++ b/dist/lib/schema/build-column-definition.d.ts @@ -0,0 +1,2 @@ +import type { BUN_MARIADB_FieldSchemaType } from "../../types"; +export default function buildColumnDefinition(field: BUN_MARIADB_FieldSchemaType): string; diff --git a/dist/lib/schema/build-column-definition.js b/dist/lib/schema/build-column-definition.js new file mode 100644 index 0000000..f5298bc --- /dev/null +++ b/dist/lib/schema/build-column-definition.js @@ -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(" "); +} diff --git a/dist/lib/schema/build-foreign-key-constraint.d.ts b/dist/lib/schema/build-foreign-key-constraint.d.ts new file mode 100644 index 0000000..cd51d92 --- /dev/null +++ b/dist/lib/schema/build-foreign-key-constraint.d.ts @@ -0,0 +1,2 @@ +import type { BUN_MARIADB_FieldSchemaType } from "../../types"; +export default function buildForeignKeyConstraint(field: BUN_MARIADB_FieldSchemaType): string; diff --git a/dist/lib/schema/build-foreign-key-constraint.js b/dist/lib/schema/build-foreign-key-constraint.js new file mode 100644 index 0000000..0749c98 --- /dev/null +++ b/dist/lib/schema/build-foreign-key-constraint.js @@ -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; +} diff --git a/dist/lib/schema/build-table-options.d.ts b/dist/lib/schema/build-table-options.d.ts new file mode 100644 index 0000000..d940cb0 --- /dev/null +++ b/dist/lib/schema/build-table-options.d.ts @@ -0,0 +1,2 @@ +import type { BUN_MARIADB_TableSchemaType } from "../../types"; +export default function buildTableOptions(table: BUN_MARIADB_TableSchemaType): string; diff --git a/dist/lib/schema/build-table-options.js b/dist/lib/schema/build-table-options.js new file mode 100644 index 0000000..af3af79 --- /dev/null +++ b/dist/lib/schema/build-table-options.js @@ -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(" ")}`; +} diff --git a/dist/lib/schema/create-db-manager-table.d.ts b/dist/lib/schema/create-db-manager-table.d.ts new file mode 100644 index 0000000..3bb5742 --- /dev/null +++ b/dist/lib/schema/create-db-manager-table.d.ts @@ -0,0 +1,2 @@ +import type { CreateDBSchemaParams } from "../../types"; +export default function createDBManagerTable({ db_schema, config, }: CreateDBSchemaParams): Promise; diff --git a/dist/lib/schema/create-db-manager-table.js b/dist/lib/schema/create-db-manager-table.js new file mode 100644 index 0000000..68862ed --- /dev/null +++ b/dist/lib/schema/create-db-manager-table.js @@ -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, + }); +} diff --git a/dist/lib/schema/create-db-schema.d.ts b/dist/lib/schema/create-db-schema.d.ts new file mode 100644 index 0000000..6afee76 --- /dev/null +++ b/dist/lib/schema/create-db-schema.d.ts @@ -0,0 +1,2 @@ +import type { CreateDBSchemaParams } from "../../types"; +export default function createDBSchema(params: CreateDBSchemaParams): Promise; diff --git a/dist/lib/schema/create-db-schema.js b/dist/lib/schema/create-db-schema.js new file mode 100644 index 0000000..9274dd7 --- /dev/null +++ b/dist/lib/schema/create-db-schema.js @@ -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 }); +} diff --git a/dist/lib/schema/create-table.d.ts b/dist/lib/schema/create-table.d.ts new file mode 100644 index 0000000..7246832 --- /dev/null +++ b/dist/lib/schema/create-table.d.ts @@ -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; diff --git a/dist/lib/schema/create-table.js b/dist/lib/schema/create-table.js new file mode 100644 index 0000000..6ede61e --- /dev/null +++ b/dist/lib/schema/create-table.js @@ -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 }); +} diff --git a/dist/lib/schema/get-existing-tables-from-tables-manager-table.d.ts b/dist/lib/schema/get-existing-tables-from-tables-manager-table.d.ts new file mode 100644 index 0000000..26304e1 --- /dev/null +++ b/dist/lib/schema/get-existing-tables-from-tables-manager-table.d.ts @@ -0,0 +1,2 @@ +import type { CreateDBSchemaParams } from "../../types"; +export default function getExistingTablesFromTablesManagerTable({ config, }: CreateDBSchemaParams): Promise<(string | undefined)[]>; diff --git a/dist/lib/schema/get-existing-tables-from-tables-manager-table.js b/dist/lib/schema/get-existing-tables-from-tables-manager-table.js new file mode 100644 index 0000000..07854eb --- /dev/null +++ b/dist/lib/schema/get-existing-tables-from-tables-manager-table.js @@ -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) || []; +} diff --git a/dist/lib/schema/get-table-columns.d.ts b/dist/lib/schema/get-table-columns.d.ts new file mode 100644 index 0000000..971b65a --- /dev/null +++ b/dist/lib/schema/get-table-columns.d.ts @@ -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; diff --git a/dist/lib/schema/get-table-columns.js b/dist/lib/schema/get-table-columns.js new file mode 100644 index 0000000..7fc28b0 --- /dev/null +++ b/dist/lib/schema/get-table-columns.js @@ -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, + })); +} diff --git a/dist/lib/schema/handle-db-schema-table.d.ts b/dist/lib/schema/handle-db-schema-table.d.ts new file mode 100644 index 0000000..5802f0c --- /dev/null +++ b/dist/lib/schema/handle-db-schema-table.d.ts @@ -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; diff --git a/dist/lib/schema/handle-db-schema-table.js b/dist/lib/schema/handle-db-schema-table.js new file mode 100644 index 0000000..71792ee --- /dev/null +++ b/dist/lib/schema/handle-db-schema-table.js @@ -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 }); +} diff --git a/dist/lib/schema/handle-db-schema-tables.d.ts b/dist/lib/schema/handle-db-schema-tables.d.ts new file mode 100644 index 0000000..2325c4f --- /dev/null +++ b/dist/lib/schema/handle-db-schema-tables.d.ts @@ -0,0 +1,2 @@ +import type { CreateDBSchemaParams } from "../../types"; +export default function handleDBSchemaTables(params: CreateDBSchemaParams): Promise; diff --git a/dist/lib/schema/handle-db-schema-tables.js b/dist/lib/schema/handle-db-schema-tables.js new file mode 100644 index 0000000..da6f5f3 --- /dev/null +++ b/dist/lib/schema/handle-db-schema-tables.js @@ -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 }); + } +} diff --git a/dist/lib/schema/is-vector-field.d.ts b/dist/lib/schema/is-vector-field.d.ts new file mode 100644 index 0000000..8459be1 --- /dev/null +++ b/dist/lib/schema/is-vector-field.d.ts @@ -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; diff --git a/dist/lib/schema/is-vector-field.js b/dist/lib/schema/is-vector-field.js new file mode 100644 index 0000000..f4d06a9 --- /dev/null +++ b/dist/lib/schema/is-vector-field.js @@ -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"; +} diff --git a/dist/lib/schema/map-data-types.d.ts b/dist/lib/schema/map-data-types.d.ts new file mode 100644 index 0000000..2c4705d --- /dev/null +++ b/dist/lib/schema/map-data-types.d.ts @@ -0,0 +1,2 @@ +import type { BUN_MARIADB_FieldSchemaType } from "../../types"; +export default function mapDataType(field: BUN_MARIADB_FieldSchemaType): string; diff --git a/dist/lib/schema/map-data-types.js b/dist/lib/schema/map-data-types.js new file mode 100644 index 0000000..10e6af5 --- /dev/null +++ b/dist/lib/schema/map-data-types.js @@ -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"; + } +} diff --git a/dist/lib/schema/mariadb-quote-gen.d.ts b/dist/lib/schema/mariadb-quote-gen.d.ts new file mode 100644 index 0000000..a12be9d --- /dev/null +++ b/dist/lib/schema/mariadb-quote-gen.d.ts @@ -0,0 +1 @@ +export default function MariaDBQuoteGen(str: string): string; diff --git a/dist/lib/schema/mariadb-quote-gen.js b/dist/lib/schema/mariadb-quote-gen.js new file mode 100644 index 0000000..cfbd780 --- /dev/null +++ b/dist/lib/schema/mariadb-quote-gen.js @@ -0,0 +1,3 @@ +export default function MariaDBQuoteGen(str) { + return `\`${str.replace(/`/g, "``")}\``; +} diff --git a/dist/lib/schema/order-db-schema.d.ts b/dist/lib/schema/order-db-schema.d.ts new file mode 100644 index 0000000..52a43c0 --- /dev/null +++ b/dist/lib/schema/order-db-schema.d.ts @@ -0,0 +1,2 @@ +import type { BUN_MARIADB_DatabaseSchemaType, CreateDBSchemaParams } from "../../types"; +export default function orderDBSchema({ db_schema, }: CreateDBSchemaParams): Promise; diff --git a/dist/lib/schema/order-db-schema.js b/dist/lib/schema/order-db-schema.js new file mode 100644 index 0000000..1555a8d --- /dev/null +++ b/dist/lib/schema/order-db-schema.js @@ -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; +} diff --git a/dist/lib/schema/recreate-table.d.ts b/dist/lib/schema/recreate-table.d.ts new file mode 100644 index 0000000..d411755 --- /dev/null +++ b/dist/lib/schema/recreate-table.d.ts @@ -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; diff --git a/dist/lib/schema/recreate-table.js b/dist/lib/schema/recreate-table.js new file mode 100644 index 0000000..183ecc9 --- /dev/null +++ b/dist/lib/schema/recreate-table.js @@ -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 }); + } +} diff --git a/dist/lib/schema/resolve-table.d.ts b/dist/lib/schema/resolve-table.d.ts new file mode 100644 index 0000000..7aa8da1 --- /dev/null +++ b/dist/lib/schema/resolve-table.d.ts @@ -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; diff --git a/dist/lib/schema/resolve-table.js b/dist/lib/schema/resolve-table.js new file mode 100644 index 0000000..f0f9235 --- /dev/null +++ b/dist/lib/schema/resolve-table.js @@ -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 || []), + ], + }; +} diff --git a/dist/lib/schema/run-schema-query.d.ts b/dist/lib/schema/run-schema-query.d.ts new file mode 100644 index 0000000..ec18e5c --- /dev/null +++ b/dist/lib/schema/run-schema-query.d.ts @@ -0,0 +1,11 @@ +import type { BunMariaDBConfig } from "../../types"; +export default function runSchemaQuery({ query, values, config, }: { + query: string; + values?: any[]; + config?: BunMariaDBConfig; +}): Promise; +export declare function querySchemaRows>({ query, values, config, }: { + query: string; + values?: any[]; + config?: BunMariaDBConfig; +}): Promise; diff --git a/dist/lib/schema/run-schema-query.js b/dist/lib/schema/run-schema-query.js new file mode 100644 index 0000000..7073374 --- /dev/null +++ b/dist/lib/schema/run-schema-query.js @@ -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 || []); +} diff --git a/dist/lib/schema/schema-condition.d.ts b/dist/lib/schema/schema-condition.d.ts new file mode 100644 index 0000000..ceeb658 --- /dev/null +++ b/dist/lib/schema/schema-condition.d.ts @@ -0,0 +1,5 @@ +import type { BunMariaDBConfig } from "../../types"; +export default function schemaCondition(config?: BunMariaDBConfig): { + where: string; + values: string[]; +}; diff --git a/dist/lib/schema/schema-condition.js b/dist/lib/schema/schema-condition.js new file mode 100644 index 0000000..570ae72 --- /dev/null +++ b/dist/lib/schema/schema-condition.js @@ -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: [], + }; +} diff --git a/dist/lib/schema/sync-indexes.d.ts b/dist/lib/schema/sync-indexes.d.ts new file mode 100644 index 0000000..8d5e65c --- /dev/null +++ b/dist/lib/schema/sync-indexes.d.ts @@ -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; diff --git a/dist/lib/schema/sync-indexes.js b/dist/lib/schema/sync-indexes.js new file mode 100644 index 0000000..2326bd8 --- /dev/null +++ b/dist/lib/schema/sync-indexes.js @@ -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, + }); + } + } + } +} diff --git a/dist/lib/schema/update-table.d.ts b/dist/lib/schema/update-table.d.ts new file mode 100644 index 0000000..435ce86 --- /dev/null +++ b/dist/lib/schema/update-table.d.ts @@ -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; diff --git a/dist/lib/schema/update-table.js b/dist/lib/schema/update-table.js new file mode 100644 index 0000000..27f035a --- /dev/null +++ b/dist/lib/schema/update-table.js @@ -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 }); + } +} diff --git a/dist/lib/schema/upsert-db-manager-table.d.ts b/dist/lib/schema/upsert-db-manager-table.d.ts new file mode 100644 index 0000000..ecb5b31 --- /dev/null +++ b/dist/lib/schema/upsert-db-manager-table.d.ts @@ -0,0 +1,9 @@ +import type { BunMariaDBConfig } from "../../types"; +export default function upsertDbManagerTable({ tableName, config, }: { + tableName: string; + config?: BunMariaDBConfig; +}): Promise; +export declare function removeDbManagerTable({ tableName, config, }: { + tableName: string; + config?: BunMariaDBConfig; +}): Promise; diff --git a/dist/lib/schema/upsert-db-manager-table.js b/dist/lib/schema/upsert-db-manager-table.js new file mode 100644 index 0000000..80fa9a3 --- /dev/null +++ b/dist/lib/schema/upsert-db-manager-table.js @@ -0,0 +1,18 @@ +import { AppData } from "../../data/app-data"; +import MariaDBQuoteGen from "./mariadb-quote-gen"; +import runSchemaQuery from "./run-schema-query"; +export default async function upsertDbManagerTable({ tableName, config, }) { + const now = Date.now(); + await runSchemaQuery({ + query: `INSERT INTO ${MariaDBQuoteGen(AppData["DbSchemaManagerTableName"])} (table_name, created_at, updated_at) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE updated_at = VALUES(updated_at)`, + values: [tableName, now, now], + config, + }); +} +export async function removeDbManagerTable({ tableName, config, }) { + await runSchemaQuery({ + query: `DELETE FROM ${MariaDBQuoteGen(AppData["DbSchemaManagerTableName"])} WHERE table_name = ?`, + values: [tableName], + config, + }); +} diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts new file mode 100644 index 0000000..b9c72a5 --- /dev/null +++ b/dist/types/index.d.ts @@ -0,0 +1,1440 @@ +import type { RequestOptions } from "https"; +/** + * Fully-qualified database name used when a database needs to be referenced + * across local and remote operations. + */ +export type BUN_MARIADB_DatabaseFullName = string; +/** + * User fields that should be omitted from general-purpose payloads and public + * responses. + */ +export declare const UsersOmitedFields: readonly ["password", "social_id", "verification_status", "date_created", "date_created_code", "date_created_timestamp", "date_updated", "date_updated_code", "date_updated_timestamp"]; +/** + * Describes an entire database schema, including its tables and clone/child + * database metadata. + */ +export interface BUN_MARIADB_DatabaseSchemaType { + tables: BUN_MARIADB_TableSchemaType[]; + collation?: MariaDbCollationsType; +} +/** + * Minimal reference to a child database linked to a parent database schema. + */ +export interface BUN_MARIADB_ChildrenDatabaseObject { + dbId?: string | number; +} +/** + * Supported MariaDB collations that can be applied at the database or table + * level. + */ +export declare const MariaDBCollations: readonly ["utf8mb4_bin", "utf8mb4_unicode_520_ci"]; +export type MariaDbCollationsType = (typeof MariaDBCollations)[number]; +export declare const MariaDBCharsets: readonly ["utf8mb4"]; +export type MariaDbCharsetType = (typeof MariaDBCharsets)[number]; +/** + * Describes a single table within a database schema, including its fields, + * indexes, unique constraints, and parent/child table relationships. + */ +export interface BUN_MARIADB_TableSchemaType { + id?: string | number; + tableName: string; + tableDescription?: string; + fields: BUN_MARIADB_FieldSchemaType[]; + indexes?: BUN_MARIADB_IndexSchemaType[]; + uniqueConstraints?: BUN_MARIADB_UniqueConstraintSchemaType[]; + childrenTables?: BUN_MARIADB_ChildrenTablesType[]; + /** + * Whether this is a child table + */ + childTable?: boolean; + updateData?: boolean; + /** + * ID of the parent table + */ + childTableId?: string | number; + /** + * ID of the parent table + */ + parentTableId?: string | number; + /** + * ID of the Database of parent table + */ + parentTableDbId?: string | number; + /** + * Name of the Database of parent table + */ + parentTableDbName?: string; + /** + * Name of the parent table + */ + parentTableName?: string; + tableNameOld?: string; + /** + * ID of the Database of parent table + */ + childTableDbId?: string | number; + collation?: (typeof MariaDBCollations)[number]; + /** + * If this is a vector-oriented table (native MariaDB VECTOR columns/indexes) + */ + isVector?: boolean; +} +/** + * Reference object used to link a table to one of its child tables. + */ +export interface BUN_MARIADB_ChildrenTablesType { + tableId?: string | number; + dbId?: string | number; +} +/** + * Supported editor/content modes for text fields. + */ +export declare const TextFieldTypesArray: readonly [{ + readonly title: "Plain Text"; + readonly value: "plain"; +}, { + readonly title: "Markdown"; + readonly value: "markdown"; +}, { + readonly title: "JSON"; + readonly value: "json"; +}, { + readonly title: "YAML"; + readonly value: "yaml"; +}, { + readonly title: "HTML"; + readonly value: "html"; +}, { + readonly title: "CSS"; + readonly value: "css"; +}, { + readonly title: "Javascript"; + readonly value: "javascript"; +}, { + readonly title: "Shell"; + readonly value: "shell"; +}, { + readonly title: "Code"; + readonly value: "code"; +}]; +/** + * Native MariaDB column types supported by the schema builder. + */ +export declare const BUN_MARIADB_DATATYPES: readonly [{ + readonly value: "CHAR"; +}, { + readonly value: "VARCHAR"; +}, { + readonly value: "TEXT"; +}, { + readonly value: "TINYTEXT"; +}, { + readonly value: "MEDIUMTEXT"; +}, { + readonly value: "LONGTEXT"; +}, { + readonly value: "TINYINT"; +}, { + readonly value: "SMALLINT"; +}, { + readonly value: "MEDIUMINT"; +}, { + readonly value: "INT"; +}, { + readonly value: "BIGINT"; +}, { + readonly value: "FLOAT"; +}, { + readonly value: "DOUBLE"; +}, { + readonly value: "DECIMAL"; +}, { + readonly value: "BINARY"; +}, { + readonly value: "VARBINARY"; +}, { + readonly value: "BLOB"; +}, { + readonly value: "TINYBLOB"; +}, { + readonly value: "MEDIUMBLOB"; +}, { + readonly value: "LONGBLOB"; +}, { + readonly value: "DATE"; +}, { + readonly value: "TIME"; +}, { + readonly value: "DATETIME"; +}, { + readonly value: "TIMESTAMP"; +}, { + readonly value: "YEAR"; +}, { + readonly value: "BOOLEAN"; +}, { + readonly value: "UUID"; +}, { + readonly value: "JSON"; +}, { + readonly value: "INET6"; +}, { + readonly value: "ENUM"; +}, { + readonly value: "SET"; +}, { + readonly value: "VECTOR"; +}]; +/** + * Describes a table column, including SQL constraints, text editor options, + * vector metadata, and foreign key configuration. + */ +export type BUN_MARIADB_FieldSchemaType = { + id?: number | string; + fieldName?: string; + fieldDescription?: string; + originName?: string; + dataType: (typeof BUN_MARIADB_DATATYPES)[number]["value"]; + nullValue?: boolean; + notNullValue?: boolean; + primaryKey?: boolean; + encrypted?: boolean; + autoIncrement?: boolean; + defaultValue?: string | number; + defaultValueLiteral?: string; + foreignKey?: BUN_MARIADB_ForeignKeyType; + defaultField?: boolean; + plainText?: boolean; + unique?: boolean; + pattern?: string; + patternFlags?: string; + onUpdate?: string; + onUpdateLiteral?: string; + onDelete?: string; + onDeleteLiteral?: string; + cssFiles?: string[]; + integerLength?: string | number; + decimals?: string | number; + code?: boolean; + options?: (string | number)[]; + isVector?: boolean; + vectorSize?: number; +} & { + [key in (typeof TextFieldTypesArray)[number]["value"]]?: boolean; +}; +/** + * Defines a foreign key relationship from one field to another table column. + */ +export interface BUN_MARIADB_ForeignKeyType { + foreignKeyName?: string; + destinationTableName?: string; + destinationTableColumnName?: string; + destinationTableColumnType?: string; + cascadeDelete?: boolean; + cascadeUpdate?: boolean; +} +export declare const MariaDBIndexTypes: readonly ["BTREE", "HASH", "FULLTEXT", "SPATIAL", "VECTOR"]; +export type MariaDBIndexType = (typeof MariaDBIndexTypes)[number]; +/** + * Describes a table index and the fields it covers. + */ +export interface BUN_MARIADB_IndexSchemaType { + /** + * Name of the index as it would appear on schema. Eg. + * `idx_user_id_index` + */ + indexName?: string; + /** + * The columns included in the index. + */ + indexTableFields?: string[]; + /** + * Under the hood index type (BTREE, HASH) or modifier (FULLTEXT, SPATIAL, VECTOR) + */ + indexType?: MariaDBIndexType; + /** + * Distance metric for VECTOR indexes (`euclidean` | `cosine`). Defaults to euclidean. + */ + vectorDistanceMetric?: "euclidean" | "cosine"; + /** + * Optional documentation or tuning note inside the DB metadata + */ + comment?: string; +} +/** + * Describes a multi-field uniqueness rule for a table. + */ +export interface BUN_MARIADB_UniqueConstraintSchemaType { + id?: string | number; + constraintName?: string; + alias?: string; + constraintTableFields?: BUN_MARIADB_UniqueConstraintFieldType[]; +} +/** + * Single field reference inside a unique constraint definition. + */ +export interface BUN_MARIADB_UniqueConstraintFieldType { + value: string; +} +/** + * Field metadata used when generating SQL for indexes. + */ +export interface BUN_MARIADB_IndexTableFieldType { + value: string; + dataType: string; +} +/** + * Result shape returned by MySQL `SHOW INDEXES` queries. + */ +export interface BUN_MARIADB_MYSQL_SHOW_INDEXES_Type { + Key_name: string; + Table: string; + Column_name: string; + Collation: string; + Index_type: string; + Cardinality: string; + Index_comment: string; + Comment: string; +} +/** + * Result shape returned by MySQL `SHOW COLUMNS` queries. + */ +export interface BUN_MARIADB_MYSQL_SHOW_COLUMNS_Type { + Field: string; + Type: string; + Null: string; + Key: string; + Default: string; + Extra: string; +} +/** + * Result shape returned by MariaDB `SHOW INDEXES` queries. + */ +export interface BUN_MARIADB_MARIADB_SHOW_INDEXES_TYPE { + Table: string; + Non_unique: 0 | 1; + Key_name: string; + Seq_in_index: number; + Column_name: string; + Collation: string; + Cardinality: number; + Sub_part?: string; + Packed?: string; + Index_type?: "BTREE"; + Comment?: string; + Index_comment?: string; + Ignored?: "YES" | "NO"; +} +/** + * Minimal metadata returned when listing MySQL foreign keys. + */ +export interface BUN_MARIADB_MYSQL_FOREIGN_KEYS_Type { + CONSTRAINT_NAME: string; + CONSTRAINT_SCHEMA: string; + TABLE_NAME: string; +} +/** + * Database record describing a user-owned database and optional remote sync + * metadata. + */ +export interface BUN_MARIADB_MYSQL_user_databases_Type { + id: number; + user_id: number; + db_full_name: string; + db_name: string; + db_slug: string; + db_image: string; + db_description: string; + active_clone: number; + active_data: 0 | 1; + active_clone_parent_db: string; + remote_connected?: number; + remote_db_full_name?: string; + remote_connection_host?: string; + remote_connection_key?: string; + remote_connection_type?: string; + user_priviledge?: string; + date_created?: string; + image_thumbnail?: string; + first_name?: string; + last_name?: string; + email?: string; +} +/** + * Return value used when converting an uploaded image file to base64. + */ +export type ImageInputFileToBase64FunctionReturn = { + imageBase64?: string; + imageBase64Full?: string; + imageName?: string; + imageSize?: number; +}; +/** + * Querystring parameters accepted by generic GET data endpoints. + */ +export interface GetReqQueryObject { + db: string; + query: string; + queryValues?: string; + tableName?: string; + debug?: boolean; +} +/** + * Canonical logged-in user shape shared across auth, session, and API flows. + */ +export type DATASQUIREL_LoggedInUser = { + id: number; + uuid?: string; + first_name: string; + last_name: string; + email: string; + phone?: string; + user_type?: string; + username?: string; + image?: string; + image_thumbnail?: string; + social_login?: number; + social_platform?: string; + social_id?: string; + verification_status?: number; + csrf_k: string; + logged_in_status: boolean; + date: number; +} & { + [key: string]: any; +}; +/** + * Standard authenticated-user response wrapper. + */ +export interface AuthenticatedUser { + success: boolean; + payload: DATASQUIREL_LoggedInUser | null; + msg?: string; + userId?: number; + cookieNames?: any; +} +/** + * Minimal successful user payload returned by user creation flows. + */ +export interface SuccessUserObject { + id: number; + first_name: string; + last_name: string; + email: string; +} +/** + * Return type for helpers that create a user record. + */ +export interface AddUserFunctionReturn { + success: boolean; + payload?: SuccessUserObject | null; + msg?: string; + sqlResult?: any; +} +/** + * Shape exposed by the Google Identity prompt lifecycle callback. + */ +export interface GoogleIdentityPromptNotification { + getMomentType: () => string; + getDismissedReason: () => string; + getNotDisplayedReason: () => string; + getSkippedReason: () => string; + isDismissedMoment: () => boolean; + isDisplayMoment: () => boolean; + isDisplayed: () => boolean; + isNotDisplayed: () => boolean; + isSkippedMoment: () => boolean; +} +/** + * User data accepted by registration and profile-creation flows. + */ +export type UserDataPayload = { + first_name: string; + last_name: string; + email: string; + password?: string; + username?: string; +} & { + [key: string]: any; +}; +/** + * Return shape for helpers that fetch a single user. + */ +export interface GetUserFunctionReturn { + success: boolean; + payload: { + id: number; + first_name: string; + last_name: string; + username: string; + email: string; + phone: string; + social_id: [string]; + image: string; + image_thumbnail: string; + verification_status: [number]; + } | null; +} +/** + * Return type for reauthentication flows that refresh auth state. + */ +export interface ReauthUserFunctionReturn { + success: boolean; + payload: DATASQUIREL_LoggedInUser | null; + msg?: string; + userId?: number; + token?: string; +} +/** + * Return type for user update helpers. + */ +export interface UpdateUserFunctionReturn { + success: boolean; + payload?: Object[] | string; +} +/** + * Generic success/error wrapper for read operations. + */ +export interface GetReturn { + success: boolean; + payload?: R; + msg?: string; + error?: string; + schema?: BUN_MARIADB_TableSchemaType; + finalQuery?: string; +} +/** + * Query parameters used when requesting schema metadata. + */ +export interface GetSchemaRequestQuery { + database?: string; + table?: string; + field?: string; + user_id?: string | number; + env?: { + [k: string]: string; + }; +} +/** + * API credential payload used when a schema request must be authenticated. + */ +export interface GetSchemaAPICredentialsParam { + key: string; +} +/** + * Complete request object for authenticated schema fetches. + */ +export type GetSchemaAPIParam = GetSchemaRequestQuery & GetSchemaAPICredentialsParam; +/** + * Generic response wrapper for write operations. + */ +export interface PostReturn { + success: boolean; + payload?: Object[] | string | PostInsertReturn; + msg?: string; + error?: any; + schema?: BUN_MARIADB_TableSchemaType; +} +/** + * High-level CRUD payload accepted by server-side data mutation handlers. + */ +export interface PostDataPayload { + action: "insert" | "update" | "delete"; + table: string; + data?: object; + identifierColumnName?: string; + identifierValue?: string; + duplicateColumnName?: string; + duplicateColumnValue?: string; + update?: boolean; +} +/** + * Local-only variant of `PostReturn` used by in-process operations. + */ +export interface LocalPostReturn { + success: boolean; + payload?: any; + msg?: string; + error?: string; +} +/** + * Query object for local write operations that may accept raw SQL or a CRUD + * payload. + */ +export interface LocalPostQueryObject { + query: string | PostDataPayload; + tableName?: string; + queryValues?: string[]; +} +/** + * Insert/update metadata returned by SQL drivers after a write completes. + */ +export interface PostInsertReturn { + fieldCount?: number; + affectedRows?: number; + insertId?: number; + serverStatus?: number; + warningCount?: number; + message?: string; + protocol41?: boolean; + changedRows?: number; + error?: string; +} +/** + * Extended user object used within the application runtime. + */ +export type UserType = DATASQUIREL_LoggedInUser & { + isSuperUser?: boolean; + staticHost?: string; + appHost?: string; + appName?: string; +}; +/** + * Stored API key definition and its associated metadata. + */ +export interface ApiKeyDef { + name: string; + scope: string; + date_created: string; + apiKeyPayload: string; +} +/** + * Aggregate dashboard metrics shown in admin and overview screens. + */ +export interface MetricsType { + dbCount: number; + tablesCount: number; + mediaCount: number; + apiKeysCount: number; +} +/** + * Shape of the internal MariaDB users table. + */ +export interface MYSQL_mariadb_users_table_def { + id?: number; + user_id?: number; + username?: string; + host?: string; + password?: string; + primary?: number; + grants?: string; + date_created?: string; + date_created_code?: number; + date_created_timestamp?: string; + date_updated?: string; + date_updated_code?: number; + date_updated_timestamp?: string; +} +/** + * Credentials used to connect to a MariaDB instance as a specific user. + */ +export interface MariaDBUserCredType { + mariadb_user?: string; + mariadb_host?: string; + mariadb_pass?: string; +} +/** + * Supported logical operators for server-side query generation. + */ +export declare const ServerQueryOperators: readonly ["AND", "OR"]; +/** + * Supported comparison operators for server-side query generation. + */ +export declare const ServerQueryEqualities: readonly ["EQUAL", "LIKE", "LIKE_RAW", "LIKE_LOWER", "LIKE_LOWER_RAW", "NOT LIKE", "NOT LIKE_RAW", "NOT_LIKE_LOWER", "NOT_LIKE_LOWER_RAW", "NOT EQUAL", "REGEXP", "FULLTEXT", "IN", "NOT IN", "BETWEEN", "NOT BETWEEN", "IS NULL", "IS NOT NULL", "IS NOT", "EXISTS", "NOT EXISTS", "GREATER THAN", "GREATER THAN OR EQUAL", "LESS THAN", "LESS THAN OR EQUAL", "MATCH", "MATCH_BOOLEAN"]; +/** + * Top-level query-builder input used to generate SELECT statements, joins, + * grouping, pagination, and full-text search clauses. + */ +export type ServerQueryParam = { + selectFields?: (keyof T | TableSelectFieldsObject)[]; + omitFields?: (keyof T)[]; + query?: ServerQueryQueryObject; + limit?: number; + page?: number; + offset?: number; + order?: ServerQueryParamOrder | ServerQueryParamOrder[]; + searchOperator?: (typeof ServerQueryOperators)[number]; + searchEquality?: (typeof ServerQueryEqualities)[number]; + addUserId?: { + fieldName: keyof T; + }; + join?: (ServerQueryParamsJoin | ServerQueryParamsJoin[] | undefined)[]; + group?: keyof T | ServerQueryParamGroupBy | (keyof T | ServerQueryParamGroupBy)[]; + countSubQueries?: ServerQueryParamsCount[]; + fullTextSearch?: ServerQueryParamFullTextSearch; + /** + * Raw SQL to use as select + */ + [key: string]: any; +}; +/** + * Represents a single `GROUP BY` field, optionally qualified by table name. + */ +export type ServerQueryParamGroupBy = { + field: keyof T; + table?: string; +}; +/** + * Represents a single `ORDER BY` clause. + */ +export type ServerQueryParamOrder = { + field: keyof T; + strategy: "ASC" | "DESC"; +}; +/** + * Configuration for a full-text search query and the alias used for the score + * column. + */ +export type ServerQueryParamFullTextSearch = { + fields: (keyof T)[]; + searchTerm: string; + /** Field Name to user to Rank the Score of Search Results */ + scoreAlias: string; +}; +/** + * Describes a count subquery that is projected into the main SELECT result. + */ +export type ServerQueryParamsCount = { + table: string; + /** Alias for the Table From which the count is fetched */ + table_alias?: string; + srcTrgMap: { + src: string; + trg: string | ServerQueryParamsCountSrcTrgMap; + }[]; + alias: string; +}; +/** + * Source/target mapping used inside count subqueries. + */ +export type ServerQueryParamsCountSrcTrgMap = { + table: string; + field: string; +}; +/** + * Declares a selected field and an optional alias for the result set. + */ +export type TableSelectFieldsObject = { + fieldName: keyof T; + alias?: string; + count?: { + alias?: string; + }; + sum?: boolean; + max?: boolean; + min?: boolean; + average?: boolean; + group_concat?: Omit; + distinct?: boolean; +}; +export type TableSelectFieldsBasicDirective = { + alias: string; +}; +/** + * Value wrapper used when a query condition needs per-value metadata such as a + * custom equality or explicit table/field names. + */ +export type ServerQueryValuesObject = { + value?: string | number; + /** + * Defaults to EQUAL + */ + equality?: (typeof ServerQueryEqualities)[number]; + tableName?: string; + fieldName?: string; +}; +/** + * All value shapes accepted by a query condition, including arrays used by + * operators such as `IN` and `BETWEEN`. + */ +export type ServerQueryObjectValue = string | number | ServerQueryValuesObject | undefined | null | (string | number | ServerQueryValuesObject | undefined | null)[]; +/** + * Describes a single query condition and any nested subconditions for a field. + */ +export type ServerQueryObject = SQLComparisonsParams & { + value?: ServerQueryObjectValue; + nullValue?: boolean; + notNullValue?: boolean; + operator?: (typeof ServerQueryOperators)[number]; + equality?: (typeof ServerQueryEqualities)[number]; + tableName?: K; + /** + * This will replace the top level field name if + * provided + */ + fieldName?: string; + __query?: { + [key: string]: Omit, "__query">; + }; + vector?: boolean; + /** + * ### The Function to be used to generate the vector. + * Eg. `vec_f32`. This will come out as `vec_f32(?)` + * instead of just `?` + */ + vectorFunction?: string; +}; +/** + * Field-to-condition map for server-side query generation. + */ +export type ServerQueryQueryObject = { + [key in keyof T]: ServerQueryObject; +}; +/** + * Parameters for authenticated fetch helpers used by the frontend and internal + * SDK layers. + */ +export type FetchDataParams = { + path: string; + method?: (typeof DataCrudRequestMethods)[number]; + body?: object | string; + query?: AuthFetchQuery; + tableName?: string; +}; +/** + * Query object accepted by authenticated data-fetch helpers. + */ +export type AuthFetchQuery = ServerQueryParam & { + [key: string]: any; +}; +/** + * Join clause definition used by the server query builder. + */ +export type ServerQueryParamsJoin = { + joinType: "INNER JOIN" | "JOIN" | "LEFT JOIN" | "RIGHT JOIN"; + alias?: string; + tableName: Table; + match?: ServerQueryParamsJoinMatchObject | ServerQueryParamsJoinMatchObject[]; + selectFields?: (keyof Field | SelectFieldObject)[]; + omitFields?: (keyof Field | { + field: keyof Field; + alias?: string; + count?: boolean; + })[]; + operator?: (typeof ServerQueryOperators)[number]; + /** + * Raw SQL to use as join select + */ + /** + * Concatenate multiple matches from another table + */ + group_concat?: GroupConcatObject; +}; +export type GroupConcatObject = { + field: string; + alias: string; + /** + * Separator. Default `,` + */ + separator?: string; + distinct?: boolean; +}; +export type SelectFieldObject = { + field: keyof Field; + alias?: string; + count?: boolean; + sum?: boolean; + max?: boolean; + min?: boolean; + average?: boolean; + group_concat?: Pick; + distinct?: boolean; +}; +export declare const SQlComparisons: readonly [">", "<>", "<", "=", ">=", "<=", "!=", "IS NOT", "IS", "IS NULL", "IS NOT NULL", "IN", "NOT IN", "LIKE", "NOT LIKE", "GLOB", "NOT GLOB"]; +export type SQLBetween = { + min: SQLInsertGenValueType; + max: SQLInsertGenValueType; +}; +export type SQLComparisonsParams = { + raw_equality?: (typeof SQlComparisons)[number]; + between?: SQLBetween; + not_between?: SQLBetween; +}; +/** + * Defines how a root-table field maps to a join-table field in an `ON` clause. + */ +export type ServerQueryParamsJoinMatchObject = SQLComparisonsParams & { + /** Field name from the **Root Table** */ + source?: string | ServerQueryParamsJoinMatchSourceTargetObject; + /** Field name from the **Join Table** */ + target?: keyof Field | ServerQueryParamsJoinMatchSourceTargetObject; + /** A literal value: No source and target Needed! */ + targetLiteral?: string | number; + __batch?: { + matches: Omit, "__batch">[]; + operator: "AND" | "OR"; + }; +}; +/** + * Explicit table/field reference for join match definitions. + */ +export type ServerQueryParamsJoinMatchSourceTargetObject = { + tableName: string; + fieldName: string; +}; +/** + * Payload used when pushing or pulling a schema to or from a remote API. + */ +export type ApiConnectBody = { + url: string; + key: string; + database: BUN_MARIADB_MYSQL_user_databases_Type; + dbSchema: BUN_MARIADB_DatabaseSchemaType; + type: "pull" | "push"; + user_id?: string | number; +}; +/** + * Superuser credentials and auth state. + */ +export type SuUserType = { + email: string; + password: string; + authKey: string; + logged_in_status: boolean; + date: number; +}; +/** + * Configuration for a remote MariaDB host in replicated or load-balanced + * setups. + */ +export type MariadbRemoteServerObject = { + host: string; + port: number; + primary?: boolean; + loadBalanced?: boolean; + users?: MariadbRemoteServerUserObject[]; +}; +/** + * Credentials for a user provisioned on a remote MariaDB server. + */ +export type MariadbRemoteServerUserObject = { + name: string; + password: string; + host: string; +}; +/** + * Standard login response returned by API authentication helpers. + */ +export type APILoginFunctionReturn = { + success: boolean; + msg?: string; + payload?: DATASQUIREL_LoggedInUser | null; + userId?: number | string; + key?: string; + token?: string; + csrf?: string; + cookieNames?: any; +}; +/** + * Parameters required to create a user through the public API layer. + */ +export type APICreateUserFunctionParams = { + encryptionKey?: string; + payload: any; + database: string; + dsqlUserID?: string | number; + verify?: boolean; +}; +/** + * Function signature for API user-creation handlers. + */ +export type APICreateUserFunction = (params: APICreateUserFunctionParams) => Promise; +/** + * Result returned when reconciling a social login with the local user store. + */ +export type HandleSocialDbFunctionReturn = { + success: boolean; + user?: DATASQUIREL_LoggedInUser | null; + msg?: string; + social_id?: string | number; + social_platform?: string; + payload?: any; + alert?: boolean; + newUser?: any; + error?: any; +} | null; +/** + * Cookie definition used when setting auth/session cookies. + */ +export type CookieObject = { + name: string; + value: string; + domain?: string; + path?: string; + expires?: Date; + maxAge?: number; + secure?: boolean; + httpOnly?: boolean; + sameSite?: "Strict" | "Lax" | "None"; + priority?: "Low" | "Medium" | "High"; +}; +/** + * Request options accepted by the generic HTTP client helper. + */ +export type HttpRequestParams = RequestOptions & { + scheme?: "http" | "https"; + body?: ReqObj; + query?: ReqObj; + urlEncodedFormBody?: boolean; +}; +/** + * Function signature for the generic HTTP request helper. + */ +export type HttpRequestFunction = (param: HttpRequestParams) => Promise>; +/** + * Normalized response returned by the generic HTTP request helper. + */ +export type HttpFunctionResponse = { + status: number; + data?: ResObj; + error?: string; + str?: string; + requestedPath?: string; +}; +/** + * GET request payload for API data reads. + */ +export type ApiGetQueryObject = { + query: ServerQueryParam; + table: string; + dbFullName?: string; +}; +/** + * Uppercase HTTP methods supported by the CRUD helpers. + */ +export declare const DataCrudRequestMethods: readonly ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]; +/** + * Lowercase variant of the supported HTTP methods, used where string casing + * must match external APIs. + */ +export declare const DataCrudRequestMethodsLowerCase: readonly ["get", "post", "put", "patch", "delete", "options"]; +/** + * Runtime parameters passed to generic CRUD handlers. + */ +export type DsqlMethodCrudParam = { + method: (typeof DataCrudRequestMethods)[number]; + body?: T; + query?: DsqlCrudQueryObject; + tableName: string; + addUser?: { + field: keyof T; + }; + user?: DATASQUIREL_LoggedInUser; + extraData?: T; + transformData?: DsqlCrudTransformDataFunction; + transformQuery?: DsqlCrudTransformQueryFunction; + existingData?: T; + targetId?: string | number; + sanitize?: ({ data, batchData }: { + data?: T; + batchData?: T[]; + }) => T | T[]; + debug?: boolean; +}; +/** + * Hook used to mutate input data before a CRUD action is executed. + */ +export type DsqlCrudTransformDataFunction = (params: { + data: T; + user?: DATASQUIREL_LoggedInUser; + existingData?: T; + reqMethod: (typeof DataCrudRequestMethods)[number]; +}) => Promise; +/** + * Hook used to mutate query input before a CRUD action is executed. + */ +export type DsqlCrudTransformQueryFunction = (params: { + query: DsqlCrudQueryObject; + user?: DATASQUIREL_LoggedInUser; + reqMethod: (typeof DataCrudRequestMethods)[number]; +}) => Promise>; +/** + * High-level CRUD actions supported by the DSQL helpers. + */ +export declare const DsqlCrudActions: readonly ["insert", "update", "delete", "get"]; +/** + * Query object used by CRUD helpers, built on top of the server query builder. + */ +export type DsqlCrudQueryObject = ServerQueryParam & { + query?: ServerQueryQueryObject; +}; +/** + * Parameters used to generate a SQL `DELETE` statement. + */ +export type SQLDeleteGeneratorParams = { + tableName: string; + deleteKeyValues?: SQLDeleteData[]; + deleteKeyValuesOperator?: "AND" | "OR"; + dbFullName?: string; + data?: any; +}; +/** + * Single key/value predicate used by the delete SQL generator. + */ +export type SQLDeleteData = { + key: keyof T; + value: string | number | null | undefined; + operator?: (typeof ServerQueryEqualities)[number]; +}; +/** + * Prebuilt SQL where-clause fragment and its bound parameters. + */ +export type DsqlCrudParamWhereClause = { + clause: string; + params?: string[]; +}; +/** + * Callback signature used when surfacing internal errors to callers. + */ +export type ErrorCallback = (title: string, error: Error, data?: any) => void; +/** + * Reserved query parameter names used throughout the API layer. + */ +export declare const QueryFields: readonly ["duplicate", "user_id", "delegated_user_id", "db_id", "table_id", "db_slug"]; +/** + * Minimal representation of a local folder entry. + */ +export type LocalFolderType = { + name: string; + isPrivate: boolean; +}; +/** + * SQL string and bound parameters produced during query generation. + */ +export type ResponseQueryObject = { + sql?: string; + params?: (string | number)[]; +}; +/** + * # Docker Compose Types + */ +export type DockerCompose = { + services: DockerComposeServicesType; + networks: DockerComposeNetworks; + name: string; +}; +/** + * Supported Docker Compose service names used by the deployment helpers. + */ +export declare const DockerComposeServices: readonly ["setup", "cron", "reverse-proxy", "webapp", "websocket", "static", "db", "maxscale", "post-db-setup", "web-app-post-db-setup", "post-replica-db-setup", "db-replica-1", "db-replica-2", "db-cron", "web-app-post-db-setup"]; +/** + * Map of compose service names to service definitions. + */ +export type DockerComposeServicesType = { + [key in (typeof DockerComposeServices)[number]]: DockerComposeServiceWithBuildObject; +}; +/** + * Docker Compose network definitions. + */ +export type DockerComposeNetworks = { + [k: string]: { + driver?: "bridge"; + ipam?: { + config: DockerComposeNetworkConfigObject[]; + }; + external?: boolean; + }; +}; +/** + * Static network config for a Docker Compose network. + */ +export type DockerComposeNetworkConfigObject = { + subnet: string; + gateway: string; +}; +/** + * Service definition for compose services that are built from source. + */ +export type DockerComposeServiceWithBuildObject = { + build: DockerComposeServicesBuildObject; + env_file: string; + container_name: string; + hostname: string; + volumes: string[]; + environment: string[]; + ports?: string[]; + networks?: DockerComposeServiceNetworkObject; + restart?: string; + depends_on?: { + [k: string]: { + condition: string; + }; + }; + user?: string; +}; +/** + * Service definition for compose services that use a prebuilt image. + */ +export type DockerComposeServiceWithImage = Omit & { + image: string; +}; +/** + * Build instructions for a Docker Compose service. + */ +export type DockerComposeServicesBuildObject = { + context: string; + dockerfile: string; +}; +/** + * Per-network addressing information for a Docker Compose service. + */ +export type DockerComposeServiceNetworkObject = { + [k: string]: { + ipv4_address: string; + }; +}; +/** + * Metadata recorded for a table cloned from another database. + */ +export type ClonedTableInfo = { + dbId?: string | number; + tableId?: string | number; + keepUpdated?: boolean; + keepDataUpdated?: boolean; +}; +/** + * Common columns present on default/generated table entries. + */ +export type DefaultEntryType = { + id?: number; + uuid?: string; + date_created?: string; + date_created_code?: number; + date_created_timestamp?: string; + date_updated?: string; + date_updated_code?: number; + date_updated_timestamp?: string; +} & { + [k: string]: string | number | null; +}; +/** + * Supported index strategies for generated schemas. + */ +export declare const IndexTypes: readonly ["regular", "full_text", "vector"]; +/** + * Structured error payload captured alongside generated SQL. + */ +export type BunMariaDBErrorObject = { + sql?: string; + sqlValues?: any[]; + error?: string; +}; +/** + * Output of the SQL insert generator. + */ +export interface SQLInsertGenReturn { + query: string; + values: SQLInsertGenValueType[]; +} +/** + * Values accepted by the SQL insert generator, including vector buffers. + */ +export type SQLInsertGenValueType = string | number | Float32Array | Buffer | null; +/** + * Callback variant used when a generated insert value needs a custom + * placeholder. + */ +export type SQLInsertGenDataFn = () => { + placeholder: string; + value: SQLInsertGenValueType; +}; +/** + * Record shape accepted by the SQL insert generator. + */ +export type SQLInsertGenDataType = { + [k: string]: SQLInsertGenValueType | SQLInsertGenDataFn | undefined | null; +}; +/** + * Parameters used to build a multi-row SQL insert statement. + */ +export type SQLInsertGenParams = { + data: SQLInsertGenDataType[]; + tableName: string; + dbFullName?: string; +}; +/** + * Library configuration used to locate the MariaDB database, schema file, + * generated types, and backup settings. + */ +export type BunMariaDBConfig = { + db_name: string; + /** + * The Directory for backups. Relative to db_dir. + */ + db_backup_dir?: string; + max_backups?: number; + /** + * Max export archives to keep in `{db_dir}/.exports` (default: 10) + */ + max_exports?: number; + /** + * Root directory for schema, types, and local artifacts (relative to project root) + */ + db_dir?: string; + /** + * The File Path relative to the root(working) directory for the type + * definition export. Example `db_types.ts` or `types/db_types.ts` + */ + typedef_file_path?: string; + /** + * Configuration for the MariaDB Connection + */ + db_config?: Bun.SQL.Options; + /** + * Database charset. Defaults to `utf8mb4` + */ + charset?: MariaDbCharsetType; + /** + * Database Connection timeout. Defaults to `10000` + */ + connection_timeout?: number; + /** + * File path to the SSL certificate + */ + ssl_ca?: string; + /** + * Extra HTML sanitization allowlists appended to the defaults + * when inserting/updating fields marked `html: true`. + */ + html_sanitize?: { + /** + * Additional tags to allow (merged with built-in defaults). + */ + allowed_tags?: string[]; + /** + * Additional attributes to allow per tag (merged with built-in defaults). + * Values for each tag are appended to any existing allowed attributes. + */ + allowed_attributes?: Record; + }; +}; +/** + * Resolved Bun MariaDB config paired with the loaded database schema. + */ +export type BunMariaDBConfigReturn = { + config: BunMariaDBConfig; + dbSchema: BUN_MARIADB_DatabaseSchemaType; +}; +/** + * Default fields automatically suggested for new tables. + */ +export declare const DefaultFields: BUN_MARIADB_FieldSchemaType[]; +export type BunMariaDBQueryFieldValues = { + field: F; + table?: T; +}; +export type QueryRawValueType = string | number | null | undefined; +export type DBResponseObject = { + success: boolean; + payload?: T[]; + single_res?: T; + insert_return?: DBInsertReturn; + error?: any; + msg?: string; + debug?: any; + count?: number; +}; +export type DBInsertReturn = { + count?: number; + last_insert_id?: number; + affected_rows?: number; +}; +export declare const RequiredENVs: readonly ["BUN_MARIADB_SERVER_HOST", "BUN_MARIADB_SERVER_PORT", "BUN_MARIADB_SERVER_USERNAME", "BUN_MARIADB_SERVER_PASSWORD", "BUN_MARIADB_SERVER_SSL_KEY_PATH"]; +export type CreateDBSchemaParams = { + db_schema: BUN_MARIADB_DatabaseSchemaType; + config?: BunMariaDBConfig; +}; +export type CreateDBSchemaTableHandlerParams = CreateDBSchemaParams & { + table: BUN_MARIADB_TableSchemaType; + db_manager_table_name?: string; + existing_live_table?: BUN_MARIADB_INFORMATION_SCHEMA_TABLES; +}; +export type BUN_MARIADB_DB_TABLE_MANAGER_TABLE = { + table_name?: string; + created_at?: number; + updated_at?: number; +}; +export type BUN_MARIADB_INFORMATION_SCHEMA_TABLES = { + TABLE_NAME?: string; + TABLE_TYPE?: "BASE TABLE"; + ENGINE?: "InnoDB"; + VERSION?: number; +}; diff --git a/dist/types/index.js b/dist/types/index.js new file mode 100644 index 0000000..e5b858f --- /dev/null +++ b/dist/types/index.js @@ -0,0 +1,235 @@ +/** + * User fields that should be omitted from general-purpose payloads and public + * responses. + */ +export const UsersOmitedFields = [ + "password", + "social_id", + "verification_status", + "date_created", + "date_created_code", + "date_created_timestamp", + "date_updated", + "date_updated_code", + "date_updated_timestamp", +]; +/** + * Supported MariaDB collations that can be applied at the database or table + * level. + */ +export const MariaDBCollations = [ + "utf8mb4_bin", + "utf8mb4_unicode_520_ci", +]; +export const MariaDBCharsets = ["utf8mb4"]; +/** + * Supported editor/content modes for text fields. + */ +export const TextFieldTypesArray = [ + { title: "Plain Text", value: "plain" }, + { title: "Markdown", value: "markdown" }, + { title: "JSON", value: "json" }, + { title: "YAML", value: "yaml" }, + { title: "HTML", value: "html" }, + { title: "CSS", value: "css" }, + { title: "Javascript", value: "javascript" }, + { title: "Shell", value: "shell" }, + { title: "Code", value: "code" }, +]; +/** + * Native MariaDB column types supported by the schema builder. + */ +export const BUN_MARIADB_DATATYPES = [ + // Strings & Text + { value: "CHAR" }, + { value: "VARCHAR" }, + { value: "TEXT" }, + { value: "TINYTEXT" }, + { value: "MEDIUMTEXT" }, + { value: "LONGTEXT" }, + // Numeric Integers + { value: "TINYINT" }, + { value: "SMALLINT" }, + { value: "MEDIUMINT" }, + { value: "INT" }, + { value: "BIGINT" }, + // Numeric Floats & Exact + { value: "FLOAT" }, + { value: "DOUBLE" }, + { value: "DECIMAL" }, + // Binary Layouts + { value: "BINARY" }, + { value: "VARBINARY" }, + { value: "BLOB" }, + { value: "TINYBLOB" }, + { value: "MEDIUMBLOB" }, + { value: "LONGBLOB" }, + // Temporal (Dates / Times) + { value: "DATE" }, + { value: "TIME" }, + { value: "DATETIME" }, + { value: "TIMESTAMP" }, + { value: "YEAR" }, + // Core Schema Primitives + { value: "BOOLEAN" }, + { value: "UUID" }, + { value: "JSON" }, + { value: "INET6" }, + { value: "ENUM" }, + { value: "SET" }, + { value: "VECTOR" }, +]; +export const MariaDBIndexTypes = [ + "BTREE", + "HASH", + "FULLTEXT", + "SPATIAL", + "VECTOR", +]; +/** + * Supported logical operators for server-side query generation. + */ +export const ServerQueryOperators = ["AND", "OR"]; +/** + * Supported comparison operators for server-side query generation. + */ +export const ServerQueryEqualities = [ + "EQUAL", + "LIKE", + "LIKE_RAW", + "LIKE_LOWER", + "LIKE_LOWER_RAW", + "NOT LIKE", + "NOT LIKE_RAW", + "NOT_LIKE_LOWER", + "NOT_LIKE_LOWER_RAW", + "NOT EQUAL", + "REGEXP", + "FULLTEXT", + "IN", + "NOT IN", + "BETWEEN", + "NOT BETWEEN", + "IS NULL", + "IS NOT NULL", + "IS NOT", + "EXISTS", + "NOT EXISTS", + "GREATER THAN", + "GREATER THAN OR EQUAL", + "LESS THAN", + "LESS THAN OR EQUAL", + "MATCH", + "MATCH_BOOLEAN", +]; +export const SQlComparisons = [ + ">", + "<>", + "<", + "=", + ">=", + "<=", + "!=", + "IS NOT", + "IS", + "IS NULL", + "IS NOT NULL", + "IN", + "NOT IN", + "LIKE", + "NOT LIKE", + "GLOB", + "NOT GLOB", +]; +/** + * Uppercase HTTP methods supported by the CRUD helpers. + */ +export const DataCrudRequestMethods = [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE", + "OPTIONS", +]; +/** + * Lowercase variant of the supported HTTP methods, used where string casing + * must match external APIs. + */ +export const DataCrudRequestMethodsLowerCase = [ + "get", + "post", + "put", + "patch", + "delete", + "options", +]; +/** + * High-level CRUD actions supported by the DSQL helpers. + */ +export const DsqlCrudActions = ["insert", "update", "delete", "get"]; +/** + * Reserved query parameter names used throughout the API layer. + */ +export const QueryFields = [ + "duplicate", + "user_id", + "delegated_user_id", + "db_id", + "table_id", + "db_slug", +]; +/** + * Supported Docker Compose service names used by the deployment helpers. + */ +export const DockerComposeServices = [ + "setup", + "cron", + "reverse-proxy", + "webapp", + "websocket", + "static", + "db", + "maxscale", + "post-db-setup", + "web-app-post-db-setup", + "post-replica-db-setup", + "db-replica-1", + "db-replica-2", + "db-cron", + "web-app-post-db-setup", +]; +/** + * Supported index strategies for generated schemas. + */ +export const IndexTypes = ["regular", "full_text", "vector"]; +/** + * Default fields automatically suggested for new tables. + */ +export const DefaultFields = [ + { + fieldName: "id", + dataType: "BIGINT", + primaryKey: true, + autoIncrement: true, + notNullValue: true, + fieldDescription: "The unique identifier of the record.", + }, + { + fieldName: "created_at", + dataType: "BIGINT", + fieldDescription: "The time when the record was created. (Unix Timestamp)", + }, + { + fieldName: "updated_at", + dataType: "BIGINT", + fieldDescription: "The time when the record was updated. (Unix Timestamp)", + }, +]; +export const RequiredENVs = [ + "BUN_MARIADB_SERVER_HOST", + "BUN_MARIADB_SERVER_PORT", + "BUN_MARIADB_SERVER_USERNAME", + "BUN_MARIADB_SERVER_PASSWORD", + "BUN_MARIADB_SERVER_SSL_KEY_PATH", +]; diff --git a/dist/utils/append-default-fields-to-db-schema.d.ts b/dist/utils/append-default-fields-to-db-schema.d.ts new file mode 100644 index 0000000..9d031fc --- /dev/null +++ b/dist/utils/append-default-fields-to-db-schema.d.ts @@ -0,0 +1,6 @@ +import { type BUN_MARIADB_DatabaseSchemaType } from "../types"; +type Params = { + dbSchema: BUN_MARIADB_DatabaseSchemaType; +}; +export default function ({ dbSchema }: Params): BUN_MARIADB_DatabaseSchemaType; +export {}; diff --git a/dist/utils/append-default-fields-to-db-schema.js b/dist/utils/append-default-fields-to-db-schema.js new file mode 100644 index 0000000..d1b758a --- /dev/null +++ b/dist/utils/append-default-fields-to-db-schema.js @@ -0,0 +1,12 @@ +import _ from "lodash"; +import { DefaultFields } from "../types"; +export default function ({ dbSchema }) { + const finaldbSchema = _.cloneDeep(dbSchema); + finaldbSchema.tables = finaldbSchema.tables.map((t) => { + const newTable = _.cloneDeep(t); + newTable.fields = newTable.fields.filter((f) => !f.fieldName?.match(/^(id|created_at|updated_at)$/)); + newTable.fields.unshift(...DefaultFields); + return newTable; + }); + return finaldbSchema; +} diff --git a/dist/utils/export-archive.d.ts b/dist/utils/export-archive.d.ts new file mode 100644 index 0000000..b794574 --- /dev/null +++ b/dist/utils/export-archive.d.ts @@ -0,0 +1,21 @@ +export declare const ExportArchiveMembers: { + readonly SqlFileName: "dump.sql"; + readonly SchemaFileName: "schema.ts"; +}; +export type ExportArchiveContents = { + sql: string; + schemaTs: string; +}; +export declare function isArchivePath(filePath: string): boolean; +export declare function isSqlPath(filePath: string): boolean; +/** + * Create a portable export archive (tar / tar.gz / zip) with dump.sql + schema.ts. + */ +export declare function writeExportArchive({ contents, outPath, }: { + contents: ExportArchiveContents; + outPath: string; +}): Promise; +/** + * Read a portable export archive (tar / tar.gz / zip). + */ +export declare function readExportArchive(archivePath: string): Promise; diff --git a/dist/utils/export-archive.js b/dist/utils/export-archive.js new file mode 100644 index 0000000..9668843 --- /dev/null +++ b/dist/utils/export-archive.js @@ -0,0 +1,159 @@ +import fs from "fs"; +import path from "path"; +import { AppData } from "../data/app-data"; +import grabDirNames from "../data/grab-dir-names"; +export const ExportArchiveMembers = { + SqlFileName: "dump.sql", + SchemaFileName: AppData.DbSchemaFileName, +}; +const ARCHIVE_EXTENSIONS = [".tar.gz", ".tgz", ".tar", ".zip"]; +export function isArchivePath(filePath) { + const lower = filePath.toLowerCase(); + return ARCHIVE_EXTENSIONS.some((ext) => lower.endsWith(ext)); +} +export function isSqlPath(filePath) { + return filePath.toLowerCase().endsWith(".sql"); +} +/** + * Create a portable export archive (tar / tar.gz / zip) with dump.sql + schema.ts. + */ +export async function writeExportArchive({ contents, outPath, }) { + const lower = outPath.toLowerCase(); + if (lower.endsWith(".zip")) { + await writeZipArchive({ contents, outPath }); + return; + } + const members = { + [ExportArchiveMembers.SqlFileName]: contents.sql, + [ExportArchiveMembers.SchemaFileName]: contents.schemaTs, + }; + const gzip = lower.endsWith(".gz") || lower.endsWith(".tgz"); + if (gzip) { + await Bun.Archive.write(outPath, members, { compress: "gzip" }); + } + else { + await Bun.Archive.write(outPath, members); + } +} +/** + * Read a portable export archive (tar / tar.gz / zip). + */ +export async function readExportArchive(archivePath) { + const lower = archivePath.toLowerCase(); + if (lower.endsWith(".zip")) { + return readZipArchive(archivePath); + } + const bytes = await Bun.file(archivePath).bytes(); + const archive = new Bun.Archive(bytes); + const files = await archive.files(); + const sql = (await readArchiveMember(files, ExportArchiveMembers.SqlFileName)) ?? + (await readFirstMatching(files, (name) => name.endsWith(".sql"))); + const schemaTs = (await readArchiveMember(files, ExportArchiveMembers.SchemaFileName)) ?? + (await readFirstMatching(files, (name) => name.endsWith("schema.ts"))); + if (!sql) { + throw new Error(`Archive is missing SQL dump (expected \`${ExportArchiveMembers.SqlFileName}\`)`); + } + if (!schemaTs) { + throw new Error(`Archive is missing schema TypeScript (expected \`${ExportArchiveMembers.SchemaFileName}\`)`); + } + return { sql, schemaTs }; +} +async function readArchiveMember(files, name) { + // Exact match, or basename match for nested paths + for (const [entry, file] of files) { + if (entry === name || path.basename(entry) === name) { + return await file.text(); + } + } + return null; +} +async function readFirstMatching(files, predicate) { + for (const [entry, file] of files) { + if (predicate(entry) || predicate(path.basename(entry))) { + return await file.text(); + } + } + return null; +} +async function writeZipArchive({ contents, outPath, }) { + const { BUN_MARIADB_TEMP_DIR } = grabDirNames(); + const tempDir = path.join(BUN_MARIADB_TEMP_DIR, `export-${Date.now()}-${Math.random().toString(36).slice(2)}`); + fs.mkdirSync(tempDir, { recursive: true }); + try { + const sqlPath = path.join(tempDir, ExportArchiveMembers.SqlFileName); + const schemaPath = path.join(tempDir, ExportArchiveMembers.SchemaFileName); + fs.writeFileSync(sqlPath, contents.sql, "utf-8"); + fs.writeFileSync(schemaPath, contents.schemaTs, "utf-8"); + const absOut = path.resolve(outPath); + const proc = Bun.spawn([ + "zip", + "-q", + "-j", + absOut, + ExportArchiveMembers.SqlFileName, + ExportArchiveMembers.SchemaFileName, + ], { + cwd: tempDir, + stdout: "pipe", + stderr: "pipe", + }); + const [stderr, exitCode] = await Promise.all([ + new Response(proc.stderr).text(), + proc.exited, + ]); + if (exitCode !== 0) { + throw new Error(`zip failed (exit ${exitCode}): ${stderr || "unknown error"}. Ensure \`zip\` is installed, or use .tar.gz.`); + } + } + finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } +} +async function readZipArchive(archivePath) { + const { BUN_MARIADB_TEMP_DIR } = grabDirNames(); + const tempDir = path.join(BUN_MARIADB_TEMP_DIR, `import-${Date.now()}-${Math.random().toString(36).slice(2)}`); + fs.mkdirSync(tempDir, { recursive: true }); + try { + const absArchive = path.resolve(archivePath); + const proc = Bun.spawn(["unzip", "-q", "-o", absArchive, "-d", tempDir], { + stdout: "pipe", + stderr: "pipe", + }); + const [stderr, exitCode] = await Promise.all([ + new Response(proc.stderr).text(), + proc.exited, + ]); + if (exitCode !== 0) { + throw new Error(`unzip failed (exit ${exitCode}): ${stderr || "unknown error"}. Ensure \`unzip\` is installed.`); + } + const sql = findFileContents(tempDir, (name) => name === ExportArchiveMembers.SqlFileName || name.endsWith(".sql")); + const schemaTs = findFileContents(tempDir, (name) => name === ExportArchiveMembers.SchemaFileName || + name.endsWith("schema.ts")); + if (!sql) { + throw new Error(`Archive is missing SQL dump (expected \`${ExportArchiveMembers.SqlFileName}\`)`); + } + if (!schemaTs) { + throw new Error(`Archive is missing schema TypeScript (expected \`${ExportArchiveMembers.SchemaFileName}\`)`); + } + return { sql, schemaTs }; + } + finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } +} +function findFileContents(dir, predicate) { + const stack = [dir]; + while (stack.length) { + const current = stack.pop(); + for (const entry of fs.readdirSync(current, { withFileTypes: true })) { + const full = path.join(current, entry.name); + if (entry.isDirectory()) { + stack.push(full); + } + else if (predicate(entry.name)) { + return fs.readFileSync(full, "utf-8"); + } + } + } + return null; +} diff --git a/dist/utils/grab-backup-data.d.ts b/dist/utils/grab-backup-data.d.ts new file mode 100644 index 0000000..5e9a727 --- /dev/null +++ b/dist/utils/grab-backup-data.d.ts @@ -0,0 +1,12 @@ +type Params = { + backup_name: string; +}; +/** + * Parse timestamped backup/export names: `{db_name}-{timestamp}[.sql|.tar.gz|...]` + */ +export default function grabBackupData({ backup_name }: Params): { + backup_date: Date; + backup_date_timestamp: number; + origin_backup_name: string; +}; +export {}; diff --git a/dist/utils/grab-backup-data.js b/dist/utils/grab-backup-data.js new file mode 100644 index 0000000..b323f9b --- /dev/null +++ b/dist/utils/grab-backup-data.js @@ -0,0 +1,33 @@ +/** + * Strip known backup/export extensions from a file name. + */ +function stripBackupExtension(name) { + const lower = name.toLowerCase(); + if (lower.endsWith(".tar.gz")) { + return name.slice(0, -7); + } + if (lower.endsWith(".tgz")) { + return name.slice(0, -4); + } + if (lower.endsWith(".tar")) { + return name.slice(0, -4); + } + if (lower.endsWith(".zip")) { + return name.slice(0, -4); + } + if (lower.endsWith(".sql")) { + return name.slice(0, -4); + } + return name; +} +/** + * Parse timestamped backup/export names: `{db_name}-{timestamp}[.sql|.tar.gz|...]` + */ +export default function grabBackupData({ backup_name }) { + const normalized = stripBackupExtension(backup_name); + const backup_parts = normalized.split("-"); + const backup_date_timestamp = Number(backup_parts.pop()); + const origin_backup_name = backup_parts.join("-"); + const backup_date = new Date(backup_date_timestamp); + return { backup_date, backup_date_timestamp, origin_backup_name }; +} diff --git a/dist/utils/grab-db-backup-file-name.d.ts b/dist/utils/grab-db-backup-file-name.d.ts new file mode 100644 index 0000000..dfab4e1 --- /dev/null +++ b/dist/utils/grab-db-backup-file-name.d.ts @@ -0,0 +1,6 @@ +import type { BunMariaDBConfig } from "../types"; +type Params = { + config: BunMariaDBConfig; +}; +export default function grabDBBackupFileName({ config }: Params): string; +export {}; diff --git a/dist/utils/grab-db-backup-file-name.js b/dist/utils/grab-db-backup-file-name.js new file mode 100644 index 0000000..783fb56 --- /dev/null +++ b/dist/utils/grab-db-backup-file-name.js @@ -0,0 +1,3 @@ +export default function grabDBBackupFileName({ config }) { + return `${config.db_name}-${Date.now()}.sql`; +} diff --git a/dist/utils/grab-db-dir.d.ts b/dist/utils/grab-db-dir.d.ts new file mode 100644 index 0000000..2081a6c --- /dev/null +++ b/dist/utils/grab-db-dir.d.ts @@ -0,0 +1,10 @@ +import type { BunMariaDBConfig } from "../types"; +type Params = { + config: BunMariaDBConfig; +}; +export default function grabDBDir({ config }: Params): { + db_dir: string; + backup_dir: string; + export_dir: string; +}; +export {}; diff --git a/dist/utils/grab-db-dir.js b/dist/utils/grab-db-dir.js new file mode 100644 index 0000000..56e286d --- /dev/null +++ b/dist/utils/grab-db-dir.js @@ -0,0 +1,13 @@ +import path from "path"; +import grabDirNames from "../data/grab-dir-names"; +import { AppData } from "../data/app-data"; +export default function grabDBDir({ config }) { + const { ROOT_DIR } = grabDirNames(); + const db_dir = config.db_dir + ? path.resolve(ROOT_DIR, config.db_dir) + : ROOT_DIR; + const backup_dir_name = config.db_backup_dir || AppData["DefaultBackupDirName"]; + const backup_dir = path.resolve(db_dir, backup_dir_name); + const export_dir = path.resolve(db_dir, AppData["DefaultExportDirName"]); + return { db_dir, backup_dir, export_dir }; +} diff --git a/dist/utils/grab-db-schema.d.ts b/dist/utils/grab-db-schema.d.ts new file mode 100644 index 0000000..fe6cb52 --- /dev/null +++ b/dist/utils/grab-db-schema.d.ts @@ -0,0 +1 @@ +export default function grabDbSchema(): Promise; diff --git a/dist/utils/grab-db-schema.js b/dist/utils/grab-db-schema.js new file mode 100644 index 0000000..fd6e5aa --- /dev/null +++ b/dist/utils/grab-db-schema.js @@ -0,0 +1,5 @@ +export default async function grabDbSchema() { + const config = global.CONFIG; + const dbSchema = global.DB_SCHEMA; + return dbSchema; +} diff --git a/dist/utils/grab-join-fields-from-query-object.d.ts b/dist/utils/grab-join-fields-from-query-object.d.ts new file mode 100644 index 0000000..a1186c9 --- /dev/null +++ b/dist/utils/grab-join-fields-from-query-object.d.ts @@ -0,0 +1,7 @@ +import type { BunMariaDBQueryFieldValues, ServerQueryParam } from "../types"; +type Params = Record> = { + query: ServerQueryParam; + ignore_select_fields?: boolean; +}; +export default function grabJoinFieldsFromQueryObject = Record, F extends string = string, T extends string = string>({ query, ignore_select_fields, }: Params): BunMariaDBQueryFieldValues[]; +export {}; diff --git a/dist/utils/grab-join-fields-from-query-object.js b/dist/utils/grab-join-fields-from-query-object.js new file mode 100644 index 0000000..2672c70 --- /dev/null +++ b/dist/utils/grab-join-fields-from-query-object.js @@ -0,0 +1,55 @@ +import _ from "lodash"; +export default function grabJoinFieldsFromQueryObject({ query, ignore_select_fields, }) { + const fields_values = []; + const new_query = _.cloneDeep(query); + if (new_query.join) { + for (let i = 0; i < new_query.join.length; i++) { + const join = new_query.join[i]; + if (!join) + continue; + if (Array.isArray(join)) { + for (let i = 0; i < join.length; i++) { + const single_join = join[i]; + fields_values.push(...grabSingleJoinData({ + join: single_join, + ignore_select_fields, + })); + } + } + else { + fields_values.push(...grabSingleJoinData({ + join: join, + ignore_select_fields, + })); + } + } + } + return fields_values; +} +function grabSingleJoinData({ join, ignore_select_fields, }) { + let values = []; + const join_select_fields = join?.selectFields; + if (!join_select_fields?.[0] && !ignore_select_fields) { + throw new Error(`\`selectFields\` required in joins. To ignore this error, pass the \`ignore_select_fields\` parameter`); + } + if (join_select_fields?.[0]) { + for (let i = 0; i < join_select_fields.length; i++) { + const select_field = join_select_fields[i]; + if (select_field) { + values.push({ + table: join.tableName, + field: typeof select_field == "object" + ? String(select_field.field) + : String(select_field), + }); + } + } + } + if (join.group_concat) { + values.push({ + table: join.tableName, + field: join.group_concat.field, + }); + } + return values; +} diff --git a/dist/utils/grab-sorted-backups.d.ts b/dist/utils/grab-sorted-backups.d.ts new file mode 100644 index 0000000..5331719 --- /dev/null +++ b/dist/utils/grab-sorted-backups.d.ts @@ -0,0 +1,6 @@ +import type { BunMariaDBConfig } from "../types"; +type Params = { + config: BunMariaDBConfig; +}; +export default function grabSortedBackups({ config }: Params): string[]; +export {}; diff --git a/dist/utils/grab-sorted-backups.js b/dist/utils/grab-sorted-backups.js new file mode 100644 index 0000000..7460e5d --- /dev/null +++ b/dist/utils/grab-sorted-backups.js @@ -0,0 +1,32 @@ +import grabDBDir from "../utils/grab-db-dir"; +import fs from "fs"; +function stripBackupExtension(name) { + const lower = name.toLowerCase(); + if (lower.endsWith(".tar.gz")) + return name.slice(0, -7); + if (lower.endsWith(".tgz")) + return name.slice(0, -4); + if (lower.endsWith(".tar")) + return name.slice(0, -4); + if (lower.endsWith(".zip")) + return name.slice(0, -4); + if (lower.endsWith(".sql")) + return name.slice(0, -4); + return name; +} +function backupTimestamp(name) { + const base = stripBackupExtension(name); + const ts = Number(base.split("-").pop()); + return Number.isFinite(ts) ? ts : 0; +} +export default function grabSortedBackups({ config }) { + const { backup_dir } = grabDBDir({ config }); + if (!fs.existsSync(backup_dir)) { + return []; + } + const backups = fs.readdirSync(backup_dir); + /** + * Order Backups. Most recent first. + */ + return backups.sort((a, b) => backupTimestamp(b) - backupTimestamp(a)); +} diff --git a/dist/utils/grab-sorted-exports.d.ts b/dist/utils/grab-sorted-exports.d.ts new file mode 100644 index 0000000..c628cb4 --- /dev/null +++ b/dist/utils/grab-sorted-exports.d.ts @@ -0,0 +1,6 @@ +import type { BunMariaDBConfig } from "../types"; +type Params = { + config: BunMariaDBConfig; +}; +export default function grabSortedExports({ config }: Params): string[]; +export {}; diff --git a/dist/utils/grab-sorted-exports.js b/dist/utils/grab-sorted-exports.js new file mode 100644 index 0000000..b30700c --- /dev/null +++ b/dist/utils/grab-sorted-exports.js @@ -0,0 +1,27 @@ +import fs from "fs"; +import grabDBDir from "./grab-db-dir"; +function stripExportExtension(name) { + const lower = name.toLowerCase(); + if (lower.endsWith(".tar.gz")) + return name.slice(0, -7); + if (lower.endsWith(".tgz")) + return name.slice(0, -4); + if (lower.endsWith(".tar")) + return name.slice(0, -4); + if (lower.endsWith(".zip")) + return name.slice(0, -4); + return name; +} +function exportTimestamp(name) { + const base = stripExportExtension(name); + const ts = Number(base.split("-").pop()); + return Number.isFinite(ts) ? ts : 0; +} +export default function grabSortedExports({ config }) { + const { export_dir } = grabDBDir({ config }); + if (!fs.existsSync(export_dir)) { + return []; + } + const exports = fs.readdirSync(export_dir); + return exports.sort((a, b) => exportTimestamp(b) - exportTimestamp(a)); +} diff --git a/dist/utils/mariadb-cli-env.d.ts b/dist/utils/mariadb-cli-env.d.ts new file mode 100644 index 0000000..daa79ba --- /dev/null +++ b/dist/utils/mariadb-cli-env.d.ts @@ -0,0 +1,6 @@ +/** + * Build env for mariadb / mariadb-dump child processes without putting + * the password on the process argv (visible via `ps`). + */ +export default function mariadbCliEnv(): NodeJS.ProcessEnv; +export declare function mariadbCliConnectionArgs(): string[]; diff --git a/dist/utils/mariadb-cli-env.js b/dist/utils/mariadb-cli-env.js new file mode 100644 index 0000000..574701a --- /dev/null +++ b/dist/utils/mariadb-cli-env.js @@ -0,0 +1,24 @@ +/** + * Build env for mariadb / mariadb-dump child processes without putting + * the password on the process argv (visible via `ps`). + */ +export default function mariadbCliEnv() { + const env = { ...process.env }; + const password = process.env.BUN_MARIADB_SERVER_PASSWORD; + if (password) { + // Standard MySQL/MariaDB client env vars (prefer not using -p on argv) + env.MYSQL_PWD = password; + env.MARIADB_PWD = password; + } + return env; +} +export function mariadbCliConnectionArgs() { + const host = process.env.BUN_MARIADB_SERVER_HOST || "127.0.0.1"; + const user = process.env.BUN_MARIADB_SERVER_USERNAME || "root"; + const port = process.env.BUN_MARIADB_SERVER_PORT; + return [ + `-h${host}`, + `-u${user}`, + ...(port ? [`-P${port}`] : []), + ]; +} diff --git a/dist/utils/mariadb-dump-restore.d.ts b/dist/utils/mariadb-dump-restore.d.ts new file mode 100644 index 0000000..4cc7ced --- /dev/null +++ b/dist/utils/mariadb-dump-restore.d.ts @@ -0,0 +1,17 @@ +import type { BunMariaDBConfig } from "../types"; +/** + * Prefer mariadb-dump, fall back to mysqldump. + */ +export declare function resolveDumpBinary(): string; +/** + * Prefer mariadb client, fall back to mysql. + */ +export declare function resolveClientBinary(): string; +/** + * Dump the configured database to an SQL string. + */ +export declare function dumpDatabase(config: BunMariaDBConfig): Promise; +/** + * Restore an SQL dump into the configured database. + */ +export declare function restoreDatabase(config: BunMariaDBConfig, sql: string): Promise; diff --git a/dist/utils/mariadb-dump-restore.js b/dist/utils/mariadb-dump-restore.js new file mode 100644 index 0000000..a2ba04d --- /dev/null +++ b/dist/utils/mariadb-dump-restore.js @@ -0,0 +1,92 @@ +import mariadbCliEnv, { mariadbCliConnectionArgs } from "./mariadb-cli-env"; +/** + * Prefer mariadb-dump, fall back to mysqldump. + */ +export function resolveDumpBinary() { + const candidates = ["mariadb-dump", "mysqldump"]; + for (const bin of candidates) { + try { + const result = Bun.spawnSync(["which", bin], { + stdout: "pipe", + stderr: "pipe", + }); + if (result.exitCode === 0) { + return new TextDecoder().decode(result.stdout).trim() || bin; + } + } + catch { + // try next + } + } + return "mariadb-dump"; +} +/** + * Prefer mariadb client, fall back to mysql. + */ +export function resolveClientBinary() { + const candidates = ["mariadb", "mysql"]; + for (const bin of candidates) { + try { + const result = Bun.spawnSync(["which", bin], { + stdout: "pipe", + stderr: "pipe", + }); + if (result.exitCode === 0) { + return new TextDecoder().decode(result.stdout).trim() || bin; + } + } + catch { + // try next + } + } + return "mariadb"; +} +/** + * Dump the configured database to an SQL string. + */ +export async function dumpDatabase(config) { + const dumpBin = resolveDumpBinary(); + const args = [ + dumpBin, + ...mariadbCliConnectionArgs(), + "--single-transaction", + "--routines", + "--triggers", + "--events", + config.db_name, + ]; + const proc = Bun.spawn(args, { + stdout: "pipe", + stderr: "pipe", + env: mariadbCliEnv(), + }); + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]); + if (exitCode !== 0) { + throw new Error(`Dump failed (exit ${exitCode}): ${stderr || "unknown error"}. Ensure \`mariadb-dump\` or \`mysqldump\` is installed.`); + } + return stdout; +} +/** + * Restore an SQL dump into the configured database. + */ +export async function restoreDatabase(config, sql) { + const clientBin = resolveClientBinary(); + const args = [clientBin, ...mariadbCliConnectionArgs(), config.db_name]; + const proc = Bun.spawn(args, { + stdin: new Blob([sql]), + stdout: "pipe", + stderr: "pipe", + env: mariadbCliEnv(), + }); + const [stderr, exitCode] = await Promise.all([ + new Response(proc.stderr).text(), + proc.exited, + ]); + if (exitCode !== 0) { + throw new Error(`Restore failed (exit ${exitCode}): ${stderr || "unknown error"}. Ensure \`mariadb\` or \`mysql\` is installed.`); + } +} diff --git a/dist/utils/query-value-parser.d.ts b/dist/utils/query-value-parser.d.ts new file mode 100644 index 0000000..bf5460c --- /dev/null +++ b/dist/utils/query-value-parser.d.ts @@ -0,0 +1,6 @@ +import type { QueryRawValueType, ServerQueryObjectValue } from "../types"; +type Params = { + query_value: ServerQueryObjectValue; +}; +export default function queryValueParser({ query_value, }: Params): QueryRawValueType | QueryRawValueType[]; +export {}; diff --git a/dist/utils/query-value-parser.js b/dist/utils/query-value-parser.js new file mode 100644 index 0000000..dfa68ea --- /dev/null +++ b/dist/utils/query-value-parser.js @@ -0,0 +1,21 @@ +export default function queryValueParser({ query_value, }) { + if (typeof query_value == "string" || typeof query_value == "number") { + return query_value; + } + if (Array.isArray(query_value)) { + let values = []; + for (let i = 0; i < query_value.length; i++) { + const single_value = query_value[i]; + if (single_value) { + const single_parsed_value = queryValueParser({ + query_value: single_value, + }); + if (!Array.isArray(single_parsed_value)) { + values.push(single_parsed_value); + } + } + } + return values; + } + return query_value?.value; +} diff --git a/dist/utils/sanitize-html-fields.d.ts b/dist/utils/sanitize-html-fields.d.ts new file mode 100644 index 0000000..72eddb6 --- /dev/null +++ b/dist/utils/sanitize-html-fields.d.ts @@ -0,0 +1,18 @@ +import type { BunMariaDBConfig } from "../types"; +/** + * Sanitize string values ONLY for schema fields with explicit `html: true`. + * Other fields (including plain text that happens to contain HTML) are left untouched. + */ +export default function sanitizeHtmlFields = Record>({ table, data, config, }: { + table: string; + data: T; + config?: BunMariaDBConfig; +}): T; +/** + * Sanitize an array of row objects for insert. + */ +export declare function sanitizeHtmlFieldsBatch = Record>({ table, data, config, }: { + table: string; + data: T[]; + config?: BunMariaDBConfig; +}): T[]; diff --git a/dist/utils/sanitize-html-fields.js b/dist/utils/sanitize-html-fields.js new file mode 100644 index 0000000..18e11f3 --- /dev/null +++ b/dist/utils/sanitize-html-fields.js @@ -0,0 +1,54 @@ +import sanitizeHtml from "sanitize-html"; +import { readLiveSchema } from "../functions/live-schema"; +import getSanitizeHtmlOptions from "./sanitize-html-options"; +function grabTableFields(tableName) { + const dbSchema = global.DB_SCHEMA || readLiveSchema(); + const tableSchema = dbSchema?.tables?.find((t) => t.tableName === tableName); + return tableSchema?.fields || []; +} +/** + * Only fields with an explicit `html: true` flag are sanitized. + * Missing / falsy / non-true values are never sanitized. + */ +function isExplicitHtmlField(field) { + return field?.html === true; +} +function sanitizeValue(value, config) { + if (typeof value !== "string") { + return value; + } + return sanitizeHtml(value, getSanitizeHtmlOptions(config)); +} +/** + * Sanitize string values ONLY for schema fields with explicit `html: true`. + * Other fields (including plain text that happens to contain HTML) are left untouched. + */ +export default function sanitizeHtmlFields({ table, data, config, }) { + const fields = grabTableFields(table); + if (fields.length === 0) { + return data; + } + const htmlFieldNames = new Set(fields + .filter(isExplicitHtmlField) + .map((f) => f.fieldName) + .filter((name) => Boolean(name))); + // No explicitly marked html fields on this table — skip entirely + if (htmlFieldNames.size === 0) { + return data; + } + const sanitized = { ...data }; + const resolvedConfig = config || global.CONFIG; + for (const key of Object.keys(sanitized)) { + // Only sanitize keys that map to fields with html: true + if (!htmlFieldNames.has(key)) + continue; + sanitized[key] = sanitizeValue(sanitized[key], resolvedConfig); + } + return sanitized; +} +/** + * Sanitize an array of row objects for insert. + */ +export function sanitizeHtmlFieldsBatch({ table, data, config, }) { + return data.map((row) => sanitizeHtmlFields({ table, data: row, config })); +} diff --git a/dist/utils/sanitize-html-options.d.ts b/dist/utils/sanitize-html-options.d.ts new file mode 100644 index 0000000..6dc04eb --- /dev/null +++ b/dist/utils/sanitize-html-options.d.ts @@ -0,0 +1,7 @@ +import type { IOptions } from "sanitize-html"; +import type { BunMariaDBConfig } from "../types"; +export declare const defaultSanitizeHtmlOptions: IOptions; +/** + * Build sanitize-html options, appending any tags/attributes from config. + */ +export default function getSanitizeHtmlOptions(config?: BunMariaDBConfig): IOptions; diff --git a/dist/utils/sanitize-html-options.js b/dist/utils/sanitize-html-options.js new file mode 100644 index 0000000..2ba207b --- /dev/null +++ b/dist/utils/sanitize-html-options.js @@ -0,0 +1,72 @@ +export const defaultSanitizeHtmlOptions = { + allowedTags: [ + "b", + "i", + "em", + "strong", + "a", + "p", + "span", + "ul", + "ol", + "li", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "img", + "div", + "button", + "pre", + "code", + "br", + "hr", + "blockquote", + "table", + "tr", + "td", + "th", + "thead", + "tbody", + "tfoot", + "caption", + "colgroup", + "col", + ], + allowedAttributes: { + a: ["href", "title", "class", "style", "target", "rel"], + img: ["src", "alt", "width", "height", "class", "style"], + "*": ["style", "class", "title", "id"], + }, +}; +function uniqueStrings(values) { + return Array.from(new Set(values)); +} +/** + * Build sanitize-html options, appending any tags/attributes from config. + */ +export default function getSanitizeHtmlOptions(config) { + const cfg = config || global.CONFIG; + const extra = cfg?.html_sanitize; + const baseTags = defaultSanitizeHtmlOptions.allowedTags || []; + const baseAttrs = { + ...(defaultSanitizeHtmlOptions.allowedAttributes || {}), + }; + const allowedTags = uniqueStrings([ + ...(Array.isArray(baseTags) ? baseTags : []), + ...(extra?.allowed_tags || []), + ]); + const allowedAttributes = { ...baseAttrs }; + for (const [tag, attrs] of Object.entries(extra?.allowed_attributes || {})) { + allowedAttributes[tag] = uniqueStrings([ + ...(allowedAttributes[tag] || []), + ...attrs, + ]); + } + return { + allowedTags, + allowedAttributes, + }; +} diff --git a/dist/utils/sql-equality-parser.d.ts b/dist/utils/sql-equality-parser.d.ts new file mode 100644 index 0000000..cd5a253 --- /dev/null +++ b/dist/utils/sql-equality-parser.d.ts @@ -0,0 +1,2 @@ +import { ServerQueryEqualities } from "../types"; +export default function sqlEqualityParser(eq: (typeof ServerQueryEqualities)[number]): string; diff --git a/dist/utils/sql-equality-parser.js b/dist/utils/sql-equality-parser.js new file mode 100644 index 0000000..77dc7a9 --- /dev/null +++ b/dist/utils/sql-equality-parser.js @@ -0,0 +1,41 @@ +import { ServerQueryEqualities } from "../types"; +export default function sqlEqualityParser(eq) { + switch (eq) { + case "EQUAL": + return "="; + case "LIKE": + return "LIKE"; + case "NOT LIKE": + return "NOT LIKE"; + case "NOT EQUAL": + return "<>"; + case "IS NOT": + return "IS NOT"; + case "IN": + return "IN"; + case "NOT IN": + return "NOT IN"; + case "BETWEEN": + return "BETWEEN"; + case "NOT BETWEEN": + return "NOT BETWEEN"; + case "IS NULL": + return "IS NULL"; + case "IS NOT NULL": + return "IS NOT NULL"; + case "EXISTS": + return "EXISTS"; + case "NOT EXISTS": + return "NOT EXISTS"; + case "GREATER THAN": + return ">"; + case "GREATER THAN OR EQUAL": + return ">="; + case "LESS THAN": + return "<"; + case "LESS THAN OR EQUAL": + return "<="; + default: + return "="; + } +} diff --git a/dist/utils/sql-gen-operator-gen.d.ts b/dist/utils/sql-gen-operator-gen.d.ts new file mode 100644 index 0000000..50496ba --- /dev/null +++ b/dist/utils/sql-gen-operator-gen.d.ts @@ -0,0 +1,20 @@ +import type { ServerQueryEqualities, ServerQueryObject, SQLInsertGenValueType } from "../types"; +type Params = { + fieldName: string; + value?: SQLInsertGenValueType; + equality?: (typeof ServerQueryEqualities)[number]; + queryObj: ServerQueryObject<{ + [key: string]: any; + }, string>; + isValueFieldValue?: boolean; +}; +type Return = { + str?: string; + param?: SQLInsertGenValueType; +}; +/** + * # SQL Gen Operator Gen + * @description Maps query equality operators to MariaDB SQL fragments + */ +export default function sqlGenOperatorGen({ fieldName, value, equality, queryObj, isValueFieldValue, }: Params): Return; +export {}; diff --git a/dist/utils/sql-gen-operator-gen.js b/dist/utils/sql-gen-operator-gen.js new file mode 100644 index 0000000..26fefa1 --- /dev/null +++ b/dist/utils/sql-gen-operator-gen.js @@ -0,0 +1,133 @@ +import sqlEqualityParser from "./sql-equality-parser"; +/** + * # SQL Gen Operator Gen + * @description Maps query equality operators to MariaDB SQL fragments + */ +export default function sqlGenOperatorGen({ fieldName, value, equality, queryObj, isValueFieldValue, }) { + if (queryObj.nullValue) { + return { str: `${fieldName} IS NULL` }; + } + if (queryObj.notNullValue) { + return { str: `${fieldName} IS NOT NULL` }; + } + if (value) { + const finalValue = isValueFieldValue ? value : "?"; + const finalParams = isValueFieldValue ? undefined : value; + if (equality == "MATCH") { + return { + str: `MATCH(${fieldName}) AGAINST(${finalValue} IN NATURAL LANGUAGE MODE)`, + param: finalParams, + }; + } + else if (equality == "MATCH_BOOLEAN") { + return { + str: `MATCH(${fieldName}) AGAINST(${finalValue} IN BOOLEAN MODE)`, + param: finalParams, + }; + } + else if (equality == "LIKE_LOWER") { + return { + str: `LOWER(${fieldName}) LIKE LOWER(${finalValue})`, + param: `%${finalParams}%`, + }; + } + else if (equality == "LIKE_LOWER_RAW") { + return { + str: `LOWER(${fieldName}) LIKE LOWER(${finalValue})`, + param: finalParams, + }; + } + else if (equality == "LIKE") { + return { + str: `${fieldName} LIKE ${finalValue}`, + param: `%${finalParams}%`, + }; + } + else if (equality == "LIKE_RAW") { + return { + str: `${fieldName} LIKE ${finalValue}`, + param: finalParams, + }; + } + else if (equality == "NOT_LIKE_LOWER") { + return { + str: `LOWER(${fieldName}) NOT LIKE LOWER(${finalValue})`, + param: `%${finalParams}%`, + }; + } + else if (equality == "NOT_LIKE_LOWER_RAW") { + return { + str: `LOWER(${fieldName}) NOT LIKE LOWER(${finalValue})`, + param: finalParams, + }; + } + else if (equality == "NOT LIKE") { + return { + str: `${fieldName} NOT LIKE ${finalValue}`, + param: finalParams, + }; + } + else if (equality == "NOT LIKE_RAW") { + return { + str: `${fieldName} NOT LIKE ${finalValue}`, + param: finalParams, + }; + } + else if (equality == "REGEXP") { + return { + str: `LOWER(${fieldName}) REGEXP LOWER(${finalValue})`, + param: finalParams, + }; + } + else if (equality == "FULLTEXT") { + return { + str: `MATCH(${fieldName}) AGAINST(${finalValue} IN BOOLEAN MODE)`, + param: finalParams, + }; + } + else if (equality == "NOT EQUAL") { + return { + str: `${fieldName} != ${finalValue}`, + param: finalParams, + }; + } + else if (equality == "IS NOT") { + return { + str: `${fieldName} IS NOT ${finalValue}`, + param: finalParams, + }; + } + else if (equality) { + return { + str: `${fieldName} ${sqlEqualityParser(equality)} ${finalValue}`, + param: finalParams, + }; + } + else { + return { + str: `${fieldName} = ${finalValue}`, + param: finalParams, + }; + } + } + else { + if (equality == "IS NULL") { + return { str: `${fieldName} IS NULL` }; + } + else if (equality == "IS NOT NULL") { + return { str: `${fieldName} IS NOT NULL` }; + } + else if (equality) { + return { + str: `${fieldName} ${sqlEqualityParser(equality)} ?`, + param: value, + }; + } + else { + return { + str: `${fieldName} = ?`, + param: value, + }; + } + } +} diff --git a/dist/utils/sql-generator-gen-join-str.d.ts b/dist/utils/sql-generator-gen-join-str.d.ts new file mode 100644 index 0000000..5dbf8cb --- /dev/null +++ b/dist/utils/sql-generator-gen-join-str.d.ts @@ -0,0 +1,11 @@ +import type { ServerQueryParamsJoin, ServerQueryParamsJoinMatchObject, SQLInsertGenValueType } from "../types"; +type Param = { + mtch: ServerQueryParamsJoinMatchObject; + join: ServerQueryParamsJoin; + table_name: string; +}; +export default function sqlGenGenJoinStr({ join, mtch, table_name }: Param): { + str: string; + values: SQLInsertGenValueType[]; +}; +export {}; diff --git a/dist/utils/sql-generator-gen-join-str.js b/dist/utils/sql-generator-gen-join-str.js new file mode 100644 index 0000000..0120afa --- /dev/null +++ b/dist/utils/sql-generator-gen-join-str.js @@ -0,0 +1,65 @@ +export default function sqlGenGenJoinStr({ join, mtch, table_name }) { + let values = []; + if (mtch.__batch) { + let btch_mtch = ``; + btch_mtch += `(`; + for (let i = 0; i < mtch.__batch.matches.length; i++) { + const __mtch = mtch.__batch.matches[i]; + const { str, values: batch_values } = sqlGenGenJoinStr({ + join, + mtch: __mtch, + table_name, + }); + btch_mtch += str; + values.push(...batch_values); + if (i < mtch.__batch.matches.length - 1) { + btch_mtch += ` ${mtch.__batch.operator || "OR"} `; + } + } + btch_mtch += `)`; + return { + str: btch_mtch, + values, + }; + } + const equality = mtch.raw_equality || "="; + const lhs = `${typeof mtch.source == "object" ? mtch.source.tableName : table_name}.${typeof mtch.source == "object" ? mtch.source.fieldName : mtch.source}`; + const rhs = `${(() => { + if (mtch.targetLiteral) { + values.push(mtch.targetLiteral); + // if (typeof mtch.targetLiteral == "number") { + // return `${mtch.targetLiteral}`; + // } + // return `'${mtch.targetLiteral}'`; + return `?`; + } + if (join.alias) { + return `${typeof mtch.target == "object" + ? mtch.target.tableName + : join.alias}.${typeof mtch.target == "object" + ? mtch.target.fieldName + : mtch.target}`; + } + return `${typeof mtch.target == "object" + ? mtch.target.tableName + : join.tableName}.${typeof mtch.target == "object" ? mtch.target.fieldName : mtch.target}`; + })()}`; + if (mtch.between) { + values.push(mtch.between.min, mtch.between.max); + return { + str: `${lhs} BETWEEN ? AND ?`, + values, + }; + } + if (mtch.not_between) { + values.push(mtch.not_between.min, mtch.not_between.max); + return { + str: `${lhs} NOT BETWEEN ? AND ?`, + values, + }; + } + return { + str: `${lhs} ${equality} ${rhs}`, + values, + }; +} diff --git a/dist/utils/sql-generator-gen-query-str.d.ts b/dist/utils/sql-generator-gen-query-str.d.ts new file mode 100644 index 0000000..886d00a --- /dev/null +++ b/dist/utils/sql-generator-gen-query-str.d.ts @@ -0,0 +1,22 @@ +import type { ServerQueryParam, TableSelectFieldsObject } from "../types"; +type Param = { + genObject?: ServerQueryParam; + selectFields?: (keyof T | TableSelectFieldsObject)[]; + append_table_names?: boolean; + table_name: string; + full_text_match_str?: string; + full_text_search_str?: string; +}; +export default function sqlGenGenQueryStr(params: Param): { + str: string; + values: any[]; +}; +export {}; diff --git a/dist/utils/sql-generator-gen-query-str.js b/dist/utils/sql-generator-gen-query-str.js new file mode 100644 index 0000000..16fa389 --- /dev/null +++ b/dist/utils/sql-generator-gen-query-str.js @@ -0,0 +1,193 @@ +import { isUndefined } from "lodash"; +import sqlGenGrabConcatStr from "./sql-generator-grab-concat-str"; +import sqlGenGenJoinStr from "./sql-generator-gen-join-str"; +import sqlGenGrabSelectFieldSQL from "./sql-generator-grab-select-field-sql"; +export default function sqlGenGenQueryStr(params) { + let str = "SELECT"; + const genObject = params.genObject; + const table_name = params.table_name; + const full_text_match_str = params.full_text_match_str; + const full_text_search_str = params.full_text_search_str; + let sqlSearhValues = []; + if (genObject?.select_sql) { + str += ` ${genObject.select_sql}`; + } + else if (genObject?.selectFields?.[0]) { + if (genObject.join) { + str += sqlGenGrabSelectFieldSQL({ + selectFields: genObject.selectFields, + append_table_names: true, + table_name, + }); + } + else { + str += sqlGenGrabSelectFieldSQL({ + selectFields: genObject.selectFields, + table_name, + }); + } + } + else { + if (genObject?.join) { + str += ` ${table_name}.*`; + } + else { + str += " *"; + } + } + if (genObject?.countSubQueries) { + let countSqls = []; + for (let i = 0; i < genObject.countSubQueries.length; i++) { + const countSubQuery = genObject.countSubQueries[i]; + if (!countSubQuery) + continue; + const tableAlias = countSubQuery.table_alias; + let subQStr = `(SELECT COUNT(*)`; + subQStr += ` FROM ${countSubQuery.table}${tableAlias ? ` ${tableAlias}` : ""}`; + subQStr += ` WHERE (`; + for (let j = 0; j < countSubQuery.srcTrgMap.length; j++) { + const csqSrc = countSubQuery.srcTrgMap[j]; + if (!csqSrc) + continue; + subQStr += ` ${tableAlias || countSubQuery.table}.${csqSrc.src}`; + if (typeof csqSrc.trg == "string") { + subQStr += ` = ?`; + sqlSearhValues.push(csqSrc.trg); + } + else if (typeof csqSrc.trg == "object") { + subQStr += ` = ${csqSrc.trg.table}.${csqSrc.trg.field}`; + } + if (j < countSubQuery.srcTrgMap.length - 1) { + subQStr += ` AND `; + } + } + subQStr += ` )) AS ${countSubQuery.alias}`; + countSqls.push(subQStr); + } + str += `, ${countSqls.join(",")}`; + } + if (genObject?.join) { + const existingJoinTableNames = [table_name]; + str += + "," + + genObject.join + .flat() + .filter((j) => !isUndefined(j)) + .map((joinObj) => { + const joinTableName = joinObj.alias + ? joinObj.alias + : joinObj.tableName; + if (existingJoinTableNames.includes(joinTableName)) + return null; + existingJoinTableNames.push(joinTableName); + if (joinObj.group_concat) { + return sqlGenGrabConcatStr({ + field: `${joinTableName}.${joinObj.group_concat.field}`, + alias: joinObj.group_concat.alias, + separator: joinObj.group_concat.separator, + }); + } + else if (joinObj.selectFields) { + return joinObj.selectFields + .map((selectField) => { + if (typeof selectField == "string") { + return `${joinTableName}.${selectField}`; + } + else if (typeof selectField == "object") { + let aliasSelectField = `${joinTableName}.${selectField.field}`; + if (selectField.count) { + aliasSelectField = `COUNT(${joinTableName}.${selectField.field})`; + } + else if (selectField.sum) { + aliasSelectField = `SUM(${selectField.distinct ? "DISTINCT " : ""}${joinTableName}.${selectField.field})`; + } + else if (selectField.average) { + aliasSelectField = `AVERAGE(${joinTableName}.${selectField.field})`; + } + else if (selectField.max) { + aliasSelectField = `MAX(${joinTableName}.${selectField.field})`; + } + else if (selectField.min) { + aliasSelectField = `MIN(${joinTableName}.${selectField.field})`; + } + else if (selectField.group_concat && + selectField.alias) { + return sqlGenGrabConcatStr({ + field: `${joinTableName}.${selectField.field}`, + alias: selectField.alias, + separator: selectField.group_concat + .separator, + distinct: selectField.group_concat + .distinct, + }); + } + else if (selectField.distinct) { + aliasSelectField = `DISTINCT ${joinTableName}.${selectField.field}`; + } + if (selectField.alias) + aliasSelectField += ` AS ${selectField.alias}`; + return aliasSelectField; + } + }) + .join(","); + } + else { + return `${joinTableName}.*`; + } + }) + .filter((_) => Boolean(_)) + .join(","); + } + if (genObject?.fullTextSearch && + full_text_match_str && + full_text_search_str) { + str += `, ${full_text_match_str} AS ${genObject.fullTextSearch.scoreAlias}`; + sqlSearhValues.push(full_text_search_str); + } + str += ` FROM ${table_name}`; + if (genObject?.join) { + str += + " " + + genObject.join + .flat() + .filter((j) => !isUndefined(j)) + .map((join) => { + return (join.joinType + + " " + + (join.alias + ? `${join.tableName}` + " " + join.alias + : `${join.tableName}`) + + " ON " + + (() => { + if (Array.isArray(join.match)) { + return ("(" + + join.match + .map((mtch) => { + const { str, values } = sqlGenGenJoinStr({ + mtch, + join, + table_name, + }); + sqlSearhValues.push(...values); + return str; + }) + .join(join.operator + ? ` ${join.operator} ` + : " AND ") + + ")"); + } + else if (typeof join.match == "object") { + const { str, values } = sqlGenGenJoinStr({ + mtch: join.match, + join, + table_name, + }); + sqlSearhValues.push(...values); + return str; + } + })()); + }) + .join(" "); + } + return { str, values: sqlSearhValues }; +} diff --git a/dist/utils/sql-generator-gen-search-str.d.ts b/dist/utils/sql-generator-gen-search-str.d.ts new file mode 100644 index 0000000..563daf8 --- /dev/null +++ b/dist/utils/sql-generator-gen-search-str.d.ts @@ -0,0 +1,12 @@ +import type { ServerQueryParamsJoin, ServerQueryQueryObject, SQLInsertGenValueType } from "../types"; +type Param = { + queryObj: ServerQueryQueryObject[string]; + join?: (ServerQueryParamsJoin | ServerQueryParamsJoin[] | undefined)[]; + field?: string; + table_name: string; +}; +export default function sqlGenGenSearchStr({ queryObj, join, field, table_name, }: Param): { + str: string; + values: SQLInsertGenValueType[]; +}; +export {}; diff --git a/dist/utils/sql-generator-gen-search-str.js b/dist/utils/sql-generator-gen-search-str.js new file mode 100644 index 0000000..e7cf269 --- /dev/null +++ b/dist/utils/sql-generator-gen-search-str.js @@ -0,0 +1,92 @@ +import sqlGenOperatorGen from "./sql-gen-operator-gen"; +export default function sqlGenGenSearchStr({ queryObj, join, field, table_name, }) { + let sqlSearhValues = []; + const finalFieldName = (() => { + if (queryObj?.tableName) { + return `${queryObj.tableName}.${field}`; + } + if (join) { + return `${table_name}.${field}`; + } + return field; + })(); + let str = `${finalFieldName}=?`; + function grabValue(val) { + const valueParsed = val; + if (!valueParsed) + return; + const valueString = typeof valueParsed == "string" || typeof valueParsed == "number" + ? valueParsed + : valueParsed + ? valueParsed.fieldName && valueParsed.tableName + ? `${valueParsed.tableName}.${valueParsed.fieldName}` + : valueParsed.value + : undefined; + const valueEquality = typeof valueParsed == "object" + ? valueParsed.equality || queryObj.equality + : queryObj.equality; + const operatorStrParam = sqlGenOperatorGen({ + queryObj, + equality: valueEquality, + fieldName: finalFieldName || "", + value: valueString || "", + isValueFieldValue: Boolean(typeof valueParsed == "object" && + valueParsed.fieldName && + valueParsed.tableName), + }); + return operatorStrParam; + } + if (Array.isArray(queryObj.value)) { + const strArray = []; + queryObj.value.forEach((val) => { + const operatorStrParam = grabValue(val); + if (!operatorStrParam) + return; + if (operatorStrParam.str && operatorStrParam.param) { + strArray.push(operatorStrParam.str); + sqlSearhValues.push(operatorStrParam.param); + } + else if (operatorStrParam.str) { + strArray.push(operatorStrParam.str); + } + }); + str = "(" + strArray.join(` ${queryObj.operator || "AND"} `) + ")"; + } + else if (typeof queryObj.value == "object") { + const operatorStrParam = grabValue(queryObj.value); + if (operatorStrParam?.str) { + str = operatorStrParam.str; + if (operatorStrParam.param) { + sqlSearhValues.push(operatorStrParam.param); + } + } + } + else if (queryObj.raw_equality && queryObj.value) { + str = `${finalFieldName} ${queryObj.raw_equality} ?`; + sqlSearhValues.push(queryObj.value); + } + else if (queryObj.between) { + str = `${finalFieldName} BETWEEN ? AND ?`; + sqlSearhValues.push(queryObj.between.min, queryObj.between.max); + } + else { + const valueParsed = queryObj.value ? queryObj.value : undefined; + const operatorStrParam = sqlGenOperatorGen({ + equality: queryObj.equality, + fieldName: finalFieldName || "", + value: valueParsed, + queryObj, + }); + if (operatorStrParam.str && operatorStrParam.param) { + str = operatorStrParam.str; + sqlSearhValues.push(operatorStrParam.param); + } + else if (operatorStrParam.str && !operatorStrParam.str.match(/\?/)) { + str = operatorStrParam.str; + } + else { + sqlSearhValues.push(valueParsed || ""); + } + } + return { str, values: sqlSearhValues }; +} diff --git a/dist/utils/sql-generator-grab-concat-str.d.ts b/dist/utils/sql-generator-grab-concat-str.d.ts new file mode 100644 index 0000000..3eb39c7 --- /dev/null +++ b/dist/utils/sql-generator-grab-concat-str.d.ts @@ -0,0 +1,8 @@ +type Param = { + field: string; + alias: string; + separator?: string; + distinct?: boolean; +}; +export default function sqlGenGrabConcatStr({ alias, field, separator, distinct, }: Param): string; +export {}; diff --git a/dist/utils/sql-generator-grab-concat-str.js b/dist/utils/sql-generator-grab-concat-str.js new file mode 100644 index 0000000..8993f00 --- /dev/null +++ b/dist/utils/sql-generator-grab-concat-str.js @@ -0,0 +1,13 @@ +export default function sqlGenGrabConcatStr({ alias, field, separator = ",", distinct, }) { + let gc = `GROUP_CONCAT(`; + if (distinct) { + gc += `DISTINCT `; + } + gc += `${field}`; + if (!distinct) { + gc += `, '${separator}'`; + } + gc += `)`; + gc += ` AS ${alias}`; + return gc; +} diff --git a/dist/utils/sql-generator-grab-select-field-sql.d.ts b/dist/utils/sql-generator-grab-select-field-sql.d.ts new file mode 100644 index 0000000..83d13cc --- /dev/null +++ b/dist/utils/sql-generator-grab-select-field-sql.d.ts @@ -0,0 +1,16 @@ +import type { TableSelectFieldsObject } from "../types"; +type Param = { + selectFields: (keyof T | TableSelectFieldsObject)[]; + append_table_names?: boolean; + table_name: string; +}; +export default function sqlGenGrabSelectFieldSQL({ selectFields, append_table_names, table_name }: Param): string; +export {}; diff --git a/dist/utils/sql-generator-grab-select-field-sql.js b/dist/utils/sql-generator-grab-select-field-sql.js new file mode 100644 index 0000000..4056788 --- /dev/null +++ b/dist/utils/sql-generator-grab-select-field-sql.js @@ -0,0 +1,55 @@ +import sqlGenGrabConcatStr from "./sql-generator-grab-concat-str"; +export default function sqlGenGrabSelectFieldSQL({ selectFields, append_table_names, table_name }) { + let str = ""; + str += ` ${selectFields + ?.map((fld) => { + let fld_str = ``; + const final_fld_name = typeof fld == "object" + ? append_table_names + ? `${table_name}.${String(fld)}` + : `${String(fld.fieldName)}` + : `${String(fld)}`; + if (typeof fld == "object") { + const fld_name = `${String(fld.fieldName)}`; + if (fld.count) { + fld_str += `COUNT(${fld_name})`; + } + else if (fld.sum) { + fld_str += `SUM(${fld_name})`; + } + else if (fld.average) { + fld_str += `AVERAGE(${fld_name})`; + } + else if (fld.max) { + fld_str += `MAX(${fld_name})`; + } + else if (fld.min) { + fld_str += `MIN(${fld_name})`; + } + else if (fld.distinct) { + fld_str += `DISTINCT ${fld_name}`; + } + else if (fld.group_concat) { + fld_str += sqlGenGrabConcatStr({ + field: fld_name, + alias: fld.group_concat.alias, + separator: fld.group_concat.separator, + distinct: fld.group_concat.distinct, + }); + } + else { + fld_str += + final_fld_name + (fld.alias ? ` as ${fld.alias}` : ``); + } + if (fld.alias) { + fld_str += ` AS ${fld.alias}`; + } + } + else { + fld_str += final_fld_name; + } + return fld_str; + }) + .join(",")}`; + return str; +} diff --git a/dist/utils/sql-generator.d.ts b/dist/utils/sql-generator.d.ts new file mode 100644 index 0000000..21eedbb --- /dev/null +++ b/dist/utils/sql-generator.d.ts @@ -0,0 +1,25 @@ +import type { ServerQueryParam, SQLInsertGenValueType } from "../types"; +type Param = { + genObject?: ServerQueryParam; + tableName: string; + dbFullName?: string; + count?: boolean; +}; +type Return = { + string: string; + values: SQLInsertGenValueType[]; +}; +/** + * # SQL Query Generator + * @description Builds parameterized SELECT SQL for MariaDB + */ +export default function sqlGenerator({ tableName, genObject, dbFullName, count }: Param): Return; +export {}; diff --git a/dist/utils/sql-generator.js b/dist/utils/sql-generator.js new file mode 100644 index 0000000..c93b0ef --- /dev/null +++ b/dist/utils/sql-generator.js @@ -0,0 +1,303 @@ +import sqlGenGenSearchStr from "./sql-generator-gen-search-str"; +import sqlGenGenQueryStr from "./sql-generator-gen-query-str"; +/** + * # SQL Query Generator + * @description Builds parameterized SELECT SQL for MariaDB + */ +export default function sqlGenerator({ tableName, genObject, dbFullName, count }) { + const finalQuery = genObject?.query ? genObject.query : undefined; + const queryKeys = finalQuery ? Object.keys(finalQuery) : undefined; + const sqlSearhValues = []; + let fullTextMatchStr = genObject?.fullTextSearch + ? ` MATCH(${genObject.fullTextSearch.fields + .map((f) => genObject.join ? `${tableName}.${String(f)}` : `${String(f)}`) + .join(",")}) AGAINST (? IN BOOLEAN MODE)` + : undefined; + const fullTextSearchStr = genObject?.fullTextSearch + ? genObject.fullTextSearch.searchTerm + .split(` `) + .map((t) => `${t}`) + .join(" ") + : undefined; + let { str: queryString, values } = sqlGenGenQueryStr({ + table_name: tableName, + append_table_names: true, + full_text_match_str: fullTextMatchStr, + full_text_search_str: fullTextSearchStr, + genObject, + }); + sqlSearhValues.push(...values); + const sqlSearhString = queryKeys?.map((field) => { + const queryObj = finalQuery?.[field]; + if (!queryObj) + return; + if (queryObj.__query) { + const subQueryGroup = queryObj.__query; + const subSearchKeys = Object.keys(subQueryGroup); + const subSearchString = subSearchKeys.map((_field) => { + const newSubQueryObj = subQueryGroup?.[_field]; + if (newSubQueryObj) { + const { str, values } = sqlGenGenSearchStr({ + queryObj: newSubQueryObj, + field: newSubQueryObj.fieldName || _field, + join: genObject?.join, + table_name: tableName, + }); + sqlSearhValues.push(...values); + return str; + } + }); + return ("(" + + subSearchString.join(` ${queryObj.operator || "AND"} `) + + ")"); + } + const { str, values } = sqlGenGenSearchStr({ + queryObj, + field: queryObj.fieldName || field, + join: genObject?.join, + table_name: tableName, + }); + sqlSearhValues.push(...values); + return str; + }); + const cleanedUpSearchStr = sqlSearhString?.filter((str) => typeof str == "string"); + const isSearchStr = cleanedUpSearchStr?.[0] && cleanedUpSearchStr.find((str) => str); + if (isSearchStr) { + const stringOperator = genObject?.searchOperator || "AND"; + queryString += ` WHERE ${cleanedUpSearchStr.join(` ${stringOperator} `)}`; + } + if (genObject?.fullTextSearch && fullTextSearchStr && fullTextMatchStr) { + queryString += `${isSearchStr ? " AND" : " WHERE"} ${fullTextMatchStr}`; + sqlSearhValues.push(fullTextSearchStr); + } + if (genObject?.group) { + let group_by_txt = ``; + if (typeof genObject.group == "string") { + group_by_txt = genObject.group; + } + else if (Array.isArray(genObject.group)) { + for (let i = 0; i < genObject.group.length; i++) { + const group = genObject.group[i]; + if (typeof group == "string") { + group_by_txt += `\`${group.toString()}\``; + } + else if (typeof group == "object" && group.table) { + group_by_txt += `${group.table}.${String(group.field)}`; + } + else if (typeof group == "object") { + group_by_txt += `${String(group.field)}`; + } + if (i < genObject.group.length - 1) { + group_by_txt += ","; + } + } + } + else if (typeof genObject.group == "object") { + if (genObject.group.table) { + group_by_txt = `${genObject.group.table}.${String(genObject.group.field)}`; + } + else { + group_by_txt = `${String(genObject.group.field)}`; + } + } + queryString += ` GROUP BY ${group_by_txt}`; + } + function grabOrderString(order) { + let orderFields = []; + let orderSrt = ``; + if (genObject?.fullTextSearch && genObject.fullTextSearch.scoreAlias) { + orderFields.push(genObject.fullTextSearch.scoreAlias); + } + else if (genObject?.join) { + orderFields.push(`${tableName}.${String(order.field)}`); + } + else { + orderFields.push(order.field); + } + orderSrt += ` ${orderFields.join(", ")} ${order.strategy}`; + return orderSrt; + } + if (genObject?.order) { + let orderSrt = ` ORDER BY`; + if (Array.isArray(genObject.order)) { + for (let i = 0; i < genObject.order.length; i++) { + const order = genObject.order[i]; + if (order) { + orderSrt += + grabOrderString(order) + + (i < genObject.order.length - 1 ? `,` : ""); + } + } + } + else { + orderSrt += grabOrderString(genObject.order); + } + queryString += ` ${orderSrt}`; + } + if (genObject?.limit && !count) + queryString += ` LIMIT ${genObject.limit}`; + if (genObject?.offset) { + queryString += ` OFFSET ${genObject.offset}`; + } + else if (genObject?.page && genObject.limit && !count) { + queryString += ` OFFSET ${(genObject.page - 1) * genObject.limit}`; + } + return { + string: queryString, + values: sqlSearhValues, + }; +} +// let queryString = (() => { +// let str = "SELECT"; +// if (genObject?.select_sql) { +// str += ` ${genObject.select_sql}`; +// } else if (genObject?.selectFields?.[0]) { +// if (genObject.join) { +// str += sqlGenGrabSelectFieldSQL({ +// selectFields: genObject.selectFields, +// append_table_names: true, +// table_name: tableName, +// }); +// } else { +// str += sqlGenGrabSelectFieldSQL({ +// selectFields: genObject.selectFields, +// table_name: tableName, +// }); +// } +// } else { +// if (genObject?.join) { +// str += ` ${tableName}.*`; +// } else { +// str += " *"; +// } +// } +// if (genObject?.countSubQueries) { +// let countSqls: string[] = []; +// for (let i = 0; i < genObject.countSubQueries.length; i++) { +// const countSubQuery = genObject.countSubQueries[i]; +// if (!countSubQuery) continue; +// const tableAlias = countSubQuery.table_alias; +// let subQStr = `(SELECT COUNT(*)`; +// subQStr += ` FROM ${countSubQuery.table}${ +// tableAlias ? ` ${tableAlias}` : "" +// }`; +// subQStr += ` WHERE (`; +// for (let j = 0; j < countSubQuery.srcTrgMap.length; j++) { +// const csqSrc = countSubQuery.srcTrgMap[j]; +// if (!csqSrc) continue; +// subQStr += ` ${tableAlias || countSubQuery.table}.${ +// csqSrc.src +// }`; +// if (typeof csqSrc.trg == "string") { +// subQStr += ` = ?`; +// sqlSearhValues.push(csqSrc.trg); +// } else if (typeof csqSrc.trg == "object") { +// subQStr += ` = ${csqSrc.trg.table}.${csqSrc.trg.field}`; +// } +// if (j < countSubQuery.srcTrgMap.length - 1) { +// subQStr += ` AND `; +// } +// } +// subQStr += ` )) AS ${countSubQuery.alias}`; +// countSqls.push(subQStr); +// } +// str += `, ${countSqls.join(",")}`; +// } +// if (genObject?.join) { +// const existingJoinTableNames: string[] = [tableName]; +// str += +// "," + +// genObject.join +// .flat() +// .filter((j) => !isUndefined(j)) +// .map((joinObj) => { +// const joinTableName = joinObj.alias +// ? joinObj.alias +// : joinObj.tableName; +// if (existingJoinTableNames.includes(joinTableName)) +// return null; +// existingJoinTableNames.push(joinTableName); +// if (joinObj.group_concat) { +// return sqlGenGrabConcatStr({ +// field: `${joinTableName}.${joinObj.group_concat.field}`, +// alias: joinObj.group_concat.alias, +// separator: joinObj.group_concat.separator, +// }); +// } else if (joinObj.selectFields) { +// return joinObj.selectFields +// .map((selectField) => { +// if (typeof selectField == "string") { +// return `${joinTableName}.${selectField}`; +// } else if (typeof selectField == "object") { +// let aliasSelectField = selectField.count +// ? `COUNT(${joinTableName}.${selectField.field})` +// : `${joinTableName}.${selectField.field}`; +// if (selectField.alias) +// aliasSelectField += ` AS ${selectField.alias}`; +// return aliasSelectField; +// } +// }) +// .join(","); +// } else { +// return `${joinTableName}.*`; +// } +// }) +// .filter((_) => Boolean(_)) +// .join(","); +// } +// if ( +// genObject?.fullTextSearch && +// fullTextMatchStr && +// fullTextSearchStr +// ) { +// str += `, ${fullTextMatchStr} AS ${genObject.fullTextSearch.scoreAlias}`; +// sqlSearhValues.push(fullTextSearchStr); +// } +// str += ` FROM ${tableName}`; +// if (genObject?.join) { +// str += +// " " + +// genObject.join +// .flat() +// .filter((j) => !isUndefined(j)) +// .map((join) => { +// return ( +// join.joinType + +// " " + +// (join.alias +// ? `${join.tableName}` + " " + join.alias +// : `${join.tableName}`) + +// " ON " + +// (() => { +// if (Array.isArray(join.match)) { +// return ( +// "(" + +// join.match +// .map((mtch) => +// sqlGenGenJoinStr({ +// mtch, +// join, +// table_name: tableName, +// }), +// ) +// .join( +// join.operator +// ? ` ${join.operator} ` +// : " AND ", +// ) + +// ")" +// ); +// } else if (typeof join.match == "object") { +// return sqlGenGenJoinStr({ +// mtch: join.match, +// join, +// table_name: tableName, +// }); +// } +// })() +// ); +// }) +// .join(" "); +// } +// return str; +// })(); diff --git a/dist/utils/sql-insert-generator.d.ts b/dist/utils/sql-insert-generator.d.ts new file mode 100644 index 0000000..1b43778 --- /dev/null +++ b/dist/utils/sql-insert-generator.d.ts @@ -0,0 +1,5 @@ +import type { SQLInsertGenParams, SQLInsertGenReturn } from "../types"; +/** + * # SQL Insert Generator + */ +export default function sqlInsertGenerator({ tableName, data, dbFullName, }: SQLInsertGenParams): SQLInsertGenReturn | undefined; diff --git a/dist/utils/sql-insert-generator.js b/dist/utils/sql-insert-generator.js new file mode 100644 index 0000000..197f584 --- /dev/null +++ b/dist/utils/sql-insert-generator.js @@ -0,0 +1,62 @@ +function quoteIdentifier(identifier) { + return `\`${identifier.replace(/`/g, "``")}\``; +} +/** + * # SQL Insert Generator + */ +export default function sqlInsertGenerator({ tableName, data, dbFullName, }) { + const finalDbName = dbFullName ? `${quoteIdentifier(dbFullName)}.` : ""; + try { + if (Array.isArray(data) && data?.[0]) { + let insertKeys = []; + data.forEach((dt) => { + const kys = Object.keys(dt); + kys.forEach((ky) => { + if (!insertKeys.includes(ky)) { + insertKeys.push(ky); + } + }); + }); + let queryBatches = []; + let queryValues = []; + data.forEach((item) => { + queryBatches.push(`(${insertKeys + .map((ky) => { + const value = item[ky]; + const finalValue = typeof value == "string" || + typeof value == "number" + ? value + : typeof value == "function" + ? value().value + : value + ? value + : null; + if (!finalValue) { + queryValues.push(null); + return "?"; + } + queryValues.push(finalValue); + const placeholder = typeof value == "function" + ? value().placeholder + : "?"; + return placeholder; + }) + .filter((k) => Boolean(k)) + .join(",")})`); + }); + const insertColumns = insertKeys.map(quoteIdentifier).join(","); + let query = `INSERT INTO ${finalDbName}${quoteIdentifier(tableName)} (${insertColumns}) VALUES ${queryBatches.join(",")}`; + return { + query: query, + values: queryValues, + }; + } + else { + return undefined; + } + } + catch ( /** @type {any} */error) { + console.log(`SQL insert gen ERROR: ${error.message}`); + return undefined; + } +} diff --git a/dist/utils/trim-backups.d.ts b/dist/utils/trim-backups.d.ts new file mode 100644 index 0000000..3772d2a --- /dev/null +++ b/dist/utils/trim-backups.d.ts @@ -0,0 +1,6 @@ +import type { BunMariaDBConfig } from "../types"; +type Params = { + config: BunMariaDBConfig; +}; +export default function trimBackups({ config }: Params): void; +export {}; diff --git a/dist/utils/trim-backups.js b/dist/utils/trim-backups.js new file mode 100644 index 0000000..a9bd3a7 --- /dev/null +++ b/dist/utils/trim-backups.js @@ -0,0 +1,19 @@ +import grabDBDir from "../utils/grab-db-dir"; +import fs from "fs"; +import grabSortedBackups from "./grab-sorted-backups"; +import { AppData } from "../data/app-data"; +import path from "path"; +export default function trimBackups({ config }) { + const { backup_dir } = grabDBDir({ config }); + const backups = grabSortedBackups({ config }); + const max_backups = config.max_backups || AppData["MaxBackups"]; + for (let i = 0; i < backups.length; i++) { + const backup_name = backups[i]; + if (!backup_name) + continue; + if (i > max_backups - 1) { + const backup_file_to_unlink = path.join(backup_dir, backup_name); + fs.unlinkSync(backup_file_to_unlink); + } + } +} diff --git a/dist/utils/trim-exports.d.ts b/dist/utils/trim-exports.d.ts new file mode 100644 index 0000000..57705fd --- /dev/null +++ b/dist/utils/trim-exports.d.ts @@ -0,0 +1,6 @@ +import type { BunMariaDBConfig } from "../types"; +type Params = { + config: BunMariaDBConfig; +}; +export default function trimExports({ config }: Params): void; +export {}; diff --git a/dist/utils/trim-exports.js b/dist/utils/trim-exports.js new file mode 100644 index 0000000..ac4fb63 --- /dev/null +++ b/dist/utils/trim-exports.js @@ -0,0 +1,18 @@ +import fs from "fs"; +import path from "path"; +import { AppData } from "../data/app-data"; +import grabDBDir from "./grab-db-dir"; +import grabSortedExports from "./grab-sorted-exports"; +export default function trimExports({ config }) { + const { export_dir } = grabDBDir({ config }); + const exports = grabSortedExports({ config }); + const max_exports = config.max_exports || AppData["MaxExports"]; + for (let i = 0; i < exports.length; i++) { + const export_name = exports[i]; + if (!export_name) + continue; + if (i > max_exports - 1) { + fs.unlinkSync(path.join(export_dir, export_name)); + } + } +}