mirror of
https://github.com/h-sphere/sql-seal.git
synced 2026-07-22 10:10:28 +00:00
adding ability to refresh the data when datasource changes
This commit is contained in:
parent
130c5fd16e
commit
cd159d0911
6 changed files with 145 additions and 109 deletions
|
|
@ -4,65 +4,15 @@ export class SealObserver {
|
|||
private tables: Map<string, Set<Callback>>;
|
||||
private verbose: boolean;
|
||||
|
||||
private files: Map<string, Set<string>>;
|
||||
private tags: Map<string, Set<Callback>>;
|
||||
|
||||
constructor(verbose = false) {
|
||||
this.tables = new Map();
|
||||
this.files = new Map();
|
||||
this.tags = new Map();
|
||||
this.verbose = verbose;
|
||||
}
|
||||
|
||||
registerFile(fileName: string) {
|
||||
if (!this.files.has(fileName)) {
|
||||
this.files.set(fileName, new Set<string>());
|
||||
if (this.verbose) {
|
||||
console.log(`File "${fileName}" registered.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerTableToFile(tableName: string, fileName: string) {
|
||||
if (!this.files.has(fileName)) {
|
||||
this.files.set(fileName, new Set<string>());
|
||||
if (this.verbose) {
|
||||
console.log(`File "${fileName}" registered.`);
|
||||
}
|
||||
}
|
||||
const tables = this.files.get(fileName);
|
||||
if (tables !== undefined) {
|
||||
tables.add(tableName);
|
||||
if (this.verbose) {
|
||||
console.log(`Table "${tableName}" registered to file "${fileName}".`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerElementToFile(elementName: string, fileName: string) {
|
||||
if (!this.files.has(fileName)) {
|
||||
this.files.set(fileName, new Set<string>());
|
||||
if (this.verbose) {
|
||||
console.log(`File "${fileName}" registered.`);
|
||||
}
|
||||
}
|
||||
const elements = this.files.get(fileName);
|
||||
if (elements !== undefined) {
|
||||
elements.add(elementName);
|
||||
if (this.verbose) {
|
||||
console.log(`Element "${elementName}" registered to file "${fileName}".`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerTable(tableName: string) {
|
||||
if (!this.tables.has(tableName)) {
|
||||
this.tables.set(tableName, new Set<Callback>());
|
||||
if (this.verbose) {
|
||||
console.log(`Table "${tableName}" registered.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerObserver(tableNames: string | string[], observer: Callback) {
|
||||
registerObserver(tableNames: string | string[], observer: Callback, tag?: string) {
|
||||
if (typeof tableNames === 'string') {
|
||||
tableNames = [tableNames];
|
||||
}
|
||||
|
|
@ -80,9 +30,30 @@ export class SealObserver {
|
|||
console.log(`Observer registered for table "${tableName}".`);
|
||||
}
|
||||
}
|
||||
if (tag) {
|
||||
if (!this.tags.has(tag)) {
|
||||
this.tags.set(tag, new Set<Callback>());
|
||||
}
|
||||
const tags = this.tags.get(tag);
|
||||
if (tags !== undefined) {
|
||||
tags.add(observer);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
unregisterObserversByTag(tag: string) {
|
||||
if (this.tags.has(tag)) {
|
||||
const observers = this.tags.get(tag);
|
||||
if (observers) {
|
||||
observers.forEach(observer => {
|
||||
this.unregisterObserver(observer)
|
||||
})
|
||||
this.tags.delete(tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unregisterObserver(observer: Callback) {
|
||||
this.tables.forEach(observers => {
|
||||
observers.delete(observer);
|
||||
|
|
@ -103,21 +74,4 @@ export class SealObserver {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fireFilenameObservers(fileName: string) {
|
||||
if (this.files.has(fileName)) {
|
||||
const tables = this.files.get(fileName);
|
||||
if (tables) {
|
||||
tables.forEach(tableName => {
|
||||
this.fireObservers(tableName);
|
||||
});
|
||||
}
|
||||
}
|
||||
this.tables.forEach(observers => {
|
||||
observers.forEach(observer => observer());
|
||||
});
|
||||
if (this.verbose) {
|
||||
console.log(`Observers fired for file "${fileName}".`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import path from 'path'
|
|||
import Papa from 'papaparse'
|
||||
import { prefixedIfNotGlobal } from "./sqlReparseTables"
|
||||
import fs from 'fs'
|
||||
import { fetchBlobData } from "./utils"
|
||||
import { delay, fetchBlobData } from "./utils"
|
||||
|
||||
function isNumeric(str: string) {
|
||||
if (typeof str != "string") return false // we only process strings!
|
||||
|
|
@ -57,8 +57,6 @@ const predictType = (field: string, data: Array<Record<string, string>>) => {
|
|||
return 'TEXT'
|
||||
}
|
||||
|
||||
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
||||
export class SqlSealDatabase {
|
||||
private savedDatabases: Record<string, any> = {}
|
||||
db: typeof Database
|
||||
|
|
@ -96,7 +94,7 @@ export class SqlSealDatabase {
|
|||
}
|
||||
console.log('FETCHED')
|
||||
|
||||
await delay(1000) // Making sure everything is in order
|
||||
await delay(1000) // Making sure everything is in order: ;
|
||||
|
||||
//@ts-ignore
|
||||
const defaultDbPath = path.resolve(this.app.vault.adapter.basePath, this.app.vault.configDir, "obsidian.db")
|
||||
|
|
@ -173,17 +171,8 @@ export class SqlSealDatabase {
|
|||
return types;
|
||||
}
|
||||
|
||||
async defineDatabaseFromUrl(unprefixedName: string, url: string, prefix: string) {
|
||||
const name = prefixedIfNotGlobal(unprefixedName, [], prefix)
|
||||
if (this.savedDatabases[name]) {
|
||||
console.log('Database Exists', name)
|
||||
return
|
||||
}
|
||||
async loadDataForDatabaseFromUrl(name: string, url: string) {
|
||||
const file = this.app.vault.getFileByPath(url)
|
||||
if (!file) {
|
||||
console.log('File not found')
|
||||
return
|
||||
}
|
||||
const data = await this.app.vault.cachedRead(file)
|
||||
|
||||
const parsed = Papa.parse(data, {
|
||||
|
|
@ -194,9 +183,6 @@ export class SqlSealDatabase {
|
|||
const fields = parsed.meta.fields
|
||||
const { data: parsedData, types } = toTypeStatements(fields, parsed.data)
|
||||
|
||||
await this.createTable(name, types)
|
||||
// this.savedDatabases[name] = url
|
||||
|
||||
// Purge the database
|
||||
await this.db.prepare(`DELETE FROM ${name}`).run()
|
||||
|
||||
|
|
@ -213,4 +199,36 @@ export class SqlSealDatabase {
|
|||
|
||||
await insertMany(parsedData)
|
||||
}
|
||||
|
||||
async defineDatabaseFromUrl(unprefixedName: string, url: string, prefix: string, reloadData: boolean = false) {
|
||||
const name = prefixedIfNotGlobal(unprefixedName, [], prefix)
|
||||
if (this.savedDatabases[name]) {
|
||||
console.log('Database Exists', name)
|
||||
if (reloadData) {
|
||||
await this.loadDataForDatabaseFromUrl(name, url)
|
||||
}
|
||||
return name
|
||||
}
|
||||
const file = this.app.vault.getFileByPath(url)
|
||||
if (!file) {
|
||||
console.log('File not found')
|
||||
return name
|
||||
}
|
||||
const data = await this.app.vault.cachedRead(file)
|
||||
|
||||
const parsed = Papa.parse(data, {
|
||||
header: true,
|
||||
dynamicTyping: false,
|
||||
skipEmptyLines: true
|
||||
})
|
||||
const fields = parsed.meta.fields
|
||||
const { data: parsedData, types } = toTypeStatements(fields, parsed.data)
|
||||
|
||||
await this.createTable(name, types)
|
||||
// this.savedDatabases[name] = url
|
||||
await this.loadDataForDatabaseFromUrl(name, url)
|
||||
|
||||
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { BaseFrom, Parser, Select } from "node-sql-parser"
|
||||
import { generatePrefix } from "./hash"
|
||||
import { table } from "console"
|
||||
|
||||
const isGlobal = (table: string, globalTables: string[]) => {
|
||||
return globalTables.map(t =>
|
||||
|
|
@ -21,20 +22,29 @@ export const prefixedIfNotGlobal = (tableName: string, globalTables: string[], p
|
|||
|
||||
const updateSelect = (selectAst: Select, globalTables: string[], prefix: string) => {
|
||||
if (!selectAst.from) {
|
||||
return selectAst
|
||||
return { selectAst: selectAst, tables: [] }
|
||||
}
|
||||
return {
|
||||
...selectAst,
|
||||
from: selectAst.from.map(from => {
|
||||
// FIXME: better typing here.
|
||||
if(isBaseFrom(from)) {
|
||||
return {
|
||||
...from,
|
||||
table: prefixedIfNotGlobal(from.table, globalTables, prefix)
|
||||
}
|
||||
|
||||
const tables: Array<string> = []
|
||||
|
||||
const updatedFrom = selectAst.from.map(from => {
|
||||
if(isBaseFrom(from)) {
|
||||
const t = prefixedIfNotGlobal(from.table, globalTables, prefix)
|
||||
tables.push(t)
|
||||
return {
|
||||
...from,
|
||||
table: t
|
||||
}
|
||||
return from
|
||||
})
|
||||
}
|
||||
return from
|
||||
})
|
||||
|
||||
return {
|
||||
selectAst: {
|
||||
...selectAst,
|
||||
from: updatedFrom
|
||||
},
|
||||
tables
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -42,8 +52,8 @@ export const updateTables = (selectStatement: string, globalTables: string[], pr
|
|||
const parser = new Parser()
|
||||
const { ast } = parser.parse(selectStatement)
|
||||
if (!Array.isArray(ast) && ast.type === 'select') {
|
||||
const updated = updateSelect(ast!, globalTables, prefix)
|
||||
return parser.sqlify(updated)
|
||||
const { selectAst, tables } = updateSelect(ast!, globalTables, prefix)
|
||||
return { statement: parser.sqlify(selectAst), tables }
|
||||
} else {
|
||||
throw new Error('Invalid Statement. Only single SELECTs are accepted at the moment.')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,16 @@ import { displayData, displayError, displayInfo, displayLoader } from "./ui";
|
|||
import { App, MarkdownPostProcessorContext } from "obsidian";
|
||||
import { updateTables } from "./sqlReparseTables";
|
||||
import { hashString } from "./hash";
|
||||
import { SealObserver } from "./SealObserver";
|
||||
import { delay } from "./utils";
|
||||
|
||||
export class SqlSeal {
|
||||
public db: SqlSealDatabase
|
||||
private observer: SealObserver
|
||||
constructor(private readonly app: App, verbose = false) {
|
||||
this.db = new SqlSealDatabase(app, verbose)
|
||||
this.observer = new SealObserver(verbose)
|
||||
this.observeAllFileChanges()
|
||||
}
|
||||
|
||||
async connect() {
|
||||
|
|
@ -30,12 +35,21 @@ export class SqlSeal {
|
|||
return frontmatter
|
||||
}
|
||||
|
||||
|
||||
private observeAllFileChanges() {
|
||||
// Use fs to observe file changes
|
||||
this.app.vault.on('modify', async (file) => {
|
||||
console.log('Firing observers for file:', file.path)
|
||||
this.observer.fireObservers('file:' + file.path)
|
||||
})
|
||||
}
|
||||
|
||||
getHandler() {
|
||||
return async (source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) => {
|
||||
|
||||
displayLoader(el)
|
||||
await this.connect()
|
||||
|
||||
|
||||
const frontmatter = await this.resolveFrontmatter(ctx)
|
||||
const prefix = hashString(ctx.sourcePath)
|
||||
const regex = /TABLE\s+(.+)\s+=\s+file\(([^)]+)\)/g;
|
||||
|
|
@ -43,7 +57,15 @@ export class SqlSeal {
|
|||
while ((match = regex.exec(source)) !== null) {
|
||||
const name = match[1];
|
||||
const url = match[2];
|
||||
this.db.defineDatabaseFromUrl(name, url, prefix)
|
||||
const prefixedName = await this.db.defineDatabaseFromUrl(name, url, prefix)
|
||||
this.observer.registerObserver(`file:${url}`, async () => {
|
||||
console.log('File was changes and we need to update related table: ', name)
|
||||
// Update table
|
||||
await this.db.defineDatabaseFromUrl(name, url, prefix, true)
|
||||
|
||||
// Fire observers for the table
|
||||
this.observer.fireObservers(`table:${prefixedName}`)
|
||||
}, ctx.docId)
|
||||
}
|
||||
|
||||
const selectRegexp = /SELECT\s+(.*)/g;
|
||||
|
|
@ -51,11 +73,40 @@ export class SqlSeal {
|
|||
if (selectMatch) {
|
||||
try {
|
||||
const selectStatement = selectMatch[0]
|
||||
const selectUpdated = updateTables(selectStatement, ['files'], prefix)
|
||||
const stmt = await this.db.db.prepare(selectUpdated)
|
||||
const columns = await stmt.columns().map(column => column.name);
|
||||
const data = await stmt.all(frontmatter)
|
||||
displayData(el, columns, data)
|
||||
const { statement, tables } = updateTables(selectStatement, ['files'], prefix)
|
||||
|
||||
|
||||
const renderSelect = async () => {
|
||||
const stmt = await this.db.db.prepare(statement)
|
||||
const columns = await stmt.columns().map(column => column.name);
|
||||
const data = await stmt.all(frontmatter)
|
||||
displayData(el, columns, data)
|
||||
}
|
||||
|
||||
// Register observer for each table
|
||||
tables.forEach(table => {
|
||||
const observer = async () => {
|
||||
console.log('Table was changes and we need to update related select: ', table)
|
||||
|
||||
// FIXME: check if element is still in the DOM.
|
||||
console.log(el, el.isShown(), el.isConnected)
|
||||
|
||||
if (!el.isConnected) {
|
||||
// Unregistering using the context id
|
||||
this.observer.unregisterObserversByTag(ctx.docId)
|
||||
}
|
||||
|
||||
|
||||
displayLoader(el)
|
||||
// await delay(1000)
|
||||
|
||||
await renderSelect()
|
||||
}
|
||||
this.observer.registerObserver(`table:${table}`, observer, ctx.docId)
|
||||
})
|
||||
|
||||
await renderSelect()
|
||||
|
||||
} catch (e) {
|
||||
if (e instanceof RangeError && Object.keys(ctx.frontmatter).length === 0) {
|
||||
displayInfo(el, 'Cannot access frontmatter properties in Live Preview Mode. Switch to Reading Mode to see the results.')
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export const displayInfo = (el: HTMLElement, message: string) => {
|
|||
}
|
||||
|
||||
export const displayLoader = (el: HTMLElement) => {
|
||||
el.empty()
|
||||
const loader = el.createEl("div", { cls: 'callout', text: 'Loading SQLSeal database...' })
|
||||
loader.dataset.callout = 'info'
|
||||
}
|
||||
|
|
@ -50,4 +50,6 @@ export const fetchBlobData = async (url: string, filePath: string) => {
|
|||
reject(`Error writing to ${filePath}: ${err}`)
|
||||
});
|
||||
})
|
||||
};
|
||||
};
|
||||
|
||||
export const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
||||
Loading…
Reference in a new issue