mirror of
https://github.com/stfrigerio/sqliteDB.git
synced 2026-07-22 05:38:02 +00:00
created project structure and a couple of commands
This commit is contained in:
parent
9974c6b28c
commit
3706531e70
10 changed files with 418 additions and 87 deletions
152
main.ts
152
main.ts
|
|
@ -1,103 +1,81 @@
|
|||
import {
|
||||
App,
|
||||
Plugin,
|
||||
PluginSettingTab,
|
||||
Setting,
|
||||
Modal,
|
||||
import {
|
||||
App,
|
||||
Plugin,
|
||||
PluginSettingTab,
|
||||
Setting,
|
||||
Modal,
|
||||
Notice,
|
||||
FileSystemAdapter // <-- Import FileSystemAdapter
|
||||
FileSystemAdapter,
|
||||
Editor,
|
||||
MarkdownView,
|
||||
} from "obsidian";
|
||||
import { DBService } from "./src/dbService";
|
||||
import { inspectTableStructure, convertEntriesInNotes } from "./src/commands";
|
||||
import { pickTableName } from "./src/helpers";
|
||||
import { SqliteDBSettings, DEFAULT_SETTINGS } from "./src/types";
|
||||
|
||||
import initSqlJs, { Database } from "sql.js";
|
||||
import { readFileSync, writeFileSync } from "fs";
|
||||
|
||||
interface MyWasmDBSettings {
|
||||
dbFilePath: string;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: MyWasmDBSettings = {
|
||||
dbFilePath: "",
|
||||
};
|
||||
|
||||
export default class MyWasmDBPlugin extends Plugin {
|
||||
settings: MyWasmDBSettings;
|
||||
private db: Database | null = null;
|
||||
export default class SqliteDBPlugin extends Plugin {
|
||||
settings: SqliteDBSettings;
|
||||
private dbService: DBService;
|
||||
|
||||
async onload() {
|
||||
console.log("Loading MyWasmDBPlugin...");
|
||||
console.log("Loading SqliteDBPlugin...");
|
||||
await this.loadSettings();
|
||||
|
||||
this.dbService = new DBService();
|
||||
|
||||
//! i dont think we need this since we do it in each command
|
||||
// this.addCommand({
|
||||
// id: "open-db",
|
||||
// name: "Open Local SQLite DB",
|
||||
// callback: async () => {
|
||||
// await this.openDatabase();
|
||||
// },
|
||||
// });
|
||||
|
||||
this.addCommand({
|
||||
id: "open-wasm-db",
|
||||
name: "Open SQLite (WASM) DB and read a table",
|
||||
callback: async () => {
|
||||
await this.readDatabase();
|
||||
id: "inspect-table-structure",
|
||||
name: "Inspect table structure",
|
||||
editorCallback: async (editor: Editor, view: MarkdownView) => {
|
||||
// ensure DB is loaded (if not, load)
|
||||
await this.openDatabase();
|
||||
await inspectTableStructure(this.dbService, editor, this.app);
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "dump-table-to-notes",
|
||||
name: "Dump Table Rows to Notes",
|
||||
callback: async () => {
|
||||
await this.openDatabase(); // ensure DB is loaded
|
||||
|
||||
// 1) pick a table
|
||||
const chosenTable = await pickTableName(this.dbService, this.app);
|
||||
if (!chosenTable) {
|
||||
return; // user canceled or no tables
|
||||
}
|
||||
|
||||
// 2) Call the dump function with the chosen table
|
||||
await convertEntriesInNotes(this.dbService, chosenTable, this.app);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
this.addSettingTab(new MyWasmDBSettingTab(this.app, this));
|
||||
this.addSettingTab(new SqliteDBSettingTab(this.app, this));
|
||||
}
|
||||
|
||||
onunload() {
|
||||
console.log("Unloading MyWasmDBPlugin...");
|
||||
this.db = null;
|
||||
console.log("Unloading SqliteDBPlugin...");
|
||||
}
|
||||
|
||||
private async readDatabase() {
|
||||
const { dbFilePath } = this.settings;
|
||||
if (!dbFilePath) {
|
||||
new Notice("No DB path set in plugin settings.");
|
||||
private async openDatabase(forceReload = true) {
|
||||
const adapter = this.app.vault.adapter;
|
||||
if (!(adapter instanceof FileSystemAdapter)) {
|
||||
new Notice("This plugin only works with a local vault (FileSystemAdapter).");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1) Get the actual base path of the local vault, if we have a FileSystemAdapter
|
||||
let basePath: string | null = null;
|
||||
const adapter = this.app.vault.adapter;
|
||||
if (adapter instanceof FileSystemAdapter) {
|
||||
basePath = adapter.getBasePath(); // Now we can call getBasePath()
|
||||
}
|
||||
|
||||
if (!basePath) {
|
||||
new Notice("This plugin only works with a local vault (FileSystemAdapter).");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) Load the sql.js WASM module, telling it exactly where to find sql-wasm.wasm
|
||||
// Suppose we've placed sql-wasm.wasm in the same folder as main.js, i.e.
|
||||
// <Vault>/.obsidian/plugins/sqliteDB/sql-wasm.wasm
|
||||
const SQL = await initSqlJs({
|
||||
locateFile: (file) => {
|
||||
// "file" will typically be "sql-wasm.wasm"
|
||||
return `${basePath}/.obsidian/plugins/sqliteDB/${file}`;
|
||||
},
|
||||
});
|
||||
|
||||
// 3) Read file from disk
|
||||
const fileBuffer = readFileSync(dbFilePath);
|
||||
const uint8Array = new Uint8Array(fileBuffer);
|
||||
|
||||
// 4) Create the in-memory DB
|
||||
this.db = new SQL.Database(uint8Array);
|
||||
|
||||
// 5) Run a sample query
|
||||
const result = this.db.exec("SELECT name FROM sqlite_master WHERE type='table' LIMIT 1;");
|
||||
|
||||
if (result?.[0]?.values?.[0]?.[0]) {
|
||||
new Notice(`Found table: ${result[0].values[0][0]}`);
|
||||
} else {
|
||||
new Notice("Connected, but found no tables.");
|
||||
}
|
||||
|
||||
/* If you want to write changes back:
|
||||
this.db.run("CREATE TABLE test (col1 int);");
|
||||
const data = this.db.export(); // Uint8Array
|
||||
writeFileSync(dbFilePath, Buffer.from(data));
|
||||
*/
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
new Notice("Error reading DB: " + (err as Error).message);
|
||||
}
|
||||
const basePath = adapter.getBasePath();
|
||||
await this.dbService.ensureDBLoaded(this.settings, basePath, forceReload);
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
|
|
@ -109,10 +87,10 @@ export default class MyWasmDBPlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
class MyWasmDBSettingTab extends PluginSettingTab {
|
||||
plugin: MyWasmDBPlugin;
|
||||
class SqliteDBSettingTab extends PluginSettingTab {
|
||||
plugin: SqliteDBPlugin;
|
||||
|
||||
constructor(app: App, plugin: MyWasmDBPlugin) {
|
||||
constructor(app: App, plugin: SqliteDBPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
|
@ -121,14 +99,14 @@ class MyWasmDBSettingTab extends PluginSettingTab {
|
|||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
containerEl.createEl("h2", { text: "WASM DB Plugin Settings" });
|
||||
containerEl.createEl("h2", { text: "SQLite DB Plugin Settings" });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Database File Path")
|
||||
.setDesc("Absolute path to the .db file on disk.")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("C:\\path\\to\\your.db")
|
||||
.setPlaceholder("/home/user/path/to/your.db")
|
||||
.setValue(this.plugin.settings.dbFilePath)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.dbFilePath = value;
|
||||
|
|
|
|||
135
src/commands/convertEntriesInNotes.ts
Normal file
135
src/commands/convertEntriesInNotes.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import { App, Notice, TFile, TFolder, normalizePath, FileSystemAdapter } from "obsidian";
|
||||
import { DBService } from "../dbService";
|
||||
import { ColumnPickerModal } from "../components/ColumnPickerModal";
|
||||
|
||||
/**
|
||||
* Convert every row in the given table into a separate note.
|
||||
* - Prompts the user to pick a column for filenames.
|
||||
* - Creates a folder "<tableName>-dump" at the root of the vault.
|
||||
* - For each row, creates a .md file whose name is the chosen column's value.
|
||||
* - Inserts frontmatter with key:value pairs for each column.
|
||||
*
|
||||
* Usage:
|
||||
* await convertEntriesInNotes(dbService, "MyTable", this.app);
|
||||
*/
|
||||
export async function convertEntriesInNotes(dbService: DBService, tableName: string, app: App) {
|
||||
const db = dbService.getDB();
|
||||
if (!db) {
|
||||
new Notice("No DB loaded. Please open the DB first.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 1) Get list of columns from the table
|
||||
// PRAGMA table_info(tableName) gives columns metadata: cid, name, type, etc.
|
||||
const colResult = db.exec(`PRAGMA table_info('${tableName}');`);
|
||||
if (!colResult || colResult.length === 0) {
|
||||
new Notice(`Could not read columns for table '${tableName}'.`);
|
||||
return;
|
||||
}
|
||||
// colResult[0].values => array of rows, where row = [cid, name, type, ...]
|
||||
const columns: string[] = colResult[0].values.map(row => row[1] as string);
|
||||
if (!columns.length) {
|
||||
new Notice(`No columns found in table '${tableName}'.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) Prompt user to pick which column is used for the note filenames
|
||||
const pickColumn = async (): Promise<string | null> => {
|
||||
return new Promise((resolve) => {
|
||||
const modal = new ColumnPickerModal(app, columns, (col) => {
|
||||
resolve(col);
|
||||
});
|
||||
modal.open();
|
||||
});
|
||||
};
|
||||
|
||||
const chosenColumn = await pickColumn();
|
||||
if (!chosenColumn) {
|
||||
new Notice("No column chosen. Aborting.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3) Create or get the folder at the root: "<tableName>-dump"
|
||||
let basePath = "";
|
||||
if (app.vault.adapter instanceof FileSystemAdapter) {
|
||||
basePath = app.vault.adapter.getBasePath();
|
||||
} else {
|
||||
new Notice("This plugin only works in a local vault.");
|
||||
return;
|
||||
}
|
||||
const folderName = `${tableName}-dump`;
|
||||
const targetFolderPath = normalizePath(folderName);
|
||||
|
||||
// Check if folder already exists; if not, create it
|
||||
let folder = app.vault.getAbstractFileByPath(targetFolderPath);
|
||||
if (!folder) {
|
||||
folder = await app.vault.createFolder(targetFolderPath);
|
||||
} else if (!(folder instanceof TFolder)) {
|
||||
new Notice(`A file named "${folderName}" already exists and is not a folder.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 4) Query *all* rows from the table
|
||||
const allRowsResult = db.exec(`SELECT * FROM "${tableName}";`);
|
||||
if (!allRowsResult || allRowsResult.length === 0) {
|
||||
new Notice(`No rows found in table "${tableName}".`);
|
||||
return;
|
||||
}
|
||||
|
||||
const rowObj = allRowsResult[0]; // { columns: string[], values: any[][] }
|
||||
const allCols = rowObj.columns; // e.g. ["id", "title", "content", ...]
|
||||
const rows = rowObj.values; // array of arrays
|
||||
|
||||
// 5) Figure out which index corresponds to the "chosenColumn"
|
||||
const columnIndex = allCols.indexOf(chosenColumn);
|
||||
if (columnIndex < 0) {
|
||||
new Notice(`Chosen column '${chosenColumn}' not found in table.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 6) Loop each row, create a note
|
||||
let createdCount = 0;
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
// The chosen column's value for the filename
|
||||
let rawFileName = String(row[columnIndex]) || `Untitled_${i}`;
|
||||
let safeFileName = sanitizeFilename(rawFileName) || `Untitled_${i}`;
|
||||
let notePath = `${targetFolderPath}/${safeFileName}.md`;
|
||||
|
||||
// If file already exists, append a suffix
|
||||
let suffix = 1;
|
||||
while (app.vault.getAbstractFileByPath(notePath)) {
|
||||
suffix++;
|
||||
notePath = `${targetFolderPath}/${safeFileName}_${suffix}.md`;
|
||||
}
|
||||
|
||||
// Build frontmatter with all columns
|
||||
const frontmatterLines = ["---"];
|
||||
for (let c = 0; c < allCols.length; c++) {
|
||||
const colName = allCols[c];
|
||||
const val = row[c] == null ? "" : String(row[c]);
|
||||
// Escape special newlines or quotes if you want
|
||||
frontmatterLines.push(`${colName}: ${val.replace(/\n/g, "\\n")}`);
|
||||
}
|
||||
frontmatterLines.push("---\n");
|
||||
|
||||
const fileContent = frontmatterLines.join("\n");
|
||||
|
||||
try {
|
||||
await app.vault.create(notePath, fileContent);
|
||||
createdCount++;
|
||||
} catch (err) {
|
||||
console.error(`Failed to create file at ${notePath}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
new Notice(`Created ${createdCount} notes in folder "${folderName}".`);
|
||||
}
|
||||
|
||||
/**
|
||||
* A quick utility to remove forbidden filename characters on Windows, etc.
|
||||
*/
|
||||
function sanitizeFilename(name: string): string {
|
||||
// Replace slashes, backslashes, colons, etc.
|
||||
return name.replace(/[\\\/:*?"<>|]/g, "_").trim();
|
||||
}
|
||||
4
src/commands/index.ts
Normal file
4
src/commands/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { inspectTableStructure } from "./inspectTableStructure";
|
||||
import { convertEntriesInNotes } from "./convertEntriesInNotes";
|
||||
|
||||
export { inspectTableStructure, convertEntriesInNotes };
|
||||
49
src/commands/inspectTableStructure.ts
Normal file
49
src/commands/inspectTableStructure.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { Notice, Editor } from "obsidian";
|
||||
import { DBService } from "../dbService";
|
||||
import { TablePickerModal } from "../components/TablePickerModal";
|
||||
|
||||
/**
|
||||
* Show a table picker. On picking a table, query the first 10 rows and insert into the editor.
|
||||
*/
|
||||
export async function inspectTableStructure(
|
||||
dbService: DBService,
|
||||
editor: Editor,
|
||||
app: any
|
||||
) {
|
||||
const db = dbService.getDB();
|
||||
if (!db) {
|
||||
new Notice("No DB loaded. Please open the DB first.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 1) get all table names
|
||||
const result = db.exec("SELECT name FROM sqlite_master WHERE type='table';");
|
||||
if (!result || result.length === 0) {
|
||||
new Notice("No tables found in the DB.");
|
||||
return;
|
||||
}
|
||||
const tableNames = result[0].values.map((row) => row[0] as string);
|
||||
|
||||
// 2) show a modal to pick one
|
||||
const modal = new TablePickerModal(app, tableNames, (selectedTable) => {
|
||||
// 3) query first 10 rows from that table
|
||||
const rowResult = db.exec(`SELECT * FROM "${selectedTable}" LIMIT 10;`);
|
||||
if (!rowResult || rowResult.length === 0) {
|
||||
editor.replaceSelection(`\nNo rows found in table ${selectedTable}.\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
// rowResult[0].columns -> array of column names
|
||||
// rowResult[0].values -> array of row arrays
|
||||
const columns = rowResult[0].columns;
|
||||
const rows = rowResult[0].values;
|
||||
|
||||
let output = `\n**Columns in "${selectedTable}"**\n`;
|
||||
output += columns.join(", ");
|
||||
output += "\n\n**First row**\n";
|
||||
output += rows[0].join(", ");
|
||||
editor.replaceSelection(output + "\n");
|
||||
});
|
||||
|
||||
modal.open();
|
||||
}
|
||||
33
src/components/ColumnPickerModal.ts
Normal file
33
src/components/ColumnPickerModal.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { App, Notice } from "obsidian";
|
||||
|
||||
/**
|
||||
* A simple fuzzy modal to pick which column to use for filenames.
|
||||
*/
|
||||
import { FuzzySuggestModal } from "obsidian";
|
||||
|
||||
class ColumnPickerModal extends FuzzySuggestModal<string> {
|
||||
private columns: string[];
|
||||
private onChoose: (col: string) => void;
|
||||
|
||||
constructor(app: App, columns: string[], onChoose: (col: string) => void) {
|
||||
super(app);
|
||||
this.columns = columns;
|
||||
this.onChoose = onChoose;
|
||||
this.setPlaceholder("Pick a column to use as filenames...");
|
||||
}
|
||||
|
||||
getItems(): string[] {
|
||||
return this.columns;
|
||||
}
|
||||
|
||||
getItemText(item: string): string {
|
||||
return item;
|
||||
}
|
||||
|
||||
onChooseItem(item: string): void {
|
||||
new Notice(`Selected column: ${item}`);
|
||||
this.onChoose(item);
|
||||
}
|
||||
}
|
||||
|
||||
export { ColumnPickerModal };
|
||||
32
src/components/TablePickerModal.ts
Normal file
32
src/components/TablePickerModal.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { FuzzySuggestModal, Notice } from "obsidian";
|
||||
|
||||
/**
|
||||
* A modal that shows a list of table names. On choose, calls a callback.
|
||||
*/
|
||||
export class TablePickerModal extends FuzzySuggestModal<string> {
|
||||
private tables: string[];
|
||||
private onChoose: (tableName: string) => void;
|
||||
|
||||
constructor(app: any, tables: string[], onChoose: (tableName: string) => void) {
|
||||
super(app);
|
||||
this.tables = tables;
|
||||
this.onChoose = onChoose;
|
||||
this.setPlaceholder("Pick a table...");
|
||||
}
|
||||
|
||||
// The items to show in the modal
|
||||
getItems(): string[] {
|
||||
return this.tables;
|
||||
}
|
||||
|
||||
// How to display each item
|
||||
getItemText(item: string): string {
|
||||
return item;
|
||||
}
|
||||
|
||||
// Called when user selects an item
|
||||
onChooseItem(item: string, evt: MouseEvent | KeyboardEvent) {
|
||||
new Notice(`Selected table: ${item}`);
|
||||
this.onChoose(item);
|
||||
}
|
||||
}
|
||||
52
src/dbService.ts
Normal file
52
src/dbService.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { Notice, FileSystemAdapter } from "obsidian";
|
||||
import initSqlJs, { Database } from "sql.js";
|
||||
import { readFileSync } from "fs";
|
||||
import { SqliteDBSettings } from "./types";
|
||||
|
||||
/**
|
||||
* This class manages loading/reloading the SQLite DB using sql.js
|
||||
*/
|
||||
export class DBService {
|
||||
private db: Database | null = null;
|
||||
|
||||
/**
|
||||
* Ensure the DB is loaded. If it's not loaded yet, load it from disk.
|
||||
* If it is already loaded, do nothing.
|
||||
*
|
||||
* If `forceReload` is true, always reload from disk.
|
||||
*/
|
||||
async ensureDBLoaded(settings: SqliteDBSettings, basePath: string, forceReload = false) {
|
||||
if (!settings.dbFilePath) {
|
||||
new Notice("No DB path set in plugin settings.");
|
||||
return;
|
||||
}
|
||||
if (!this.db || forceReload) {
|
||||
try {
|
||||
const SQL = await initSqlJs({
|
||||
locateFile: (file) => `${basePath}/.obsidian/plugins/sqliteDB/${file}`,
|
||||
});
|
||||
const fileBuffer = readFileSync(settings.dbFilePath);
|
||||
const uint8Array = new Uint8Array(fileBuffer);
|
||||
this.db = new SQL.Database(uint8Array);
|
||||
|
||||
// Check if we have at least one table
|
||||
const result = this.db.exec("SELECT name FROM sqlite_master WHERE type='table' LIMIT 1;");
|
||||
if (result?.[0]?.values?.[0]?.[0]) {
|
||||
new Notice(`DB Loaded.`);
|
||||
} else {
|
||||
new Notice("DB Loaded, but found no tables.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
new Notice("Error reading DB: " + (err as Error).message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current Database instance, or null if not loaded.
|
||||
*/
|
||||
getDB(): Database | null {
|
||||
return this.db;
|
||||
}
|
||||
}
|
||||
3
src/helpers/index.ts
Normal file
3
src/helpers/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { pickTableName } from "./pickTableName";
|
||||
|
||||
export { pickTableName };
|
||||
38
src/helpers/pickTableName.ts
Normal file
38
src/helpers/pickTableName.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { App, Notice } from "obsidian";
|
||||
import { DBService } from "../dbService";
|
||||
import { TablePickerModal } from "../components/TablePickerModal";
|
||||
|
||||
/**
|
||||
* Prompt the user to pick a table from sqlite_master.
|
||||
* Returns the chosen table name, or null if none.
|
||||
*/
|
||||
export async function pickTableName(dbService: DBService, app: App): Promise<string | null> {
|
||||
const db = dbService.getDB();
|
||||
if (!db) {
|
||||
new Notice("No DB loaded. Please open the DB first.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 1) Get all tables
|
||||
const result = db.exec("SELECT name FROM sqlite_master WHERE type='table';");
|
||||
if (!result || result.length === 0) {
|
||||
new Notice("No tables found in the DB.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// each row is [tableName]
|
||||
const tableNames = result[0].values.map((row) => row[0] as string);
|
||||
|
||||
if (!tableNames.length) {
|
||||
new Notice("No tables found in the DB.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2) Show a fuzzy modal to let the user pick one
|
||||
return new Promise((resolve) => {
|
||||
const modal = new TablePickerModal(app, tableNames, (chosen) => {
|
||||
resolve(chosen);
|
||||
});
|
||||
modal.open();
|
||||
});
|
||||
}
|
||||
7
src/types.ts
Normal file
7
src/types.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export interface SqliteDBSettings {
|
||||
dbFilePath: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: SqliteDBSettings = {
|
||||
dbFilePath: "",
|
||||
};
|
||||
Loading…
Reference in a new issue