Initial commit

This commit is contained in:
qf3l3k 2025-03-16 20:03:08 +01:00
commit f031902d33
19 changed files with 3459 additions and 0 deletions

10
.editorconfig Normal file
View file

@ -0,0 +1,10 @@
# top-most EditorConfig file
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 4
tab_width = 4

3
.eslintignore Normal file
View file

@ -0,0 +1,3 @@
node_modules/
main.js

23
.eslintrc Normal file
View file

@ -0,0 +1,23 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"env": { "node": true },
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"sourceType": "module"
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
"@typescript-eslint/ban-ts-comment": "off",
"no-prototype-builtins": "off",
"@typescript-eslint/no-empty-function": "off"
}
}

22
.gitignore vendored Normal file
View file

@ -0,0 +1,22 @@
# vscode
.vscode
# Intellij
*.iml
.idea
# npm
node_modules
# Don't include the compiled main.js file in the repo.
# They should be uploaded to GitHub releases instead.
main.js
# Exclude sourcemaps
*.map
# obsidian
data.json
# Exclude macOS Finder (System Explorer) View States
.DS_Store

1
.npmrc Normal file
View file

@ -0,0 +1 @@
tag-version-prefix=""

5
LICENSE Normal file
View file

@ -0,0 +1,5 @@
Copyright (C) 2020-2025 by Dynalist Inc.
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

116
README.md Normal file
View file

