mirror of
https://github.com/h-sphere/sql-seal.git
synced 2026-07-22 10:10:28 +00:00
added docs, testing observers
This commit is contained in:
parent
c92fc85cc3
commit
0e57c7bda2
22 changed files with 8200 additions and 5130 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -20,3 +20,6 @@ data.json
|
|||
|
||||
# Exclude macOS Finder (System Explorer) View States
|
||||
.DS_Store
|
||||
docs/.vitepress/dist
|
||||
docs/.vitepress/cache
|
||||
rebuild
|
||||
|
|
|
|||
31
docs/.vitepress/config.mts
Normal file
31
docs/.vitepress/config.mts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { defineConfig } from 'vitepress'
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
title: "Obsidian SQLSeal",
|
||||
description: "Plugin enabling full SQL capabilities in Obsidian",
|
||||
themeConfig: {
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
nav: [
|
||||
{ text: 'Home', link: '/' },
|
||||
{ text: 'Quick Start', link: '/quick-start' }
|
||||
],
|
||||
|
||||
sidebar: [
|
||||
{
|
||||
text: 'Documentation',
|
||||
items: [
|
||||
{ text: 'Getting Started', link: '/getting-started' },
|
||||
{ text: 'Quick Start', link: '/quick-start' },
|
||||
{ text: 'Using properties', link: '/using-properties' },
|
||||
{ text: 'Future Plans', link: '/future-plans' }
|
||||
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
socialLinks: [
|
||||
{ icon: 'github', link: 'https://github.com/vuejs/vitepress' }
|
||||
]
|
||||
}
|
||||
})
|
||||
17
docs/.vitepress/theme/index.ts
Normal file
17
docs/.vitepress/theme/index.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// https://vitepress.dev/guide/custom-theme
|
||||
import { h } from 'vue'
|
||||
import type { Theme } from 'vitepress'
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import './style.css'
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
Layout: () => {
|
||||
return h(DefaultTheme.Layout, null, {
|
||||
// https://vitepress.dev/guide/extending-default-theme#layout-slots
|
||||
})
|
||||
},
|
||||
enhanceApp({ app, router, siteData }) {
|
||||
// ...
|
||||
}
|
||||
} satisfies Theme
|
||||
139
docs/.vitepress/theme/style.css
Normal file
139
docs/.vitepress/theme/style.css
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
/**
|
||||
* Customize default theme styling by overriding CSS variables:
|
||||
* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
|
||||
*/
|
||||
|
||||
/**
|
||||
* Colors
|
||||
*
|
||||
* Each colors have exact same color scale system with 3 levels of solid
|
||||
* colors with different brightness, and 1 soft color.
|
||||
*
|
||||
* - `XXX-1`: The most solid color used mainly for colored text. It must
|
||||
* satisfy the contrast ratio against when used on top of `XXX-soft`.
|
||||
*
|
||||
* - `XXX-2`: The color used mainly for hover state of the button.
|
||||
*
|
||||
* - `XXX-3`: The color for solid background, such as bg color of the button.
|
||||
* It must satisfy the contrast ratio with pure white (#ffffff) text on
|
||||
* top of it.
|
||||
*
|
||||
* - `XXX-soft`: The color used for subtle background such as custom container
|
||||
* or badges. It must satisfy the contrast ratio when putting `XXX-1` colors
|
||||
* on top of it.
|
||||
*
|
||||
* The soft color must be semi transparent alpha channel. This is crucial
|
||||
* because it allows adding multiple "soft" colors on top of each other
|
||||
* to create a accent, such as when having inline code block inside
|
||||
* custom containers.
|
||||
*
|
||||
* - `default`: The color used purely for subtle indication without any
|
||||
* special meanings attched to it such as bg color for menu hover state.
|
||||
*
|
||||
* - `brand`: Used for primary brand colors, such as link text, button with
|
||||
* brand theme, etc.
|
||||
*
|
||||
* - `tip`: Used to indicate useful information. The default theme uses the
|
||||
* brand color for this by default.
|
||||
*
|
||||
* - `warning`: Used to indicate warning to the users. Used in custom
|
||||
* container, badges, etc.
|
||||
*
|
||||
* - `danger`: Used to show error, or dangerous message to the users. Used
|
||||
* in custom container, badges, etc.
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-c-default-1: var(--vp-c-gray-1);
|
||||
--vp-c-default-2: var(--vp-c-gray-2);
|
||||
--vp-c-default-3: var(--vp-c-gray-3);
|
||||
--vp-c-default-soft: var(--vp-c-gray-soft);
|
||||
|
||||
--vp-c-brand-1: var(--vp-c-indigo-1);
|
||||
--vp-c-brand-2: var(--vp-c-indigo-2);
|
||||
--vp-c-brand-3: var(--vp-c-indigo-3);
|
||||
--vp-c-brand-soft: var(--vp-c-indigo-soft);
|
||||
|
||||
--vp-c-tip-1: var(--vp-c-brand-1);
|
||||
--vp-c-tip-2: var(--vp-c-brand-2);
|
||||
--vp-c-tip-3: var(--vp-c-brand-3);
|
||||
--vp-c-tip-soft: var(--vp-c-brand-soft);
|
||||
|
||||
--vp-c-warning-1: var(--vp-c-yellow-1);
|
||||
--vp-c-warning-2: var(--vp-c-yellow-2);
|
||||
--vp-c-warning-3: var(--vp-c-yellow-3);
|
||||
--vp-c-warning-soft: var(--vp-c-yellow-soft);
|
||||
|
||||
--vp-c-danger-1: var(--vp-c-red-1);
|
||||
--vp-c-danger-2: var(--vp-c-red-2);
|
||||
--vp-c-danger-3: var(--vp-c-red-3);
|
||||
--vp-c-danger-soft: var(--vp-c-red-soft);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Button
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-button-brand-border: transparent;
|
||||
--vp-button-brand-text: var(--vp-c-white);
|
||||
--vp-button-brand-bg: var(--vp-c-brand-3);
|
||||
--vp-button-brand-hover-border: transparent;
|
||||
--vp-button-brand-hover-text: var(--vp-c-white);
|
||||
--vp-button-brand-hover-bg: var(--vp-c-brand-2);
|
||||
--vp-button-brand-active-border: transparent;
|
||||
--vp-button-brand-active-text: var(--vp-c-white);
|
||||
--vp-button-brand-active-bg: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Home
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-home-hero-name-color: transparent;
|
||||
--vp-home-hero-name-background: -webkit-linear-gradient(
|
||||
120deg,
|
||||
#bd34fe 30%,
|
||||
#41d1ff
|
||||
);
|
||||
|
||||
--vp-home-hero-image-background-image: linear-gradient(
|
||||
-45deg,
|
||||
#bd34fe 50%,
|
||||
#47caff 50%
|
||||
);
|
||||
--vp-home-hero-image-filter: blur(44px);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
:root {
|
||||
--vp-home-hero-image-filter: blur(56px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
:root {
|
||||
--vp-home-hero-image-filter: blur(68px);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Custom Block
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-custom-block-tip-border: transparent;
|
||||
--vp-custom-block-tip-text: var(--vp-c-text-1);
|
||||
--vp-custom-block-tip-bg: var(--vp-c-brand-soft);
|
||||
--vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Algolia
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
.DocSearch {
|
||||
--docsearch-primary-color: var(--vp-c-brand-1) !important;
|
||||
}
|
||||
|
||||
5
docs/future-plans.md
Normal file
5
docs/future-plans.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Future Plans
|
||||
In the short future the following features are planned for SQLSeal:
|
||||
- Global tables: currently the tables are scoped to specific file to prevent name clashing. We plan to add settings panel to allow for setting up global tables
|
||||
- Views: Allow to store your selects as views and query them.
|
||||
- Dataview: Support for dataview data to be imported into SQLSeal.
|
||||
55
docs/getting-started.md
Normal file
55
docs/getting-started.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# Getting Started
|
||||
|
||||
|
||||
|
||||
## Custom Containers
|
||||
|
||||
**Input**
|
||||
|
||||
```md
|
||||
::: info
|
||||
This is an info box.
|
||||
:::
|
||||
|
||||
::: tip
|
||||
This is a tip.
|
||||
:::
|
||||
|
||||
::: warning
|
||||
This is a warning.
|
||||
:::
|
||||
|
||||
::: danger
|
||||
This is a dangerous warning.
|
||||
:::
|
||||
|
||||
::: details
|
||||
This is a details block.
|
||||
:::
|
||||
```
|
||||
|
||||
**Output**
|
||||
|
||||
::: info
|
||||
This is an info box.
|
||||
:::
|
||||
|
||||
::: tip
|
||||
This is a tip.
|
||||
:::
|
||||
|
||||
::: warning
|
||||
This is a warning.
|
||||
:::
|
||||
|
||||
::: danger
|
||||
This is a dangerous warning.
|
||||
:::
|
||||
|
||||
::: details
|
||||
This is a details block.
|
||||
:::
|
||||
|
||||
## More
|
||||
|
||||
Check out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown).
|
||||
28
docs/index.md
Normal file
28
docs/index.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
# https://vitepress.dev/reference/default-theme-home-page
|
||||
layout: home
|
||||
|
||||
hero:
|
||||
name: "Obsidian SQLSeal"
|
||||
# text: "Plugin enabling full SQL capabilities in Obsidian"
|
||||
tagline: "Plugin enabling full SQL capabilities in Obsidian"
|
||||
image:
|
||||
src: ./seal.png
|
||||
alt: SQL Seal Logo (In Progress)
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Markdown Examples
|
||||
link: /getting-started
|
||||
- theme: alt
|
||||
text: API Examples
|
||||
link: /api-examples
|
||||
|
||||
features:
|
||||
- title: Query data in your vault
|
||||
details: Use full power of SQL to select, join, filter data for your liking
|
||||
- title: Fully featured SQL Engine
|
||||
details: With SQLite under the hood, you can use all functionality of the database
|
||||
- title: Zero configuration
|
||||
details: Just use SQL syntax to create and query the data
|
||||
---
|
||||
|
||||
34
docs/quick-start.md
Normal file
34
docs/quick-start.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Quick Start
|
||||
In this guide you will learn:
|
||||
- How to install SQLSeal in your vault
|
||||
- How to save CSV and show it in the file explorer
|
||||
- How to create your first query
|
||||
|
||||
## Installing SQLSeal
|
||||
The easiest way to install SQLSeal is to download it from Community Plugins. Go to Settings -> Community Plugins and click on Browse button. Search for SQLSeal, install it and enable.
|
||||
|
||||
### Manual instalation
|
||||
To install it manually clone the project:
|
||||
```bash
|
||||
git clone <URL>
|
||||
```
|
||||
|
||||
## Save CSV in Obsidian
|
||||
Obsidian does not natively support CSV and it hides them in the file explorer. To enable CSVs being shown on the sidebar, you need to enable "Detect all file extensions" in "Files and Links" setting panel. Then save the following CSV in your vault: [transactions.csv](./transactions.csv)
|
||||
|
||||
|
||||
## Querying the data
|
||||
To query the data simply create the codeblock in `sqlseal` language. SQLSeal language consists of the following:
|
||||
- 0 or more `TABLE` command that define your tables
|
||||
- Standard SQL SELECT query
|
||||
|
||||
The best is to use the following example:
|
||||
|
||||
```sqlseal
|
||||
TABLE transactions = file(transactions.csv)
|
||||
|
||||
SELECT name, value FROM transactions
|
||||
```
|
||||
|
||||
The code above creates table `transactions` based on the csv file provided and selects all names and values from it.
|
||||
|
||||
BIN
docs/seal.png
Normal file
BIN
docs/seal.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 139 KiB |
5
docs/transactions.csv
Normal file
5
docs/transactions.csv
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
id,name,customer,value
|
||||
1,Bread,Mark,3.45
|
||||
2,Milk,Mark,2.43
|
||||
3,Press,Linda,4.55,
|
||||
4,Cheese,Mark,3.22
|
||||
|
7
docs/using-properties.md
Normal file
7
docs/using-properties.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Using properties
|
||||
You can use properties to customise your queries and make them dynamic. This can be helpful if you plan to reuse your queries accross multiple files or use them in the templates.
|
||||
|
||||
You can refer to your properties in all 3 ways SQLite allows for it:
|
||||
- `@` symbol, for example `@year`
|
||||
- `?` symbol, for example `?date`
|
||||
- `:` symbol, for example `:category`
|
||||
26
main.ts
26
main.ts
|
|
@ -1,4 +1,5 @@
|
|||
import { MarkdownRenderer, MarkdownView, Plugin } from 'obsidian';
|
||||
import { SealFileSync } from 'src/SealFileSync';
|
||||
import { SqlSealSettings, SqlSealSettingsTab } from 'src/settings';
|
||||
import { SqlSeal } from 'src/sqlSeal';
|
||||
|
||||
|
|
@ -9,25 +10,18 @@ export default class SqlSealPlugin extends Plugin {
|
|||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
const sqlSeal = new SqlSeal(this.app, true)
|
||||
const sqlSeal = new SqlSeal(this.app, false)
|
||||
this.registerMarkdownCodeBlockProcessor("sqlseal", sqlSeal.getHandler())
|
||||
|
||||
// or onload
|
||||
this.app.workspace.iterateAllLeaves((leaf) => {
|
||||
if (leaf.view && leaf.view instanceof MarkdownView) {
|
||||
const editorView = (leaf.view.editor as any).cm;
|
||||
console.log('EDITOR VIEW', editorView)
|
||||
// ...
|
||||
}
|
||||
})
|
||||
// })
|
||||
// MarkdownRenderer.registerLanguage({
|
||||
// id: "sqlseal",
|
||||
// codeMirrorMode: "sql",
|
||||
// name: "sqlseal",
|
||||
// });
|
||||
// const fileSync = new SealFileSync(this.app, sqlSeal)
|
||||
// setTimeout(() => {
|
||||
// fileSync.init()
|
||||
// }, 5000)
|
||||
|
||||
// this.addSettingTab(new SqlSealSettingsTab(this.app, this));
|
||||
|
||||
|
||||
|
||||
this.addSettingTab(new SqlSealSettingsTab(this.app, this));
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"id": "hypersphere-sqlseal",
|
||||
"name": "SQL Seal",
|
||||
"version": "1.0.0",
|
||||
"version": "0.1.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Demonstrates some of the capabilities of the Obsidian API.",
|
||||
"author": "Obsidian",
|
||||
"authorUrl": "https://obsidian.md",
|
||||
"fundingUrl": "https://obsidian.md/pricing",
|
||||
"description": "Use SQL in your notes!",
|
||||
"author": "hypersphere",
|
||||
"authorUrl": "https://hypersphere.blog/",
|
||||
"fundingUrl": "https://hypersphere.blog",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
|
|
|
|||
11357
package-lock.json
generated
11357
package-lock.json
generated
File diff suppressed because it is too large
Load diff
79
package.json
79
package.json
|
|
@ -1,39 +1,44 @@
|
|||
{
|
||||
"name": "obsidian-sample-plugin",
|
||||
"version": "1.0.0",
|
||||
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json",
|
||||
"rebuild": "electron-rebuild -f -w better-sqlite3"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.10",
|
||||
"@types/node": "^16.11.6",
|
||||
"@types/papaparse": "^5.3.14",
|
||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
"@typescript-eslint/parser": "5.29.0",
|
||||
"builtin-modules": "3.3.0",
|
||||
"electron": "^25.0.0",
|
||||
"electron-rebuild": "^3.2.9",
|
||||
"esbuild": "0.17.3",
|
||||
"esbuild-plugin-replace": "^1.4.0",
|
||||
"esbuild-plugin-replace-regex": "^0.0.2",
|
||||
"obsidian": "latest",
|
||||
"prettier": "3.2.5",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^9.5.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"node-sql-parser": "^5.0.0",
|
||||
"papaparse": "^5.4.1",
|
||||
"sqlite3": "^5.1.7"
|
||||
}
|
||||
"name": "obsidian-sample-plugin",
|
||||
"version": "1.0.0",
|
||||
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json",
|
||||
"rebuild": "electron-rebuild -f -w better-sqlite3",
|
||||
"docs:dev": "vitepress dev docs",
|
||||
"docs:build": "vitepress build docs",
|
||||
"docs:preview": "vitepress preview docs"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.10",
|
||||
"@types/node": "^16.11.6",
|
||||
"@types/papaparse": "^5.3.14",
|
||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
"@typescript-eslint/parser": "5.29.0",
|
||||
"builtin-modules": "3.3.0",
|
||||
"electron": "^25.0.0",
|
||||
"electron-rebuild": "^3.2.9",
|
||||
"esbuild": "0.17.3",
|
||||
"esbuild-plugin-replace": "^1.4.0",
|
||||
"esbuild-plugin-replace-regex": "^0.0.2",
|
||||
"obsidian": "latest",
|
||||
"prettier": "3.2.5",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4",
|
||||
"vitepress": "^1.1.4",
|
||||
"vue": "^3.4.26"
|
||||
},
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^9.5.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"node-sql-parser": "^5.0.0",
|
||||
"papaparse": "^5.4.1",
|
||||
"sqlite3": "^5.1.7"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
56
src/SealFileSync.ts
Normal file
56
src/SealFileSync.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { App, TAbstractFile } from "obsidian";
|
||||
import GrayMatter from "gray-matter";
|
||||
import { SqlSeal } from "./sqlSeal";
|
||||
|
||||
|
||||
|
||||
async function extractFrontmatterFromFile(file: TAbstractFile) {
|
||||
const content = await this.app.vault.read(file);
|
||||
const gm = GrayMatter(content)
|
||||
return gm.data
|
||||
}
|
||||
|
||||
function fileData(file: TAbstractFile, frontmatter: Record<string, any>) {
|
||||
return {
|
||||
...frontmatter,
|
||||
path: file.path,
|
||||
name: file.name.replace(/\.[^/.]+$/, ""),
|
||||
id: file.path
|
||||
}
|
||||
}
|
||||
|
||||
export class SealFileSync {
|
||||
private currentSchema: Record<string, 'TEXT' | 'INTEGER' | 'REAL'> = {}
|
||||
constructor(public readonly app: App, private readonly sqlSeal: SqlSeal) {
|
||||
this.app.vault.on('modify', async (file) => {
|
||||
console.log('Modified', file)
|
||||
const frontmatter = await extractFrontmatterFromFile(file)
|
||||
if (this.isFrontmatterSame([frontmatter])) {
|
||||
// we need to update the row
|
||||
await this.sqlSeal.db.updateData('files', [fileData(file, frontmatter)])
|
||||
return
|
||||
}
|
||||
await this.init()
|
||||
})
|
||||
}
|
||||
async init() {
|
||||
const files = this.app.vault.getMarkdownFiles();
|
||||
|
||||
const data = []
|
||||
|
||||
for (const file of files) {
|
||||
const frontmatter = await extractFrontmatterFromFile(file)
|
||||
data.push(fileData(file, frontmatter))
|
||||
}
|
||||
|
||||
|
||||
const schema = await this.sqlSeal.db.createTableWithData('files', data)
|
||||
this.currentSchema = schema
|
||||
}
|
||||
|
||||
isFrontmatterSame(newFrontmatter: Array<Record<string, any>>) {
|
||||
const newSchema = this.sqlSeal.db.getSchema(newFrontmatter)
|
||||
return JSON.stringify(newSchema) === JSON.stringify(this.currentSchema)
|
||||
|
||||
}
|
||||
}
|
||||
123
src/SealObserver.ts
Normal file
123
src/SealObserver.ts
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
type Callback = () => void;
|
||||
|
||||
export class SealObserver {
|
||||
private tables: Map<string, Set<Callback>>;
|
||||
private verbose: boolean;
|
||||
|
||||
private files: Map<string, Set<string>>;
|
||||
|
||||
constructor(verbose = false) {
|
||||
this.tables = new Map();
|
||||
this.files = 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) {
|
||||
if (typeof tableNames === 'string') {
|
||||
tableNames = [tableNames];
|
||||
}
|
||||
tableNames.forEach(tableName => {
|
||||
if (!this.tables.has(tableName)) {
|
||||
this.tables.set(tableName, new Set<Callback>());
|
||||
if (this.verbose) {
|
||||
console.log(`Table "${tableName}" registered.`);
|
||||
}
|
||||
}
|
||||
const observers = this.tables.get(tableName);
|
||||
if (observers !== undefined) {
|
||||
observers.add(observer);
|
||||
if (this.verbose) {
|
||||
console.log(`Observer registered for table "${tableName}".`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
unregisterObserver(observer: Callback) {
|
||||
this.tables.forEach(observers => {
|
||||
observers.delete(observer);
|
||||
});
|
||||
if (this.verbose) {
|
||||
console.log(`Observer unregistered.`);
|
||||
}
|
||||
}
|
||||
|
||||
fireObservers(tableName: string) {
|
||||
if (this.tables.has(tableName)) {
|
||||
const observers = this.tables.get(tableName);
|
||||
if (observers) {
|
||||
observers.forEach(observer => observer());
|
||||
if (this.verbose) {
|
||||
console.log(`Observers fired for table "${tableName}".`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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}".`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,10 +13,9 @@ function isNumeric(str: string) {
|
|||
|
||||
const toTypeStatements = (header: Array<string>, data: Array<Record<string, string>>) => {
|
||||
let d: Array<Record<string, string|number>> = data
|
||||
let types: Record<string, ReturnType<typeof predictType>> = {}
|
||||
const types: Record<string, ReturnType<typeof predictType>> = {}
|
||||
header.forEach(key => {
|
||||
const type = predictType(key, data)
|
||||
console.log('TYPE:', key, type)
|
||||
if (type === 'REAL' || type === 'INTEGER') {
|
||||
// converting all data here to text
|
||||
d = d.map(record => ({
|
||||
|
|
@ -65,9 +64,74 @@ export class SqlSealDatabase {
|
|||
this.db = new Database(defaultDbPath, { verbose: verbose ? console.log : undefined })
|
||||
}
|
||||
|
||||
async createTableWithData(name: string, data: Array<Record<string, unknown>>) {
|
||||
const schema = await this.getSchema(data)
|
||||
await this.createTable(name, schema)
|
||||
await this.insertData(name, data)
|
||||
|
||||
return schema
|
||||
}
|
||||
|
||||
updateData(name: string, data: Array<Record<string, unknown>>) {
|
||||
const fields = Object.keys(data.reduce((acc, obj) => ({ ...acc, ...obj }), {}));
|
||||
const update = this.db.prepare(`UPDATE ${name} SET ${fields.map((key: string) => `${key} = @${key}`).join(', ')} WHERE id = @id`);
|
||||
const updateMany = this.db.transaction((pData: Array<Record<string, any>>) => {
|
||||
pData.forEach(data => {
|
||||
try {
|
||||
update.run(data)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return updateMany(data)
|
||||
}
|
||||
|
||||
insertData(name: string, data: Array<Record<string, unknown>>) {
|
||||
const fields = Object.keys(data.reduce((acc, obj) => ({ ...acc, ...obj }), {}));
|
||||
const insert = this.db.prepare(`INSERT INTO ${name} (${fields.join(', ')}) VALUES (${fields.map((key: string) => '@' + key).join(', ')})`);
|
||||
const insertMany = this.db.transaction((pData: Array<Record<string, any>>) => {
|
||||
pData.forEach(data => {
|
||||
try {
|
||||
// update data so all missing fields are set to null
|
||||
fields.forEach(field => {
|
||||
if (!data[field]) {
|
||||
data[field] = null
|
||||
}
|
||||
})
|
||||
insert.run(data)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return insertMany(data)
|
||||
}
|
||||
|
||||
async createTable(name: string, fields: Record<string, 'TEXT'|'INTEGER'|'REAL'>) {
|
||||
const sqlFields = Object.entries(fields).map(([key, type]) => `${key} ${type}`)
|
||||
// FIXME: probably use schema generator, for now create with hardcoded fields
|
||||
await this.db.prepare(`DROP TABLE IF EXISTS ${name}`).run()
|
||||
const createSQL = `CREATE TABLE IF NOT EXISTS ${name} (
|
||||
${sqlFields.join(', ')}
|
||||
);`
|
||||
|
||||
await this.db.prepare(createSQL).run()
|
||||
this.savedDatabases[name] = true
|
||||
|
||||
// Dropping data.
|
||||
await this.db.prepare(`DELETE FROM ${name}`).run()
|
||||
}
|
||||
async getSchema(data: Array<Record<string, unknown>>) {
|
||||
const fields = Object.keys(data.reduce((acc, obj) => ({ ...acc, ...obj }), {}));
|
||||
const { types } = toTypeStatements(fields, data as Array<Record<string, string>>); // Call the toTypeStatements function with the correct arguments
|
||||
return types;
|
||||
}
|
||||
|
||||
async defineDatabaseFromUrl(unprefixedName: string, url: string, prefix: string) {
|
||||
const name = prefixedIfNotGlobal(unprefixedName, [], prefix)
|
||||
console.log('define db from url', name, url)
|
||||
if (this.savedDatabases[name]) {
|
||||
console.log('Database Exists', name)
|
||||
return
|
||||
|
|
@ -86,17 +150,9 @@ export class SqlSealDatabase {
|
|||
})
|
||||
const fields = parsed.meta.fields
|
||||
const { data: parsedData, types } = toTypeStatements(fields, parsed.data)
|
||||
console.log('TABLE CREATE', parsedData)
|
||||
|
||||
const sqlFields = Object.entries(types).map(([key, type]) => `${key} ${type}`).join(',\n')
|
||||
// FIXME: probably use schema generator, for now create with hardcoded fields
|
||||
await this.db.prepare(`DROP TABLE IF EXISTS ${name}`).run()
|
||||
const createSQL = `CREATE TABLE IF NOT EXISTS ${name} (
|
||||
${sqlFields}
|
||||
);`
|
||||
|
||||
await this.db.prepare(createSQL).run()
|
||||
this.savedDatabases[name] = url
|
||||
await this.createTable(name, types)
|
||||
// this.savedDatabases[name] = url
|
||||
|
||||
// Purge the database
|
||||
await this.db.prepare(`DELETE FROM ${name}`).run()
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ const updateSelect = (selectAst: Select, globalTables: string[], prefix: string)
|
|||
from: selectAst.from.map(from => {
|
||||
// FIXME: better typing here.
|
||||
if(isBaseFrom(from)) {
|
||||
console.log('Found table', from.table, isGlobal(from.table, globalTables))
|
||||
return {
|
||||
...from,
|
||||
table: prefixedIfNotGlobal(from.table, globalTables, prefix)
|
||||
|
|
|
|||
|
|
@ -2,29 +2,12 @@ import * as Database from "better-sqlite3";
|
|||
import GrayMatter from "gray-matter";
|
||||
import { SqlSealDatabase, defineDatabaseFromUrl, instantiateDatabase } from "./database";
|
||||
import { displayData, displayError, displayInfo } from "./ui";
|
||||
import { App, Editor, MarkdownPostProcessorContext, MarkdownSectionInformation, WorkspaceLeaf } from "obsidian";
|
||||
import { Parser } from "node-sql-parser";
|
||||
import { App, MarkdownPostProcessorContext, MarkdownSectionInformation, WorkspaceLeaf } from "obsidian";
|
||||
import { updateTables } from "./sqlReparseTables";
|
||||
import { hashString } from "./hash";
|
||||
|
||||
const EditorLine = (line: number): EditorPosition => ({ line, ch: 0 })
|
||||
function codeBlockHandler(markdown: string, containerEl: HTMLElement, ctx: MarkdownPostProcessorContext) {
|
||||
const leaf: WorkspaceLeaf = app.workspace.rootSplit.children[0].children[0]
|
||||
const view: MarkdownView = leaf.view
|
||||
const editor: Editor = view.editor
|
||||
|
||||
const sectionInfo: MarkdownSectionInformation = ctx.getSectionInfo()
|
||||
const lineStart = EditorLine(sectionInfo.lineStart + 1)
|
||||
const lineEnd = EditorLine(sectionInfo.lineEnd )
|
||||
|
||||
const text = editor.getRange(lineStart, lineEnd)
|
||||
const gm = GrayMatter(text)
|
||||
const frontmatter = gm.data
|
||||
return frontmatter
|
||||
}
|
||||
|
||||
export class SqlSeal {
|
||||
private db: SqlSealDatabase
|
||||
public db: SqlSealDatabase
|
||||
constructor(private readonly app: App, verbose = false) {
|
||||
this.db = new SqlSealDatabase(app, verbose)
|
||||
}
|
||||
|
|
@ -33,14 +16,11 @@ export class SqlSeal {
|
|||
if (ctx.frontmatter && Object.keys(ctx.frontmatter).length > 0) {
|
||||
return ctx.frontmatter as Record<string, any>
|
||||
}
|
||||
console.log('ISSUE WITH FRONTMATTER')
|
||||
const file = this.app.vault.getFileByPath(ctx.sourcePath)
|
||||
if (!file) {
|
||||
console.log('File not found')
|
||||
return null
|
||||
}
|
||||
const data = await this.app.vault.cachedRead(file)
|
||||
console.log('DATA', data)
|
||||
|
||||
const gm = GrayMatter(data)
|
||||
const frontmatter = gm.data
|
||||
|
|
@ -59,15 +39,12 @@ export class SqlSeal {
|
|||
this.db.defineDatabaseFromUrl(name, url, prefix)
|
||||
}
|
||||
|
||||
console.log(ctx)
|
||||
|
||||
const selectRegexp = /SELECT\s+(.*)/g;
|
||||
const selectMatch = selectRegexp.exec(source)
|
||||
if (selectMatch) {
|
||||
try {
|
||||
const selectStatement = selectMatch[0]
|
||||
const selectUpdated = updateTables(selectStatement, [], prefix)
|
||||
console.log('UPDATED SELECT:::', selectUpdated)
|
||||
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)
|
||||
|
|
@ -84,30 +61,3 @@ export class SqlSeal {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// export const sqlSealHandler = async (source, el, ctx) => {
|
||||
// console.log('FRONTMATTER!!!!', ctx)
|
||||
// const regex = /TABLE\s+(.+)\s+=\s+file\(([^)]+)\)/g;
|
||||
// let match
|
||||
// while ((match = regex.exec(source)) !== null) {
|
||||
// const name = match[1];
|
||||
// const url = match[2];
|
||||
// defineDatabaseFromUrl(name, url)
|
||||
// }
|
||||
|
||||
// const selectRegexp = /SELECT\s+(.*)/g;
|
||||
// const selectMatch = selectRegexp.exec(source)
|
||||
// if (selectMatch) {
|
||||
// try {
|
||||
// const stmt = await db.prepare(selectMatch[0])
|
||||
// const columns = await stmt.columns().map(column => column.name);
|
||||
// const data = await stmt.all(ctx.frontmatter)
|
||||
// displayData
|
||||
// (el, columns, data)
|
||||
// } catch (e) {
|
||||
// displayError(el, e)
|
||||
// }
|
||||
// }
|
||||
|
||||
// }
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"1.0.0": "0.15.0"
|
||||
"0.1.0": "0.15.0"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue