diff --git a/dist/commands/admin/index.js b/dist/commands/admin/index.js index 758c27f..169d369 100644 --- a/dist/commands/admin/index.js +++ b/dist/commands/admin/index.js @@ -19,15 +19,18 @@ export default function () { const paradigm = await select({ message: "Choose an action:", choices: [ - { name: "List Tables", value: "list_tables" }, - { name: "Run SQL", value: "run_sql" }, + { name: "Tables", value: "list_tables" }, + { name: "SQL", value: "run_sql" }, { name: chalk.dim("✕ Exit"), value: "exit" }, ], }); if (paradigm === "exit") break; - if (paradigm === "list_tables") - await listTables({ db }); + if (paradigm === "list_tables") { + const result = await listTables({ db }); + if (result === "__exit__") + break; + } if (paradigm === "run_sql") await runSQL({ db }); } diff --git a/dist/commands/admin/list-tables.d.ts b/dist/commands/admin/list-tables.d.ts index 72039a3..9109b3b 100644 --- a/dist/commands/admin/list-tables.d.ts +++ b/dist/commands/admin/list-tables.d.ts @@ -2,5 +2,5 @@ import { Database } from "bun:sqlite"; type Params = { db: Database; }; -export default function listTables({ db }: Params): Promise; +export default function listTables({ db, }: Params): Promise<"__exit__" | void>; export {}; diff --git a/dist/commands/admin/list-tables.js b/dist/commands/admin/list-tables.js index 2d41494..44d807d 100644 --- a/dist/commands/admin/list-tables.js +++ b/dist/commands/admin/list-tables.js @@ -3,7 +3,8 @@ import chalk from "chalk"; import { select } from "@inquirer/prompts"; import { AppData } from "../../data/app-data"; import showEntries from "./show-entries"; -export default async function listTables({ db }) { +import showFields from "./show-fields"; +export default async function listTables({ db, }) { const tables = db .query(`SELECT table_name FROM ${AppData["DbSchemaManagerTableName"]}`) .all(); @@ -16,26 +17,43 @@ export default async function listTables({ db }) { const tableName = await select({ message: "Select a table:", choices: [ - ...tables.map((t) => ({ name: t.table_name, value: t.table_name })), + ...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__"; // Level 2: action loop — stays here until "Go Back" while (true) { const action = await select({ message: `"${tableName}" — choose an action:`, choices: [ - { name: "Show Entries", value: "entries" }, - { name: "Show Schema", value: "schema" }, + { 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") { - await showEntries({ db, tableName }); + const result = await showEntries({ db, tableName }); + if (result === "__exit__") + return "__exit__"; + } + if (action === "fields") { + const result = await showFields({ db, tableName }); + if (result === "__exit__") + return "__exit__"; } if (action === "schema") { const columns = db diff --git a/dist/commands/admin/show-entries.d.ts b/dist/commands/admin/show-entries.d.ts index d97f56a..588f3ed 100644 --- a/dist/commands/admin/show-entries.d.ts +++ b/dist/commands/admin/show-entries.d.ts @@ -3,5 +3,5 @@ type Params = { db: Database; tableName: string; }; -export default function showEntries({ db, tableName }: Params): Promise; +export default function showEntries({ db, tableName }: Params): Promise<"__exit__" | undefined>; export {}; diff --git a/dist/commands/admin/show-entries.js b/dist/commands/admin/show-entries.js index d4984cc..2e788cf 100644 --- a/dist/commands/admin/show-entries.js +++ b/dist/commands/admin/show-entries.js @@ -19,19 +19,20 @@ export default async function showEntries({ db, tableName }) { ? db .query(`SELECT COUNT(*) as count FROM "${tableName}" WHERE "${searchField}" LIKE ?`) .get(`%${searchTerm}%`) - : db - .query(`SELECT COUNT(*) as count FROM "${tableName}"`) - .get()); + : db.query(`SELECT COUNT(*) as count FROM "${tableName}"`).get()); const total = countRow.count; const searchInfo = searchTerm ? chalk.dim(` · searching "${searchField}" = "${searchTerm}"`) : ""; console.log(`\n${chalk.bold(tableName)} — Page ${page + 1}${searchInfo} (${rows.length} of ${total}):\n`); - if (rows.length) - console.table(rows); - else + if (rows.length) { + console.log(rows); + // if (rows.length) console.table(rows); + } + else { console.log(chalk.yellow("No rows found.")); - console.log(); + console.log(); + } const choices = []; if (page > 0) choices.push({ name: "← Previous Page", value: "prev" }); @@ -41,9 +42,12 @@ export default async function showEntries({ db, tableName }) { 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") diff --git a/dist/commands/admin/show-fields.d.ts b/dist/commands/admin/show-fields.d.ts new file mode 100644 index 0000000..3504c55 --- /dev/null +++ b/dist/commands/admin/show-fields.d.ts @@ -0,0 +1,7 @@ +import { Database } from "bun:sqlite"; +type Params = { + db: Database; + tableName: string; +}; +export default function showFields({ db, 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..2856ce7 --- /dev/null +++ b/dist/commands/admin/show-fields.js @@ -0,0 +1,50 @@ +import { Database } from "bun:sqlite"; +import chalk from "chalk"; +import { select } from "@inquirer/prompts"; +export default async function showFields({ db, tableName }) { + const columns = db.query(`PRAGMA table_info("${tableName}")`).all(); + const indexes = db.query(`PRAGMA index_list("${tableName}")`).all(); + const foreignKeys = db.query(`PRAGMA foreign_key_list("${tableName}")`).all(); + const indexedFields = new Map(); + for (const idx of indexes) { + const cols = db.query(`PRAGMA index_info("${idx.name}")`).all(); + for (const col of cols) { + indexedFields.set(col.name, { unique: idx.unique === 1 }); + } + } + while (true) { + const fieldName = await select({ + message: `"${tableName}" — select a field:`, + choices: [ + ...columns.map((c) => ({ name: c.name, value: c.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.name === fieldName); + const idx = indexedFields.get(fieldName); + const fk = foreignKeys.find((f) => f.from === fieldName); + console.log(`\n${chalk.bold(`Field: "${fieldName}"`)}\n`); + console.log(` ${chalk.dim("Table")} ${tableName}`); + console.log(` ${chalk.dim("Column #")} ${col.cid}`); + console.log(` ${chalk.dim("Type")} ${col.type || chalk.italic("(none)")}`); + console.log(` ${chalk.dim("Primary Key")} ${col.pk ? chalk.green("YES") : "NO"}`); + console.log(` ${chalk.dim("Not Null")} ${col.notnull ? chalk.yellow("YES") : "NO"}`); + console.log(` ${chalk.dim("Default")} ${col.dflt_value ?? chalk.italic("(none)")}`); + console.log(` ${chalk.dim("Indexed")} ${idx ? chalk.cyan("YES") : "NO"}`); + console.log(` ${chalk.dim("Unique")} ${idx?.unique ? chalk.cyan("YES") : "NO"}`); + if (fk) { + console.log(` ${chalk.dim("Foreign Key")} ${chalk.magenta(`${fk.table}(${fk.to})`)}`); + console.log(` ${chalk.dim("On Update")} ${fk.on_update}`); + console.log(` ${chalk.dim("On Delete")} ${fk.on_delete}`); + } + else { + console.log(` ${chalk.dim("Foreign Key")} NO`); + } + console.log(); + } +} diff --git a/package.json b/package.json index 59cffab..f6fb833 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@moduletrace/bun-sqlite", - "version": "1.0.13", + "version": "1.0.14", "description": "SQLite manager for Bun", "author": "Benjamin Toby", "main": "dist/index.js", diff --git a/src/commands/admin/index.ts b/src/commands/admin/index.ts index cc2a78d..2d946e0 100644 --- a/src/commands/admin/index.ts +++ b/src/commands/admin/index.ts @@ -22,14 +22,17 @@ export default function () { const paradigm = await select({ message: "Choose an action:", choices: [ - { name: "List Tables", value: "list_tables" }, - { name: "Run SQL", value: "run_sql" }, + { name: "Tables", value: "list_tables" }, + { name: "SQL", value: "run_sql" }, { name: chalk.dim("✕ Exit"), value: "exit" }, ], }); if (paradigm === "exit") break; - if (paradigm === "list_tables") await listTables({ db }); + if (paradigm === "list_tables") { + const result = await listTables({ db }); + if (result === "__exit__") break; + } if (paradigm === "run_sql") await runSQL({ db }); } } catch (error: any) { diff --git a/src/commands/admin/list-tables.ts b/src/commands/admin/list-tables.ts index d2fecde..77e878c 100644 --- a/src/commands/admin/list-tables.ts +++ b/src/commands/admin/list-tables.ts @@ -3,6 +3,7 @@ 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"; type Params = { db: Database }; @@ -15,11 +16,14 @@ type ColumnInfo = { pk: number; }; -export default async function listTables({ db }: Params) { +export default async function listTables({ + db, +}: Params): Promise<"__exit__" | void> { const tables = db - .query<{ table_name: string }, []>( - `SELECT table_name FROM ${AppData["DbSchemaManagerTableName"]}`, - ) + .query< + { table_name: string }, + [] + >(`SELECT table_name FROM ${AppData["DbSchemaManagerTableName"]}`) .all(); if (!tables.length) { @@ -32,28 +36,42 @@ export default async function listTables({ db }: Params) { const tableName = await select({ message: "Select a table:", choices: [ - ...tables.map((t) => ({ name: t.table_name, value: t.table_name })), + ...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__"; // Level 2: action loop — stays here until "Go Back" while (true) { const action = await select({ message: `"${tableName}" — choose an action:`, choices: [ - { name: "Show Entries", value: "entries" }, - { name: "Show Schema", value: "schema" }, + { 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") { - await showEntries({ db, tableName }); + const result = await showEntries({ db, tableName }); + if (result === "__exit__") return "__exit__"; + } + + if (action === "fields") { + const result = await showFields({ db, tableName }); + if (result === "__exit__") return "__exit__"; } if (action === "schema") { diff --git a/src/commands/admin/show-entries.ts b/src/commands/admin/show-entries.ts index 5a267ab..c869f46 100644 --- a/src/commands/admin/show-entries.ts +++ b/src/commands/admin/show-entries.ts @@ -34,9 +34,7 @@ export default async function showEntries({ db, tableName }: Params) { `SELECT COUNT(*) as count FROM "${tableName}" WHERE "${searchField}" LIKE ?`, ) .get(`%${searchTerm}%`) - : db - .query(`SELECT COUNT(*) as count FROM "${tableName}"`) - .get() + : db.query(`SELECT COUNT(*) as count FROM "${tableName}"`).get() ) as { count: number }; const total = countRow.count; @@ -47,9 +45,14 @@ export default async function showEntries({ db, tableName }: Params) { 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(); + + if (rows.length) { + console.log(rows); + // if (rows.length) console.table(rows); + } else { + console.log(chalk.yellow("No rows found.")); + console.log(); + } const choices: { name: string; value: string }[] = []; if (page > 0) choices.push({ name: "← Previous Page", value: "prev" }); @@ -59,10 +62,12 @@ export default async function showEntries({ db, tableName }: Params) { 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") { diff --git a/src/commands/admin/show-fields.ts b/src/commands/admin/show-fields.ts new file mode 100644 index 0000000..912014d --- /dev/null +++ b/src/commands/admin/show-fields.ts @@ -0,0 +1,68 @@ +import { Database } from "bun:sqlite"; +import chalk from "chalk"; +import { select } from "@inquirer/prompts"; + +type Params = { db: Database; tableName: string }; + +type ColumnInfo = { + cid: number; + name: string; + type: string; + notnull: number; + dflt_value: string | null; + pk: number; +}; + +type IndexInfo = { name: string; unique: number; origin: string }; +type IndexColumn = { name: string }; +type ForeignKey = { id: number; from: string; table: string; to: string; on_update: string; on_delete: string }; + +export default async function showFields({ db, tableName }: Params): Promise<"__exit__" | void> { + const columns = db.query(`PRAGMA table_info("${tableName}")`).all(); + const indexes = db.query(`PRAGMA index_list("${tableName}")`).all(); + const foreignKeys = db.query(`PRAGMA foreign_key_list("${tableName}")`).all(); + + const indexedFields = new Map(); + for (const idx of indexes) { + const cols = db.query(`PRAGMA index_info("${idx.name}")`).all(); + for (const col of cols) { + indexedFields.set(col.name, { unique: idx.unique === 1 }); + } + } + + while (true) { + const fieldName = await select({ + message: `"${tableName}" — select a field:`, + choices: [ + ...columns.map((c) => ({ name: c.name, value: c.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.name === fieldName)!; + const idx = indexedFields.get(fieldName); + const fk = foreignKeys.find((f) => f.from === fieldName); + + console.log(`\n${chalk.bold(`Field: "${fieldName}"`)}\n`); + console.log(` ${chalk.dim("Table")} ${tableName}`); + console.log(` ${chalk.dim("Column #")} ${col.cid}`); + console.log(` ${chalk.dim("Type")} ${col.type || chalk.italic("(none)")}`); + console.log(` ${chalk.dim("Primary Key")} ${col.pk ? chalk.green("YES") : "NO"}`); + console.log(` ${chalk.dim("Not Null")} ${col.notnull ? chalk.yellow("YES") : "NO"}`); + console.log(` ${chalk.dim("Default")} ${col.dflt_value ?? chalk.italic("(none)")}`); + console.log(` ${chalk.dim("Indexed")} ${idx ? chalk.cyan("YES") : "NO"}`); + console.log(` ${chalk.dim("Unique")} ${idx?.unique ? chalk.cyan("YES") : "NO"}`); + if (fk) { + console.log(` ${chalk.dim("Foreign Key")} ${chalk.magenta(`${fk.table}(${fk.to})`)}`); + console.log(` ${chalk.dim("On Update")} ${fk.on_update}`); + console.log(` ${chalk.dim("On Delete")} ${fk.on_delete}`); + } else { + console.log(` ${chalk.dim("Foreign Key")} NO`); + } + console.log(); + } +}