@ -0,0 +1,116 @@
# Obsidian Data Fetcher
A plugin for [Obsidian](https://obsidian.md) that allows users to fetch data from multiple sources (REST APIs, GraphQL, gRPC, RPC) and insert the results into notes.
## Features
- Support for multiple data sources:
- REST APIs
- GraphQL endpoints
- gRPC services (via REST proxies)
- RPC services
- Two modes of operation:
- Directly define queries within notes
- Predefine endpoints in settings and reference them in notes
- Automatic caching of query results to reduce redundant requests
- Cache expiration settings
- Manual refresh capabilities
- Customizable headers for authentication
## Installation
### From Obsidian Community Plugins
1. Open Obsidian Settings
2. Go to Community Plugins
3. Search for "Data Fetcher"
4. Click Install, then Enable
### Manual Installation
1. Download the latest release from the [releases page](https://github.com/qf3l3k/obsidian-api-fetcher/releases)
2. Extract the zip file into your Obsidian vault's `.obsidian/plugins` folder
3. Enable the plugin in Obsidian settings
## Usage
### Method 1: Direct Query Definition
Create a code block with the language set to `data-query` and define your query in JSON format:
```
```data-query
{
"type": "rest",
"url": "https://api.example.com/data",
"method": "GET",
"headers": {
"Authorization": "Bearer your-token"
}
}
```
```
### Method 2: Using Aliases
1. First, define an endpoint alias in the plugin settings
2. Then reference it in your notes:
```
```data-query
@my-api-alias
body: {"id": 123}
```
```
## Configuration
Go to Settings > Data Fetcher to configure:
- Cache duration
- Pre-defined endpoint aliases
- Default headers
## Examples
### REST API Example
```
```data-query
{
"type": "rest",
"url": "https://jsonplaceholder.typicode.com/posts/1",
"method": "GET"
}
```
```
### GraphQL Example
```
```data-query
{
"type": "graphql",
"url": "https://api.spacex.land/graphql",
"query": "{ launchesPast(limit: 5) { mission_name launch_date_local } }",
"variables": {}
}
```
```
### Using Aliases
```
```data-query
@github-api
query: query { viewer { repositories(first: 5) { nodes { name } } } }
```
```
## Support
If you encounter any issues or have feature requests, please file them on the [GitHub issues page](https://github.com/yourusername/obsidian-api-fetcher/issues).
## License
MIT

49
esbuild.config.mjs Normal file
View file

@ -0,0 +1,49 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
const banner =
`/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;
const prod = (process.argv[2] === "production");
const context = await esbuild.context({
banner: {
js: banner,
},
entryPoints: ["main.ts"],
bundle: true,
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins],
format: "cjs",
target: "es2018",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
minify: prod,
});
if (prod) {
await context.rebuild();
process.exit(0);
} else {
await context.watch();
}

385
main.ts Normal file
View file

@ -0,0 +1,385 @@
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
import { DataFetcherSettings, DEFAULT_SETTINGS } from './src/settings';
import { parseDataQuery, executeQuery, QueryParams, QueryResult } from './src/queryEngine';
import { CacheManager } from './src/cacheManager';
export default class DataFetcherPlugin extends Plugin {
settings: DataFetcherSettings;
cacheManager: CacheManager;
async onload() {
console.log('Loading Data Fetcher plugin');
await this.loadSettings();
this.cacheManager = new CacheManager(this.app, this);
// Register the data fetcher processor for codeblocks
this.registerMarkdownCodeBlockProcessor('data-query', async (source, el, ctx) => {
try {
const query = parseDataQuery(source, this.settings);
const cachedResult = await this.cacheManager.getFromCache(query);
if (cachedResult) {
this.renderResult(cachedResult, el, query); // Pass the query here
} else {
el.createEl('div', { text: 'Fetching data...', cls: 'data-fetcher-loading' });
const result = await executeQuery(query);
await this.cacheManager.saveToCache(query, result);
el.empty();
this.renderResult(result, el, query); // Pass the query here
}
} catch (error) {
el.createEl('div', { text: `Error: ${error.message}`, cls: 'data-fetcher-error' });
}
});
// Add the command to manually refresh data
this.addCommand({
id: 'refresh-data-query',
name: 'Refresh Data Query',
editorCallback: (editor: Editor, view: MarkdownView) => {
new Notice('Refreshing data queries in the current note...');
this.app.workspace.trigger('data-fetcher:refresh-query');
}
});
// Add settings tab
this.addSettingTab(new DataFetcherSettingTab(this.app, this));
}
renderResult(result: QueryResult, container: HTMLElement, query?: QueryParams) {
const resultContainer = container.createEl('div', { cls: 'data-fetcher-result' });
// Create header with timestamp and refresh button
const header = resultContainer.createEl('div', { cls: 'data-fetcher-header' });
header.createEl('span', {
text: `Last updated: ${new Date(result.timestamp).toLocaleString()}`,
cls: 'data-fetcher-timestamp'
});
const refreshBtn = header.createEl('button', {
text: 'Refresh',
cls: 'data-fetcher-refresh'
});
refreshBtn.addEventListener('click', async () => {
try {
// Clear the container and show loading
container.empty();
container.createEl('div', { text: 'Refreshing data...', cls: 'data-fetcher-loading' });
// Re-execute the query using the stored query data
const storedQuery = (refreshBtn as any).query;
if (!storedQuery) {
throw new Error('Query data not found');
}
const result = await executeQuery(storedQuery);
// Update the cache
await this.cacheManager.saveToCache(storedQuery, result);
// Re-render the result
container.empty();
this.renderResult(result, container, storedQuery);
new Notice('Data refreshed successfully');
} catch (error) {
container.empty();
container.createEl('div', { text: `Error: ${error.message}`, cls: 'data-fetcher-error' });
}
});
// Store the query with the button for later use
if (query) {
(refreshBtn as any).query = query;
}
// Create the content container
const content = resultContainer.createEl('div', { cls: 'data-fetcher-content' });
// Render the data based on type
if (typeof result.data === 'object') {
const pre = content.createEl('pre');
pre.createEl('code', { text: JSON.stringify(result.data, null, 2) });
} else {
content.setText(String(result.data));
}
}
onunload() {
console.log('Unloading Data Fetcher plugin');
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
}
class DataFetcherSettingTab extends PluginSettingTab {
plugin: DataFetcherPlugin;
constructor(app: App, plugin: DataFetcherPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const {containerEl} = this;
containerEl.empty();
// Cache Management section
containerEl.createEl('h3', {text: 'Cache Management'});
const cacheInfoEl = containerEl.createEl('div', {
cls: 'data-fetcher-cache-info'
});
// Get and display cache info
this.updateCacheInfo(cacheInfoEl);
// Clear cache button
new Setting(containerEl)
.setName('Clear Cache')
.setDesc('Remove all cached API responses')
.addButton(button => button
.setButtonText('Clear Cache')
.setClass('data-fetcher-clear-cache')
.onClick(async () => {
try {
await this.plugin.cacheManager.clearAllCache();
new Notice('Cache cleared successfully');
this.updateCacheInfo(cacheInfoEl);
} catch (error) {
new Notice('Failed to clear cache: ' + error.message);
}
}));
containerEl.createEl('h3', {text: 'Data Fetcher Settings'});
// General settings
new Setting(containerEl)
.setName('Cache Duration')
.setDesc('How long to cache results (in minutes)')
.addText(text => text
.setPlaceholder('60')
.setValue(this.plugin.settings.cacheDuration.toString())
.onChange(async (value) => {
this.plugin.settings.cacheDuration = parseInt(value) || 60;
await this.plugin.saveSettings();
}));
// Endpoint aliases section
containerEl.createEl('h3', {text: 'Endpoint Aliases'});
const endpointList = containerEl.createEl('div', {cls: 'endpoint-list'});
// Create UI for each existing endpoint
this.plugin.settings.endpoints.forEach((endpoint, index) => {
this.createEndpointSetting(endpointList, endpoint, index);
});
// Add new endpoint button
new Setting(containerEl)
.addButton(button => button
.setButtonText('Add Endpoint')
.onClick(async () => {
this.plugin.settings.endpoints.push({
alias: '',
url: '',
method: 'GET',
type: 'rest',
headers: {}
});
await this.plugin.saveSettings();
this.display();
}));
}
createEndpointSetting(container: HTMLElement, endpoint: any, index: number) {
const endpointEl = container.createEl('div', {cls: 'endpoint-item'});
new Setting(endpointEl)
.setName('Alias')
.addText(text => text
.setPlaceholder('api-name')
.setValue(endpoint.alias)
.onChange(async (value) => {
this.plugin.settings.endpoints[index].alias = value;
await this.plugin.saveSettings();
}));
new Setting(endpointEl)
.setName('Type')
.addDropdown(dropdown => dropdown
.addOption('rest', 'REST')
.addOption('graphql', 'GraphQL')
.addOption('grpc', 'gRPC')
.addOption('rpc', 'RPC')
.setValue(endpoint.type)
.onChange(async (value) => {
const validTypes = ["rest", "graphql", "grpc", "rpc"];
if (validTypes.includes(value)) {
this.plugin.settings.endpoints[index].type = value as "rest" | "graphql" | "grpc" | "rpc";
} else {
// Default to REST if invalid type
this.plugin.settings.endpoints[index].type = "rest";
}
await this.plugin.saveSettings();
this.display();
}));
new Setting(endpointEl)
.setName('URL')
.addText(text => text
.setPlaceholder('https://api.example.com')
.setValue(endpoint.url)
.onChange(async (value) => {
this.plugin.settings.endpoints[index].url = value;
await this.plugin.saveSettings();
}));
// Add different fields based on endpoint type
if (endpoint.type === 'rest' || endpoint.type === 'rpc') {
new Setting(endpointEl)
.setName('Method')
.addDropdown(dropdown => dropdown
.addOption('GET', 'GET')
.addOption('POST', 'POST')
.addOption('PUT', 'PUT')
.addOption('DELETE', 'DELETE')
.setValue(endpoint.method)
.onChange(async (value) => {
this.plugin.settings.endpoints[index].method = value;
await this.plugin.saveSettings();
}));
}
// Add headers section
new Setting(endpointEl)
.setName('Headers')
.setDesc('Add headers for this endpoint')
.addButton(button => button
.setButtonText('Manage Headers')
.onClick(() => {
new HeadersModal(this.app, endpoint.headers, async (headers) => {
this.plugin.settings.endpoints[index].headers = headers;
await this.plugin.saveSettings();
}).open();
}));
// Delete button
new Setting(endpointEl)
.addButton(button => button
.setButtonText('Delete')
.setClass('data-fetcher-delete-btn')
.onClick(async () => {
this.plugin.settings.endpoints.splice(index, 1);
await this.plugin.saveSettings();
this.display();
}));
}
async updateCacheInfo(containerEl: HTMLElement) {
const cacheInfo = await this.plugin.cacheManager.getCacheInfo();
containerEl.empty();
const formatSize = (bytes: number): string => {
if (bytes < 1024) return bytes + ' bytes';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
};
containerEl.createEl('div', {
text: `Cache contains ${cacheInfo.count} items (${formatSize(cacheInfo.size)})`
});
}
}
class HeadersModal extends Modal {
headers: Record<string, string>;
onSubmit: (headers: Record<string, string>) => void;
constructor(app: App, headers: Record<string, string>, onSubmit: (headers: Record<string, string>) => void) {
super(app);
this.headers = {...headers};
this.onSubmit = onSubmit;
}
onOpen() {
const {contentEl} = this;
contentEl.createEl('h2', {text: 'Manage Headers'});
// Display existing headers
Object.entries(this.headers).forEach(([key, value]) => {
this.createHeaderRow(contentEl, key, value);
});
// Add new header button
const addBtn = contentEl.createEl('button', {text: 'Add Header'});
addBtn.addEventListener('click', () => {
this.createHeaderRow(contentEl, '', '');
});
// Save button
const saveBtn = contentEl.createEl('button', {text: 'Save', cls: 'mod-cta'});
saveBtn.addEventListener('click', () => {
this.onSubmit(this.headers);
this.close();
});
}
createHeaderRow(container: HTMLElement, key: string, value: string) {
const row = container.createEl('div', {cls: 'header-row'});
const keyInput = row.createEl('input', {
attr: {
type: 'text',
placeholder: 'Header name',
value: key
}
});
const valueInput = row.createEl('input', {
attr: {
type: 'text',
placeholder: 'Value',
value: value
}
});
const deleteBtn = row.createEl('button', {text: 'X'});
// Event listeners
const oldKey = key;
keyInput.addEventListener('change', () => {
if (oldKey) {
delete this.headers[oldKey];
}
this.headers[keyInput.value] = valueInput.value;
});
valueInput.addEventListener('change', () => {
this.headers[keyInput.value] = valueInput.value;
});
deleteBtn.addEventListener('click', () => {
if (keyInput.value) {
delete this.headers[keyInput.value];
}
row.remove();
});
}
onClose() {
const {contentEl} = this;
contentEl.empty();
}
}

10
manifest.json Normal file
View file

@ -0,0 +1,10 @@
{
"id": "obsidian-api-fetcher",
"name": "Data Fetcher",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "Fetch data from multiple sources (REST APIs, RPC, gRPC, GraphQL) and insert results into notes",
"author": "qf3l3k",
"authorUrl": "https://github.com/qf3l3k/obsidian-api-fetcher",
"isDesktopOnly": false
}

2227
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

24
package.json Normal file
View file

@ -0,0 +1,24 @@
{
"name": "obsidian-api-fetcher",
"version": "1.0.0",
"description": "Fetch data from multiple sources (REST APIs, RPC, gRPC, GraphQL) and insert results into notes",
"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"
},
"keywords": ["obsidian", "plugin", "data", "api", "fetch"],
"author": "qf3l3k",
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4"
}
}

143
src/cacheManager.ts Normal file
View file

@ -0,0 +1,143 @@
import { App } from 'obsidian';
import { QueryParams, QueryResult } from './queryEngine';
import * as crypto from 'crypto';
export class CacheManager {
private app: App;
private plugin: any;
private cache: Record<string, QueryResult> = {};
constructor(app: App, plugin: any) {
this.app = app;
this.plugin = plugin;
this.loadCache();
}
/**
* Load cache from plugin data
*/
private async loadCache(): Promise<void> {
try {
const data = await this.plugin.loadData();
this.cache = data?.cache || {};
} catch (error) {
console.error('Failed to load cache:', error);
this.cache = {};
}
}
/**
* Save cache to plugin data
*/
private async saveCache(): Promise<void> {
try {
const data = await this.plugin.loadData() || {};
data.cache = this.cache;
await this.plugin.saveData(data);
} catch (error) {
console.error('Failed to save cache:', error);
}
}
/**
* Generate a cache key for a query
*/
private generateCacheKey(params: QueryParams): string {
// Create a unique key based on query parameters
const stringToHash = JSON.stringify({
url: params.url,
type: params.type,
method: params.method,
body: params.body,
query: params.query,
variables: params.variables
});
return crypto.createHash('md5').update(stringToHash).digest('hex');
}
/**
* Get cached result for a query
*/
async getFromCache(params: QueryParams): Promise<QueryResult | null> {
try {
const cacheKey = this.generateCacheKey(params);
const cachedItem = this.cache[cacheKey];
if (!cachedItem) {
return null;
}
// Check if cache is expired
const now = Date.now();
const cacheAge = now - cachedItem.timestamp;
const cacheDurationMs = this.plugin.settings.cacheDuration * 60 * 1000;
if (cacheAge > cacheDurationMs) {
delete this.cache[cacheKey];
await this.saveCache();
return null; // Cache is expired
}
return cachedItem;
} catch (error) {
console.error('Error reading from cache:', error);
return null;
}
}
/**
* Save result to cache
*/
async saveToCache(params: QueryParams, result: QueryResult): Promise<void> {
try {
const cacheKey = this.generateCacheKey(params);
this.cache[cacheKey] = result;
await this.saveCache();
} catch (error) {
console.error('Error saving to cache:', error);
}
}
/**
* Clear a specific cache entry
*/
async clearCacheEntry(params: QueryParams): Promise<void> {
try {
const cacheKey = this.generateCacheKey(params);
delete this.cache[cacheKey];
await this.saveCache();
} catch (error) {
console.error('Error clearing cache entry:', error);
}
}
/**
* Clear all cache
*/
async clearAllCache(): Promise<void> {
try {
this.cache = {};
await this.saveCache();
} catch (error) {
console.error('Error clearing all cache:', error);
}
}
/**
* Get cache size information
* Returns the number of items and total size in bytes
*/
async getCacheInfo(): Promise<{count: number, size: number}> {
try {
const count = Object.keys(this.cache).length;
const cacheString = JSON.stringify(this.cache);
const size = new TextEncoder().encode(cacheString).length;
return { count, size };
} catch (error) {
console.error('Error getting cache info:', error);
return { count: 0, size: 0 };
}
}
}

279
src/queryEngine.ts Normal file
View file

@ -0,0 +1,279 @@
import { DataFetcherSettings } from './settings';
import { requestUrl, RequestUrlParam } from 'obsidian';
export interface QueryParams {
endpoint: string;
type: 'rest' | 'graphql' | 'grpc' | 'rpc';
url?: string;
method?: string;
headers?: Record<string, string>;
body?: any;
query?: string;
variables?: Record<string, any>;
}
export interface QueryResult {
data: any;
timestamp: number;
error?: string;
}
/**
* Parse the data query from the codeblock
*/
export function parseDataQuery(source: string, settings: DataFetcherSettings): QueryParams {
try {
// Check if using reference or direct definition
if (source.trim().startsWith('@')) {
// Using a reference to predefined endpoint
const lines = source.trim().split('\n');
const aliasLine = lines[0].trim();
const alias = aliasLine.substring(1); // Remove the @ prefix
// Find the endpoint by alias
const endpoint = settings.endpoints.find(e => e.alias === alias);
if (!endpoint) {
throw new Error(`Endpoint alias "${alias}" not found in settings`);
}
// Parse additional parameters if any
const queryParams: QueryParams = {
endpoint: alias,
type: endpoint.type,
url: endpoint.url,
method: endpoint.method,
headers: { ...endpoint.headers }
};
// Process remaining lines as additional parameters
for (let i = 1; i < lines.length; i++) {
const line = lines[i].trim();
// Skip empty lines
if (!line) continue;
// Check if it's a parameter assignment
if (line.includes(':')) {
const [key, ...valueParts] = line.split(':');
let value = valueParts.join(':').trim();
// Handle multi-line values (especially for GraphQL queries)
if (key.trim() === 'query' && i + 1 < lines.length) {
// Collect all lines until we hit a line that looks like a new parameter
for (let j = i + 1; j < lines.length; j++) {
const nextLine = lines[j].trim();
if (nextLine && !nextLine.includes(':')) {
value += '\n' + nextLine;
i = j; // Update the outer loop counter
} else if (nextLine.includes(':')) {
break; // Stop when we hit a new parameter
}
}
}
if (key.trim() === 'body') {
try {
queryParams.body = JSON.parse(value);
} catch {
queryParams.body = value;
}
} else if (key.trim() === 'query') {
queryParams.query = value;
} else if (key.trim() === 'variables') {
try {
queryParams.variables = JSON.parse(value);
} catch {
throw new Error('Variables must be valid JSON');
}
}
}
}
return queryParams;
} else {
// Direct definition
try {
const queryObj = JSON.parse(source);
// Validate required fields
if (!queryObj.type) {
throw new Error('Query type is required');
}
if (!queryObj.url) {
throw new Error('URL is required');
}
return {
endpoint: 'direct',
...queryObj
};
} catch (error) {
throw new Error(`Invalid query format: ${error.message}`);
}
}
} catch (error) {
throw new Error(`Failed to parse query: ${error.message}`);
}
}
/**
* Execute the query based on the parsed parameters
*/
export async function executeQuery(params: QueryParams): Promise<QueryResult> {
try {
let data: any;
switch (params.type) {
case 'rest':
data = await executeRestQuery(params);
break;
case 'graphql':
data = await executeGraphQLQuery(params);
break;
case 'grpc':
data = await executeGrpcQuery(params);
break;
case 'rpc':
data = await executeRpcQuery(params);
break;
default:
throw new Error(`Unsupported query type: ${params.type}`);
}
return {
data,
timestamp: Date.now()
};
} catch (error) {
console.error('Query execution error:', error);
return {
data: null,
timestamp: Date.now(),
error: error.message
};
}
}
/**
* Execute REST API query
*/
async function executeRestQuery(params: QueryParams): Promise<any> {
if (!params.url) {
throw new Error('URL is required for REST queries');
}
const requestParams: RequestUrlParam = {
url: params.url,
method: params.method || 'GET',
headers: params.headers || {},
};
if (params.body) {
if (typeof params.body === 'object') {
requestParams.body = JSON.stringify(params.body);
if (!requestParams.headers) {
requestParams.headers = {};
}
if (!requestParams.headers['Content-Type']) {
requestParams.headers['Content-Type'] = 'application/json';
}
} else {
requestParams.body = params.body;
}
}
const response = await requestUrl(requestParams);
// Parse response based on content type
const contentType = response.headers['content-type'];
if (contentType && contentType.includes('application/json')) {
return response.json;
} else {
return response.text;
}
}
/**
* Execute GraphQL query
*/
async function executeGraphQLQuery(params: QueryParams): Promise<any> {
if (!params.url) {
throw new Error('URL is required for GraphQL queries');
}
if (!params.query) {
throw new Error('Query is required for GraphQL queries');
}
const requestParams: RequestUrlParam = {
url: params.url,
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(params.headers || {})
},
body: JSON.stringify({
query: params.query,
variables: params.variables || {}
})
};
const response = await requestUrl(requestParams);
return response.json;
}
/**
* Execute gRPC query
* Note: This is a simplified implementation as Obsidian doesn't have direct gRPC support
* This will use a REST proxy approach for gRPC
*/
async function executeGrpcQuery(params: QueryParams): Promise<any> {
if (!params.url) {
throw new Error('URL is required for gRPC queries');
}
// For gRPC, we're assuming a REST proxy is set up
// We'll send a POST request with the appropriate parameters
const requestParams: RequestUrlParam = {
url: params.url,
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(params.headers || {})
},
body: JSON.stringify(params.body || {})
};
const response = await requestUrl(requestParams);
return response.json;
}
/**
* Execute RPC query
*/
async function executeRpcQuery(params: QueryParams): Promise<any> {
if (!params.url) {
throw new Error('URL is required for RPC queries');
}
const requestParams: RequestUrlParam = {
url: params.url,
method: params.method || 'POST',
headers: {
'Content-Type': 'application/json',
...(params.headers || {})
},
body: JSON.stringify({
jsonrpc: '2.0',
method: params.query || 'method',
params: params.body || {},
id: 1
})
};
const response = await requestUrl(requestParams);
return response.json;
}

19
src/settings.ts Normal file
View file

@ -0,0 +1,19 @@
export interface EndpointConfig {
alias: string;
url: string;
method: string;
type: 'rest' | 'graphql' | 'grpc' | 'rpc';
headers: Record<string, string>;
body?: string;
query?: string;
}
export interface DataFetcherSettings {
cacheDuration: number; // in minutes
endpoints: EndpointConfig[];
}
export const DEFAULT_SETTINGS: DataFetcherSettings = {
cacheDuration: 60,
endpoints: []
}

102
styles.css Normal file
View file

@ -0,0 +1,102 @@
/* Data Fetcher Plugin Styles */
.data-fetcher-result {
margin: 10px 0;
border: 1px solid var(--background-modifier-border);
border-radius: 5px;
overflow: hidden;
}
.data-fetcher-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background-color: var(--background-secondary);
border-bottom: 1px solid var(--background-modifier-border);
}
.data-fetcher-timestamp {
font-size: 0.8em;
color: var(--text-muted);
}
.data-fetcher-refresh {
font-size: 0.8em;
background-color: var(--interactive-accent);
color: var(--text-on-accent);
border: none;
border-radius: 4px;
padding: 4px 8px;
cursor: pointer;
}
.data-fetcher-refresh:hover {
background-color: var(--interactive-accent-hover);
}
.data-fetcher-content {
padding: 12px;
background-color: var(--background-primary);
overflow-x: auto;
}
.data-fetcher-content pre {
margin: 0;
white-space: pre-wrap;
}
.data-fetcher-error {
padding: 10px;
color: var(--text-error);
background-color: var(--background-modifier-error);
border-radius: 5px;
}
.data-fetcher-loading {
padding: 10px;
color: var(--text-muted);
font-style: italic;
}
/* Settings styles */
.endpoint-item {
margin-bottom: 20px;
padding: 15px;
border: 1px solid var(--background-modifier-border);
border-radius: 5px;
background-color: var(--background-secondary);
}
.endpoint-list {
margin-bottom: 20px;
}
.header-row {
display: flex;
margin-bottom: 8px;
}
.header-row input {
flex-grow: 1;
margin-right: 8px;
}
.data-fetcher-delete-btn {
color: var(--text-error);
}
.data-fetcher-clear-cache {
background-color: var(--text-error) !important;
color: var(--text-on-accent) !important;
}
.data-fetcher-clear-cache:hover {
background-color: var(--text-error-hover) !important;
}
.data-fetcher-cache-info {
margin-bottom: 10px;
font-size: 0.9em;
color: var(--text-muted);
}

24
tsconfig.json Normal file
View file

@ -0,0 +1,24 @@
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"lib": [
"DOM",
"ES5",
"ES6",
"ES7"
]
},
"include": [
"**/*.ts"
]
}

14
version-bump.mjs Normal file
View file

@ -0,0 +1,14 @@
import { readFileSync, writeFileSync } from "fs";
const targetVersion = process.env.npm_package_version;
// read minAppVersion from manifest.json and bump version to target version
let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
const { minAppVersion } = manifest;
manifest.version = targetVersion;
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
// update versions.json with target version and minAppVersion from manifest.json
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
versions[targetVersion] = minAppVersion;
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));

3
versions.json Normal file
View file

@ -0,0 +1,3 @@
{
"1.0.0": "0.15.0"
}