feat: scaffold inicial del plugin de Obsidian osint_obsidian_plugin
Plugin fino (id osint-db) que habla HTTP con el service local osint_db (FastAPI + DuckDB) y renderiza tablas de datos en las notas del vault osint mediante el code block osintdb. Incluye parser puro de directivas con tests (node --test), settings tab, comando de paleta, enlaces internos para columnas note_path, build con esbuild + tsc y deploy.sh al vault. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
main.js
|
||||
@@ -0,0 +1,120 @@
|
||||
---
|
||||
name: osint_obsidian_plugin
|
||||
lang: ts
|
||||
domain: osint
|
||||
version: 0.1.0
|
||||
description: "Plugin de Obsidian (id osint-db) para el vault osint: ejecuta queries SQL y queries nombradas contra el service local osint_db (FastAPI + DuckDB) via HTTP y renderiza tablas de datos dentro de las notas mediante el code block osintdb. Plugin fino: no embebe ninguna base de datos."
|
||||
tags: [osint, obsidian, plugin, duckdb]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
framework: "obsidian-plugin"
|
||||
entry_point: "main.ts"
|
||||
dir_path: "projects/osint/apps/osint_obsidian_plugin"
|
||||
repo_url: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/dataforge/osint_obsidian_plugin"
|
||||
e2e_checks:
|
||||
- id: build
|
||||
cmd: "pnpm install --frozen-lockfile=false && pnpm build"
|
||||
timeout_s: 300
|
||||
- id: artifact
|
||||
cmd: "test -f main.js"
|
||||
- id: tests
|
||||
cmd: "pnpm test"
|
||||
timeout_s: 60
|
||||
---
|
||||
|
||||
# osint_obsidian_plugin
|
||||
|
||||
Plugin de Obsidian para el vault `~/Obsidian/osint`. Es deliberadamente fino: no embebe
|
||||
DuckDB ni ninguna otra base de datos. Toda la lógica de datos vive en el service local
|
||||
`osint_db` (FastAPI, dueño de la DuckDB con datos OSINT), con el que el plugin habla por
|
||||
HTTP usando `requestUrl` de la API de Obsidian (evita CORS; nunca `fetch`).
|
||||
|
||||
## Qué hace
|
||||
|
||||
1. **Code block `osintdb`**: dentro de cualquier nota, un bloque de código con lenguaje
|
||||
`osintdb` se renderiza como una tabla de datos. Dos formas de contenido:
|
||||
- SQL crudo (el bloque entero es un SELECT, va a `POST /api/query`).
|
||||
- Directivas `clave: valor` al inicio del bloque: `query` (nombre de query guardada,
|
||||
va a `POST /api/query/named`), `max_rows` y `title`.
|
||||
2. **Render seguro**: tabla HTML construida con `createEl` (los valores se insertan como
|
||||
texto, nunca como HTML crudo), header sticky, conteo de filas y nota "(truncado)"
|
||||
cuando el service recorta el resultado. Errores del service y service caído se
|
||||
muestran con mensajes claros, incluido el comando para arrancar `osint_db`.
|
||||
3. **Botón "Refrescar"** por bloque: re-ejecuta la query sin recargar la nota.
|
||||
4. **Enlaces a notas**: si una columna se llama `note_path` y el valor termina en `.md`,
|
||||
la celda se renderiza como enlace interno de Obsidian que abre la nota al hacer clic.
|
||||
5. **Settings tab**: URL base del service (default `http://127.0.0.1:8771`) y `max_rows`
|
||||
por defecto (500), persistidos con `loadData`/`saveData`.
|
||||
6. **Comando de paleta** "OSINT DB: Insertar bloque de query": inserta una plantilla de
|
||||
bloque `osintdb` en el editor.
|
||||
|
||||
## Ejemplo de bloque
|
||||
|
||||
Forma con SQL crudo:
|
||||
|
||||
```osintdb
|
||||
title: Personas del caso X
|
||||
max_rows: 100
|
||||
SELECT nombre, contexto, note_path FROM main.personas WHERE contexto = 'caso_x';
|
||||
```
|
||||
|
||||
Forma con query nombrada (definida en el service):
|
||||
|
||||
```osintdb
|
||||
query: personas_por_contexto
|
||||
max_rows: 100
|
||||
```
|
||||
|
||||
## Contrato API del service osint_db
|
||||
|
||||
Base URL por defecto: `http://127.0.0.1:8771`. El service responde siempre HTTP 200 y el
|
||||
plugin decide por el campo `status` del body.
|
||||
|
||||
- `GET /api/health` → `{"status":"ok","db_path":"...","tables":N}`
|
||||
- `GET /api/tables` → listado de tablas con schema, kind, row_count y columnas.
|
||||
- `POST /api/query` body `{"sql":"SELECT ...","params":[],"max_rows":500}` →
|
||||
`{"status":"ok","columns":[...],"rows":[...],"row_count":N,"truncated":bool}` o
|
||||
`{"status":"error","error":"..."}`.
|
||||
- `GET /api/queries` → queries nombradas disponibles.
|
||||
- `POST /api/query/named` body `{"name":"...","max_rows":500}` → misma shape que `/api/query`.
|
||||
|
||||
## Cómo construir
|
||||
|
||||
```bash
|
||||
cd projects/osint/apps/osint_obsidian_plugin
|
||||
pnpm install
|
||||
pnpm build # tsc --noEmit + esbuild -> main.js
|
||||
pnpm test # tests del parser de bloques (node --test, sin Obsidian)
|
||||
```
|
||||
|
||||
Requiere Node >= 23 para los tests (usa el type stripping nativo de `node --test` sobre
|
||||
archivos `.ts`).
|
||||
|
||||
## Cómo desplegar
|
||||
|
||||
```bash
|
||||
./deploy.sh
|
||||
```
|
||||
|
||||
Copia `manifest.json`, `main.js` y `styles.css` a
|
||||
`/home/enmanuel/Obsidian/osint/.obsidian/plugins/osint-db/`.
|
||||
|
||||
**Activación manual (la hace el humano, no un agente):** tras el deploy, abrir Obsidian →
|
||||
Settings → Community plugins → activar "OSINT DB". Si Obsidian ya estaba abierto, hace
|
||||
falta recargar la app (Ctrl+R) o reiniciarla para que detecte el plugin nuevo o la
|
||||
versión nueva de `main.js`.
|
||||
|
||||
## Estructura
|
||||
|
||||
- `main.ts` — plugin completo (processor, render, settings, comando).
|
||||
- `parse.ts` — parser puro del contenido de los bloques `osintdb` (testeable sin Obsidian).
|
||||
- `tests/parse.test.ts` — tests del parser con `node --test`.
|
||||
- `esbuild.config.mjs` — bundle CommonJS a `main.js` con `obsidian` como external.
|
||||
- `deploy.sh` — copia los artefactos al vault.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- El plugin no arranca el service: si `osint_db` no está corriendo, los bloques muestran
|
||||
"osint_db no responde en <url>" con el comando de arranque como hint.
|
||||
- `main.js` es artefacto de build (gitignored): siempre `pnpm build` antes de `deploy.sh`.
|
||||
- `isDesktopOnly: true` en el manifest — el service es local, no tiene sentido en móvil.
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Despliega el plugin al vault osint: copia manifest.json, main.js y styles.css
|
||||
# a la carpeta de plugins de Obsidian. Requiere haber corrido `pnpm build` antes.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
TARGET="/home/enmanuel/Obsidian/osint/.obsidian/plugins/osint-db"
|
||||
|
||||
if [ ! -f main.js ]; then
|
||||
echo "ERROR: falta main.js — ejecuta 'pnpm build' primero." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$TARGET"
|
||||
cp manifest.json main.js styles.css "$TARGET/"
|
||||
|
||||
echo "Plugin desplegado en $TARGET"
|
||||
ls -l "$TARGET"
|
||||
@@ -0,0 +1,28 @@
|
||||
// Build del plugin: empaqueta main.ts en un main.js CommonJS que Obsidian carga
|
||||
// directamente. El paquete "obsidian" y los módulos de Electron/Node quedan como
|
||||
// externals porque los provee el propio runtime de Obsidian.
|
||||
import esbuild from "esbuild";
|
||||
import { builtinModules } from "node:module";
|
||||
|
||||
const banner = `/*
|
||||
Bundle generado por esbuild. No editar a mano: el código fuente vive en main.ts.
|
||||
*/`;
|
||||
|
||||
await esbuild.build({
|
||||
entryPoints: ["main.ts"],
|
||||
bundle: true,
|
||||
outfile: "main.js",
|
||||
format: "cjs",
|
||||
target: "es2020",
|
||||
platform: "browser",
|
||||
sourcemap: false,
|
||||
treeShaking: true,
|
||||
logLevel: "info",
|
||||
banner: { js: banner },
|
||||
external: [
|
||||
"obsidian",
|
||||
"electron",
|
||||
...builtinModules,
|
||||
...builtinModules.map((m) => `node:${m}`),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,255 @@
|
||||
// Plugin de Obsidian "OSINT DB". Plugin fino: no embebe ninguna base de datos,
|
||||
// habla HTTP con el service local osint_db (FastAPI + DuckDB) y renderiza los
|
||||
// resultados como tablas dentro de las notas mediante el code block `osintdb`.
|
||||
import {
|
||||
App,
|
||||
Editor,
|
||||
Plugin,
|
||||
PluginSettingTab,
|
||||
Setting,
|
||||
requestUrl,
|
||||
} from "obsidian";
|
||||
import { parseBlock, ParsedBlock } from "./parse";
|
||||
|
||||
interface OsintDbSettings {
|
||||
/** URL base del service osint_db. */
|
||||
baseUrl: string;
|
||||
/** Límite de filas por defecto cuando el bloque no declara max_rows. */
|
||||
maxRows: number;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: OsintDbSettings = {
|
||||
baseUrl: "http://127.0.0.1:8771",
|
||||
maxRows: 500,
|
||||
};
|
||||
|
||||
/** Respuesta de /api/query y /api/query/named. */
|
||||
interface QueryResponse {
|
||||
status: string;
|
||||
error?: string;
|
||||
columns?: string[];
|
||||
rows?: Record<string, unknown>[];
|
||||
row_count?: number;
|
||||
truncated?: boolean;
|
||||
}
|
||||
|
||||
const SNIPPET = [
|
||||
"```osintdb",
|
||||
"title: Mi consulta",
|
||||
"max_rows: 100",
|
||||
"SELECT * FROM main.personas LIMIT 20;",
|
||||
"```",
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
export default class OsintDbPlugin extends Plugin {
|
||||
settings: OsintDbSettings = DEFAULT_SETTINGS;
|
||||
|
||||
async onload(): Promise<void> {
|
||||
await this.loadSettings();
|
||||
this.addSettingTab(new OsintDbSettingTab(this.app, this));
|
||||
|
||||
this.registerMarkdownCodeBlockProcessor("osintdb", (source, el) => {
|
||||
const block = parseBlock(source);
|
||||
const container = el.createDiv({ cls: "osintdb-block" });
|
||||
void this.renderBlock(container, block);
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "insert-query-block",
|
||||
name: "Insertar bloque de query",
|
||||
editorCallback: (editor: Editor) => {
|
||||
editor.replaceSelection(SNIPPET);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async loadSettings(): Promise<void> {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
|
||||
async saveSettings(): Promise<void> {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
|
||||
/** Ejecuta la query del bloque contra el service y pinta el resultado. */
|
||||
private async renderBlock(container: HTMLElement, block: ParsedBlock): Promise<void> {
|
||||
container.empty();
|
||||
|
||||
if (block.error) {
|
||||
this.renderError(container, block.error);
|
||||
return;
|
||||
}
|
||||
|
||||
const header = container.createDiv({ cls: "osintdb-header" });
|
||||
header.createEl("span", {
|
||||
cls: "osintdb-title",
|
||||
text: block.title ?? (block.query ? `query: ${block.query}` : "SQL"),
|
||||
});
|
||||
const refreshBtn = header.createEl("button", {
|
||||
cls: "osintdb-refresh",
|
||||
text: "Refrescar",
|
||||
attr: { type: "button", "aria-label": "Re-ejecutar la query" },
|
||||
});
|
||||
refreshBtn.addEventListener("click", () => {
|
||||
void this.renderBlock(container, block);
|
||||
});
|
||||
|
||||
const body = container.createDiv({ cls: "osintdb-body" });
|
||||
body.createEl("div", { cls: "osintdb-loading", text: "Consultando osint_db..." });
|
||||
|
||||
const maxRows = block.maxRows ?? this.settings.maxRows;
|
||||
let data: QueryResponse;
|
||||
try {
|
||||
data = await this.runQuery(block, maxRows);
|
||||
} catch (e) {
|
||||
body.empty();
|
||||
this.renderUnreachable(body);
|
||||
return;
|
||||
}
|
||||
|
||||
body.empty();
|
||||
if (data.status !== "ok") {
|
||||
this.renderError(body, data.error ?? "El service devolvió un error sin mensaje.");
|
||||
return;
|
||||
}
|
||||
this.renderTable(body, data);
|
||||
}
|
||||
|
||||
/** Llama a /api/query o /api/query/named según la forma del bloque. */
|
||||
private async runQuery(block: ParsedBlock, maxRows: number): Promise<QueryResponse> {
|
||||
const base = this.settings.baseUrl.replace(/\/+$/, "");
|
||||
const isNamed = block.query !== undefined;
|
||||
const url = isNamed ? `${base}/api/query/named` : `${base}/api/query`;
|
||||
const payload = isNamed
|
||||
? { name: block.query, max_rows: maxRows }
|
||||
: { sql: block.sql, params: [], max_rows: maxRows };
|
||||
|
||||
// requestUrl (API de Obsidian) evita CORS; nunca usar fetch aquí.
|
||||
const res = await requestUrl({
|
||||
url,
|
||||
method: "POST",
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(payload),
|
||||
throw: false,
|
||||
});
|
||||
return res.json as QueryResponse;
|
||||
}
|
||||
|
||||
private renderTable(parent: HTMLElement, data: QueryResponse): void {
|
||||
const columns = data.columns ?? [];
|
||||
const rows = data.rows ?? [];
|
||||
|
||||
if (columns.length === 0) {
|
||||
parent.createEl("div", { cls: "osintdb-empty", text: "La query no devolvió columnas." });
|
||||
return;
|
||||
}
|
||||
|
||||
const wrapper = parent.createDiv({ cls: "osintdb-table-wrapper" });
|
||||
const table = wrapper.createEl("table", { cls: "osintdb-table" });
|
||||
const thead = table.createEl("thead");
|
||||
const headRow = thead.createEl("tr");
|
||||
for (const col of columns) {
|
||||
headRow.createEl("th", { text: col });
|
||||
}
|
||||
|
||||
const tbody = table.createEl("tbody");
|
||||
for (const row of rows) {
|
||||
const tr = tbody.createEl("tr");
|
||||
for (const col of columns) {
|
||||
const td = tr.createEl("td");
|
||||
this.renderCell(td, col, row[col]);
|
||||
}
|
||||
}
|
||||
|
||||
const count = data.row_count ?? rows.length;
|
||||
const suffix = data.truncated ? " (truncado)" : "";
|
||||
parent.createEl("div", {
|
||||
cls: "osintdb-footer",
|
||||
text: `${count} fila${count === 1 ? "" : "s"}${suffix}`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Pinta una celda. Los valores se insertan siempre como texto (createEl usa
|
||||
* textContent por debajo), nunca como HTML. Las columnas note_path con valor
|
||||
* terminado en .md se renderizan como enlace interno al estilo Obsidian.
|
||||
*/
|
||||
private renderCell(td: HTMLElement, column: string, value: unknown): void {
|
||||
if (value === null || value === undefined) {
|
||||
td.createEl("span", { cls: "osintdb-null", text: "∅" });
|
||||
return;
|
||||
}
|
||||
const text = typeof value === "object" ? JSON.stringify(value) : String(value);
|
||||
|
||||
if (column === "note_path" && text.endsWith(".md")) {
|
||||
const link = td.createEl("a", { cls: "internal-link", text });
|
||||
link.addEventListener("click", (ev) => {
|
||||
ev.preventDefault();
|
||||
void this.app.workspace.openLinkText(text.slice(0, -3), "");
|
||||
});
|
||||
return;
|
||||
}
|
||||
td.setText(text);
|
||||
}
|
||||
|
||||
private renderError(parent: HTMLElement, message: string): void {
|
||||
const box = parent.createDiv({ cls: "osintdb-error" });
|
||||
box.createEl("strong", { text: "Error osint_db" });
|
||||
box.createEl("div", { text: message });
|
||||
}
|
||||
|
||||
private renderUnreachable(parent: HTMLElement): void {
|
||||
const box = parent.createDiv({ cls: "osintdb-error" });
|
||||
box.createEl("strong", { text: `osint_db no responde en ${this.settings.baseUrl}` });
|
||||
box.createEl("div", {
|
||||
text: "Arranca el service local (ver projects/osint/apps/osint_db/app.md), por ejemplo:",
|
||||
});
|
||||
box.createEl("code", {
|
||||
text: "cd ~/fn_registry/projects/osint/apps/osint_db && .venv/bin/python -m uvicorn main:app --port 8771",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class OsintDbSettingTab extends PluginSettingTab {
|
||||
plugin: OsintDbPlugin;
|
||||
|
||||
constructor(app: App, plugin: OsintDbPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("URL base del service osint_db")
|
||||
.setDesc("Dirección HTTP donde escucha el service local (FastAPI + DuckDB).")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder(DEFAULT_SETTINGS.baseUrl)
|
||||
.setValue(this.plugin.settings.baseUrl)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.baseUrl = value.trim() || DEFAULT_SETTINGS.baseUrl;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("max_rows por defecto")
|
||||
.setDesc("Límite de filas cuando el bloque no declara la directiva max_rows.")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder(String(DEFAULT_SETTINGS.maxRows))
|
||||
.setValue(String(this.plugin.settings.maxRows))
|
||||
.onChange(async (value) => {
|
||||
const n = Number(value);
|
||||
if (Number.isInteger(n) && n > 0) {
|
||||
this.plugin.settings.maxRows = n;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"id": "osint-db",
|
||||
"name": "OSINT DB",
|
||||
"version": "0.1.0",
|
||||
"minAppVersion": "1.5.0",
|
||||
"description": "Ejecuta queries SQL contra el service local osint_db (DuckDB) y muestra tablas de datos dentro de las notas del vault.",
|
||||
"author": "dataforge",
|
||||
"isDesktopOnly": true
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "osint-db",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Plugin de Obsidian que habla con el service osint_db (FastAPI + DuckDB) para mostrar tablas de datos OSINT en las notas.",
|
||||
"scripts": {
|
||||
"build": "tsc --noEmit && node esbuild.config.mjs",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "node --test tests/parse.test.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.0",
|
||||
"esbuild": "^0.24.2",
|
||||
"obsidian": "^1.7.2",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Parseo del contenido de un bloque ```osintdb```. Módulo puro, sin dependencias
|
||||
// de Obsidian, para poder testearlo con node --test sin levantar la app.
|
||||
//
|
||||
// Dos formas de bloque:
|
||||
// 1. SQL crudo: el bloque entero es una sentencia SELECT que se manda a /api/query.
|
||||
// 2. Con directivas: líneas "clave: valor" al inicio del bloque. Si hay directiva
|
||||
// "query" se ejecuta una query nombrada via /api/query/named. Las directivas
|
||||
// "max_rows" y "title" aplican a ambas formas.
|
||||
|
||||
export interface ParsedBlock {
|
||||
/** Nombre de la query guardada en el service (forma named). */
|
||||
query?: string;
|
||||
/** SQL crudo a ejecutar (forma raw). */
|
||||
sql?: string;
|
||||
/** Límite de filas pedido al service. */
|
||||
maxRows?: number;
|
||||
/** Título a mostrar encima de la tabla. */
|
||||
title?: string;
|
||||
/** Mensaje de error de parseo; si está presente, el resto se ignora. */
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const DIRECTIVE_RE = /^(query|max_rows|title)\s*:\s*(.*)$/;
|
||||
|
||||
/**
|
||||
* Parsea el texto de un bloque osintdb. Consume las líneas de directivas del
|
||||
* inicio (clave: valor) y trata el resto como SQL crudo. Reglas:
|
||||
* - "query" presente -> query nombrada (no se admite SQL adicional en el bloque).
|
||||
* - sin "query" pero con SQL -> query cruda.
|
||||
* - bloque vacío o directivas inválidas -> error.
|
||||
*/
|
||||
export function parseBlock(source: string): ParsedBlock {
|
||||
const lines = source.split(/\r?\n/);
|
||||
const result: ParsedBlock = {};
|
||||
let i = 0;
|
||||
|
||||
for (; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (line === "" && result.query === undefined && result.maxRows === undefined && result.title === undefined) {
|
||||
// Líneas en blanco iniciales antes de cualquier directiva: se saltan.
|
||||
continue;
|
||||
}
|
||||
const m = line.match(DIRECTIVE_RE);
|
||||
if (!m) {
|
||||
break; // Fin de las directivas; lo que queda es SQL.
|
||||
}
|
||||
const key = m[1];
|
||||
const value = m[2].trim();
|
||||
if (key === "query") {
|
||||
if (value === "") {
|
||||
return { error: "La directiva 'query' necesita un nombre de query guardada." };
|
||||
}
|
||||
result.query = value;
|
||||
} else if (key === "max_rows") {
|
||||
const n = Number(value);
|
||||
if (!Number.isInteger(n) || n <= 0) {
|
||||
return { error: `Valor inválido para max_rows: "${value}" (debe ser un entero positivo).` };
|
||||
}
|
||||
result.maxRows = n;
|
||||
} else if (key === "title") {
|
||||
result.title = value;
|
||||
}
|
||||
}
|
||||
|
||||
const rest = lines.slice(i).join("\n").trim();
|
||||
|
||||
if (result.query !== undefined) {
|
||||
if (rest !== "") {
|
||||
return { error: "El bloque mezcla la directiva 'query' con SQL. Usa una de las dos formas." };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (rest === "") {
|
||||
return { error: "Bloque vacío: escribe un SELECT o una directiva 'query: <nombre>'." };
|
||||
}
|
||||
|
||||
result.sql = rest;
|
||||
return result;
|
||||
}
|
||||
Generated
+377
@@ -0,0 +1,377 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^22.10.0
|
||||
version: 22.19.21
|
||||
esbuild:
|
||||
specifier: ^0.24.2
|
||||
version: 0.24.2
|
||||
obsidian:
|
||||
specifier: ^1.7.2
|
||||
version: 1.13.1(@codemirror/state@6.5.0)(@codemirror/view@6.38.6)
|
||||
typescript:
|
||||
specifier: ^5.7.2
|
||||
version: 5.9.3
|
||||
|
||||
packages:
|
||||
|
||||
'@codemirror/state@6.5.0':
|
||||
resolution: {integrity: sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==}
|
||||
|
||||
'@codemirror/view@6.38.6':
|
||||
resolution: {integrity: sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==}
|
||||
|
||||
'@esbuild/aix-ppc64@0.24.2':
|
||||
resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/android-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.24.2':
|
||||
resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.24.2':
|
||||
resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/darwin-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.24.2':
|
||||
resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.24.2':
|
||||
resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/linux-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.24.2':
|
||||
resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.24.2':
|
||||
resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.24.2':
|
||||
resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.24.2':
|
||||
resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.24.2':
|
||||
resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.24.2':
|
||||
resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.24.2':
|
||||
resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.24.2':
|
||||
resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.24.2':
|
||||
resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.24.2':
|
||||
resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/sunos-x64@0.24.2':
|
||||
resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/win32-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.24.2':
|
||||
resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.24.2':
|
||||
resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@marijn/find-cluster-break@1.0.2':
|
||||
resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
|
||||
|
||||
'@types/codemirror@5.60.8':
|
||||
resolution: {integrity: sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==}
|
||||
|
||||
'@types/estree@1.0.9':
|
||||
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
|
||||
|
||||
'@types/node@22.19.21':
|
||||
resolution: {integrity: sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA==}
|
||||
|
||||
'@types/tern@0.23.9':
|
||||
resolution: {integrity: sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==}
|
||||
|
||||
crelt@1.0.6:
|
||||
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
|
||||
|
||||
esbuild@0.24.2:
|
||||
resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
moment@2.29.4:
|
||||
resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==}
|
||||
|
||||
obsidian@1.13.1:
|
||||
resolution: {integrity: sha512-qtTEA2pmhJzhuhJqzbBFRYhpIOqvW+krDYjtFynv66KbxBbumHBlsJfWw3I4jtnK/6fZwbQhCrmmDdRwXmX56w==}
|
||||
peerDependencies:
|
||||
'@codemirror/state': 6.5.0
|
||||
'@codemirror/view': 6.38.6
|
||||
|
||||
style-mod@4.1.3:
|
||||
resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==}
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
|
||||
w3c-keyname@2.2.8:
|
||||
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@codemirror/state@6.5.0':
|
||||
dependencies:
|
||||
'@marijn/find-cluster-break': 1.0.2
|
||||
|
||||
'@codemirror/view@6.38.6':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.5.0
|
||||
crelt: 1.0.6
|
||||
style-mod: 4.1.3
|
||||
w3c-keyname: 2.2.8
|
||||
|
||||
'@esbuild/aix-ppc64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@marijn/find-cluster-break@1.0.2': {}
|
||||
|
||||
'@types/codemirror@5.60.8':
|
||||
dependencies:
|
||||
'@types/tern': 0.23.9
|
||||
|
||||
'@types/estree@1.0.9': {}
|
||||
|
||||
'@types/node@22.19.21':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/tern@0.23.9':
|
||||
dependencies:
|
||||
'@types/estree': 1.0.9
|
||||
|
||||
crelt@1.0.6: {}
|
||||
|
||||
esbuild@0.24.2:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.24.2
|
||||
'@esbuild/android-arm': 0.24.2
|
||||
'@esbuild/android-arm64': 0.24.2
|
||||
'@esbuild/android-x64': 0.24.2
|
||||
'@esbuild/darwin-arm64': 0.24.2
|
||||
'@esbuild/darwin-x64': 0.24.2
|
||||
'@esbuild/freebsd-arm64': 0.24.2
|
||||
'@esbuild/freebsd-x64': 0.24.2
|
||||
'@esbuild/linux-arm': 0.24.2
|
||||
'@esbuild/linux-arm64': 0.24.2
|
||||
'@esbuild/linux-ia32': 0.24.2
|
||||
'@esbuild/linux-loong64': 0.24.2
|
||||
'@esbuild/linux-mips64el': 0.24.2
|
||||
'@esbuild/linux-ppc64': 0.24.2
|
||||
'@esbuild/linux-riscv64': 0.24.2
|
||||
'@esbuild/linux-s390x': 0.24.2
|
||||
'@esbuild/linux-x64': 0.24.2
|
||||
'@esbuild/netbsd-arm64': 0.24.2
|
||||
'@esbuild/netbsd-x64': 0.24.2
|
||||
'@esbuild/openbsd-arm64': 0.24.2
|
||||
'@esbuild/openbsd-x64': 0.24.2
|
||||
'@esbuild/sunos-x64': 0.24.2
|
||||
'@esbuild/win32-arm64': 0.24.2
|
||||
'@esbuild/win32-ia32': 0.24.2
|
||||
'@esbuild/win32-x64': 0.24.2
|
||||
|
||||
moment@2.29.4: {}
|
||||
|
||||
obsidian@1.13.1(@codemirror/state@6.5.0)(@codemirror/view@6.38.6):
|
||||
dependencies:
|
||||
'@codemirror/state': 6.5.0
|
||||
'@codemirror/view': 6.38.6
|
||||
'@types/codemirror': 5.60.8
|
||||
moment: 2.29.4
|
||||
|
||||
style-mod@4.1.3: {}
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
|
||||
w3c-keyname@2.2.8: {}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Configuración de pnpm: permite el script de build de esbuild (descarga su binario).
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
/* Estilos del plugin OSINT DB. Usa variables CSS de Obsidian para que la tabla
|
||||
sea legible tanto en tema oscuro como claro. */
|
||||
|
||||
.osintdb-block {
|
||||
margin: 0.5em 0 1em 0;
|
||||
}
|
||||
|
||||
.osintdb-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5em;
|
||||
margin-bottom: 0.35em;
|
||||
}
|
||||
|
||||
.osintdb-title {
|
||||
font-weight: 600;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.osintdb-refresh {
|
||||
font-size: var(--font-ui-smaller);
|
||||
padding: 2px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.osintdb-table-wrapper {
|
||||
max-height: 420px;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
}
|
||||
|
||||
.osintdb-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: var(--font-ui-small);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.osintdb-table thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
background: var(--background-secondary);
|
||||
color: var(--text-normal);
|
||||
text-align: left;
|
||||
padding: 6px 10px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.osintdb-table tbody td {
|
||||
padding: 4px 10px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
color: var(--text-normal);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.osintdb-table tbody tr:hover {
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.osintdb-null {
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.osintdb-footer {
|
||||
margin-top: 0.3em;
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.osintdb-error {
|
||||
border: 1px solid var(--background-modifier-error);
|
||||
border-left: 4px solid var(--text-error);
|
||||
border-radius: var(--radius-s);
|
||||
padding: 8px 12px;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.osintdb-error strong {
|
||||
color: var(--text-error);
|
||||
}
|
||||
|
||||
.osintdb-error code {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
.osintdb-loading,
|
||||
.osintdb-empty {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-ui-small);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Tests del parser de bloques osintdb. Se ejecutan con `node --test tests/`
|
||||
// gracias al type stripping nativo de Node (>= 23), sin necesidad de Obsidian.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { parseBlock } from "../parse.ts";
|
||||
|
||||
test("SQL crudo: el bloque entero es la sentencia", () => {
|
||||
const r = parseBlock("SELECT * FROM main.personas LIMIT 10;");
|
||||
assert.equal(r.sql, "SELECT * FROM main.personas LIMIT 10;");
|
||||
assert.equal(r.query, undefined);
|
||||
assert.equal(r.error, undefined);
|
||||
});
|
||||
|
||||
test("SQL multilínea se conserva entero", () => {
|
||||
const src = "SELECT nombre, contexto\nFROM main.personas\nWHERE contexto = 'caso_x'";
|
||||
const r = parseBlock(src);
|
||||
assert.equal(r.sql, src);
|
||||
});
|
||||
|
||||
test("query nombrada con max_rows", () => {
|
||||
const r = parseBlock("query: personas_por_contexto\nmax_rows: 100");
|
||||
assert.equal(r.query, "personas_por_contexto");
|
||||
assert.equal(r.maxRows, 100);
|
||||
assert.equal(r.sql, undefined);
|
||||
assert.equal(r.error, undefined);
|
||||
});
|
||||
|
||||
test("directivas title y max_rows seguidas de SQL crudo", () => {
|
||||
const r = parseBlock("title: Personas\nmax_rows: 50\nSELECT * FROM main.personas");
|
||||
assert.equal(r.title, "Personas");
|
||||
assert.equal(r.maxRows, 50);
|
||||
assert.equal(r.sql, "SELECT * FROM main.personas");
|
||||
});
|
||||
|
||||
test("líneas en blanco iniciales se ignoran", () => {
|
||||
const r = parseBlock("\n\nquery: dominios_activos");
|
||||
assert.equal(r.query, "dominios_activos");
|
||||
});
|
||||
|
||||
test("bloque vacío devuelve error", () => {
|
||||
const r = parseBlock(" \n ");
|
||||
assert.ok(r.error);
|
||||
});
|
||||
|
||||
test("max_rows no numérico devuelve error", () => {
|
||||
const r = parseBlock("max_rows: muchas\nSELECT 1");
|
||||
assert.ok(r.error?.includes("max_rows"));
|
||||
});
|
||||
|
||||
test("query sin nombre devuelve error", () => {
|
||||
const r = parseBlock("query: ");
|
||||
assert.ok(r.error);
|
||||
});
|
||||
|
||||
test("mezclar query nombrada con SQL devuelve error", () => {
|
||||
const r = parseBlock("query: personas\nSELECT 1");
|
||||
assert.ok(r.error?.includes("mezcla"));
|
||||
});
|
||||
|
||||
test("un SELECT que contiene dos puntos no se confunde con directiva", () => {
|
||||
const src = "SELECT 'query: falsa' AS texto FROM main.personas";
|
||||
const r = parseBlock(src);
|
||||
assert.equal(r.sql, src);
|
||||
assert.equal(r.query, undefined);
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["main.ts", "parse.ts", "tests/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user