mirror of
https://github.com/qf3l3k/obsidian-data-fetcher.git
synced 2026-07-22 05:43:10 +00:00
release: 1.1.3
This commit is contained in:
parent
b7baa0b6e6
commit
2a14c38b3b
9 changed files with 106 additions and 64 deletions
|
|
@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.1.3] - 2026-03-21
|
||||
|
||||
### Fixed
|
||||
- Cache keys now include endpoint identity and headers to avoid cross-endpoint cache collisions.
|
||||
- RPC requests now always use `POST`, matching JSON-RPC expectations and improving mobile compatibility.
|
||||
- Endpoint editor now blocks duplicate aliases, and endpoint import deduplicates aliases before merge/replace.
|
||||
- `Save to Note` no longer falls back to replacing the first `data-query` block when source block location is unavailable.
|
||||
- Large-response request errors now surface a clearer hint about reducing payload size or using a proxy.
|
||||
|
||||
## [1.1.2] - 2026-03-03
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ Supported endpoint types:
|
|||
- `data-query` code block processor for live request execution
|
||||
- Endpoint alias configuration in plugin settings
|
||||
- Compact endpoint list with modal editor (v1.1.1)
|
||||
- Endpoint import/export for moving aliases between devices (v1.1.2)
|
||||
- Cache with configurable expiration
|
||||
- Cache browser modal (list, preview, delete individual entries)
|
||||
- Optional ribbon icon shortcut for cache browser
|
||||
|
|
@ -162,6 +163,7 @@ Example:
|
|||
Common fields:
|
||||
- `type`: `rpc`
|
||||
- `url`: RPC endpoint URL
|
||||
- `method`: always `POST`
|
||||
- `query`: method name
|
||||
- `body`: params object
|
||||
- `headers`: object
|
||||
|
|
@ -181,6 +183,7 @@ Open `Settings -> Data Fetcher`.
|
|||
Available options:
|
||||
- Cache duration (minutes)
|
||||
- Endpoint aliases (compact list with Name/Type/URL + actions)
|
||||
- Endpoint import/export (JSON)
|
||||
- Per-alias headers
|
||||
- Cache clearing
|
||||
- Cache browser shortcut
|
||||
|
|
@ -220,6 +223,7 @@ Use command `Open cache browser` (or settings button) to:
|
|||
- `Path "..." not found`: check nested field names/indexes in response data.
|
||||
- `Table format requires an array of objects`: update `path` to point at an object array, or use `format: json`.
|
||||
- `property is required when output: frontmatter is used`: add a property path.
|
||||
- `Response too large for this device`: reduce payload size, add filters/limits, or use a proxy endpoint.
|
||||
- No result refresh from command: ensure active pane is a markdown file.
|
||||
- Build fails on PowerShell script policy: run via `cmd /c npm run build`.
|
||||
|
||||
|
|
|
|||
129
main.ts
129
main.ts
|
|
@ -674,49 +674,8 @@ export default class DataFetcherPlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
// Fallback: Try to use the Obsidian view to locate the code block
|
||||
// This is a secondary approach that might work in some cases
|
||||
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (markdownView && markdownView.getMode() === 'source') {
|
||||
// Try to find the code block in the source by looking for data-query blocks
|
||||
const text = editor.getValue();
|
||||
const lines = text.split('\n');
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i].trim() === '```data-query') {
|
||||
// Found a start marker, now find the end
|
||||
let endLine = -1;
|
||||
for (let j = i + 1; j < lines.length; j++) {
|
||||
if (lines[j].trim() === '```') {
|
||||
endLine = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (endLine > i) {
|
||||
// Replace the entire code block
|
||||
const start = { line: i, ch: 0 };
|
||||
const end = { line: endLine, ch: lines[endLine].length };
|
||||
|
||||
editor.transaction({
|
||||
changes: [
|
||||
{
|
||||
from: start,
|
||||
to: end,
|
||||
text: commentedData
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
new Notice('Data block replaced with static content');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If all attempts to find the code block failed, insert at cursor position
|
||||
const cursor = editor.getCursor();
|
||||
// If exact block location is unavailable, avoid replacing the wrong data-query block.
|
||||
const cursor = editor.getCursor();
|
||||
|
||||
editor.transaction({
|
||||
changes: [
|
||||
|
|
@ -728,7 +687,7 @@ export default class DataFetcherPlugin extends Plugin {
|
|||
]
|
||||
});
|
||||
|
||||
new Notice('Data saved to note at cursor position');
|
||||
new Notice('Block location unavailable, so data was inserted at the cursor position');
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error saving data to note:", error);
|
||||
|
|
@ -850,7 +809,7 @@ class DataFetcherSettingTab extends PluginSettingTab {
|
|||
.setCta()
|
||||
.onClick(() => {
|
||||
const newEndpoint = this.buildDefaultEndpoint();
|
||||
new EndpointEditorModal(this.app, newEndpoint, async (savedEndpoint) => {
|
||||
new EndpointEditorModal(this.app, newEndpoint, this.collectExistingAliases(), async (savedEndpoint) => {
|
||||
this.plugin.settings.endpoints.push(savedEndpoint);
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
|
|
@ -868,6 +827,21 @@ class DataFetcherSettingTab extends PluginSettingTab {
|
|||
};
|
||||
}
|
||||
|
||||
private collectExistingAliases(excludeAlias?: string): Set<string> {
|
||||
const aliases = new Set<string>();
|
||||
for (const endpoint of this.plugin.settings.endpoints) {
|
||||
const alias = endpoint.alias?.trim();
|
||||
if (!alias) {
|
||||
continue;
|
||||
}
|
||||
if (excludeAlias && alias === excludeAlias) {
|
||||
continue;
|
||||
}
|
||||
aliases.add(alias);
|
||||
}
|
||||
return aliases;
|
||||
}
|
||||
|
||||
private endpointTypeLabel(type: EndpointConfig['type']): string {
|
||||
if (type === 'rest') return 'REST';
|
||||
if (type === 'graphql') return 'GraphQL';
|
||||
|
|
@ -920,7 +894,7 @@ class DataFetcherSettingTab extends PluginSettingTab {
|
|||
|
||||
actions.createEl('button', { text: 'Edit' }).addEventListener('click', () => {
|
||||
const draft = { ...endpoint, headers: { ...(endpoint.headers || {}) } };
|
||||
new EndpointEditorModal(this.app, draft, async (savedEndpoint) => {
|
||||
new EndpointEditorModal(this.app, draft, this.collectExistingAliases(endpoint.alias), async (savedEndpoint) => {
|
||||
this.plugin.settings.endpoints[index] = savedEndpoint;
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
|
|
@ -934,7 +908,7 @@ class DataFetcherSettingTab extends PluginSettingTab {
|
|||
alias: duplicateAlias,
|
||||
headers: { ...(endpoint.headers || {}) }
|
||||
};
|
||||
new EndpointEditorModal(this.app, draft, async (savedEndpoint) => {
|
||||
new EndpointEditorModal(this.app, draft, this.collectExistingAliases(), async (savedEndpoint) => {
|
||||
this.plugin.settings.endpoints.push(savedEndpoint);
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
|
|
@ -1018,7 +992,7 @@ class DataFetcherSettingTab extends PluginSettingTab {
|
|||
|
||||
const type = typeValue as EndpointConfig['type'];
|
||||
let method = typeof endpoint.method === 'string' ? endpoint.method.toUpperCase() : 'GET';
|
||||
if (type === 'graphql' || type === 'grpc') {
|
||||
if (type === 'graphql' || type === 'grpc' || type === 'rpc') {
|
||||
method = 'POST';
|
||||
}
|
||||
|
||||
|
|
@ -1050,6 +1024,23 @@ class DataFetcherSettingTab extends PluginSettingTab {
|
|||
return [];
|
||||
}
|
||||
|
||||
private dedupeImportedEndpoints(endpoints: EndpointConfig[]): { endpoints: EndpointConfig[]; duplicateAliases: number } {
|
||||
const deduped = new Map<string, EndpointConfig>();
|
||||
let duplicateAliases = 0;
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
if (deduped.has(endpoint.alias)) {
|
||||
duplicateAliases++;
|
||||
}
|
||||
deduped.set(endpoint.alias, endpoint);
|
||||
}
|
||||
|
||||
return {
|
||||
endpoints: Array.from(deduped.values()),
|
||||
duplicateAliases
|
||||
};
|
||||
}
|
||||
|
||||
private async importEndpoints(): Promise<void> {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
|
|
@ -1086,12 +1077,15 @@ class DataFetcherSettingTab extends PluginSettingTab {
|
|||
return;
|
||||
}
|
||||
|
||||
new EndpointImportModeModal(this.app, validEndpoints.length, skipped, async (mode) => {
|
||||
const dedupedImport = this.dedupeImportedEndpoints(validEndpoints);
|
||||
const skippedTotal = skipped + dedupedImport.duplicateAliases;
|
||||
|
||||
new EndpointImportModeModal(this.app, dedupedImport.endpoints.length, skippedTotal, async (mode) => {
|
||||
if (mode === 'replace') {
|
||||
this.plugin.settings.endpoints = validEndpoints;
|
||||
this.plugin.settings.endpoints = dedupedImport.endpoints;
|
||||
} else {
|
||||
const merged = [...this.plugin.settings.endpoints];
|
||||
for (const endpoint of validEndpoints) {
|
||||
for (const endpoint of dedupedImport.endpoints) {
|
||||
const existingIndex = merged.findIndex(item => item.alias === endpoint.alias);
|
||||
if (existingIndex >= 0) {
|
||||
merged[existingIndex] = endpoint;
|
||||
|
|
@ -1105,7 +1099,7 @@ class DataFetcherSettingTab extends PluginSettingTab {
|
|||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
const modeLabel = mode === 'replace' ? 'replaced' : 'merged';
|
||||
new Notice(`Imported ${validEndpoints.length} endpoint(s), ${skipped} skipped (${modeLabel})`);
|
||||
new Notice(`Imported ${dedupedImport.endpoints.length} endpoint(s), ${skippedTotal} skipped (${modeLabel})`);
|
||||
}).open();
|
||||
} catch (error) {
|
||||
new Notice(`Import failed: ${error.message}`);
|
||||
|
|
@ -1177,17 +1171,20 @@ class EndpointImportModeModal extends Modal {
|
|||
|
||||
class EndpointEditorModal extends Modal {
|
||||
private endpoint: EndpointConfig;
|
||||
private existingAliases: Set<string>;
|
||||
private onSubmit: (endpoint: EndpointConfig) => void;
|
||||
private methodSettingEl: HTMLElement | null = null;
|
||||
private headersSummaryEl: HTMLElement | null = null;
|
||||
private rpcMethodNoticeEl: HTMLElement | null = null;
|
||||
|
||||
constructor(app: App, endpoint: EndpointConfig, onSubmit: (endpoint: EndpointConfig) => void) {
|
||||
constructor(app: App, endpoint: EndpointConfig, existingAliases: Set<string>, onSubmit: (endpoint: EndpointConfig) => void) {
|
||||
super(app);
|
||||
this.endpoint = {
|
||||
...endpoint,
|
||||
headers: { ...(endpoint.headers || {}) },
|
||||
method: endpoint.method || 'GET'
|
||||
};
|
||||
this.existingAliases = existingAliases;
|
||||
this.onSubmit = onSubmit;
|
||||
}
|
||||
|
||||
|
|
@ -1204,8 +1201,21 @@ class EndpointEditorModal extends Modal {
|
|||
this.methodSettingEl.remove();
|
||||
this.methodSettingEl = null;
|
||||
}
|
||||
if (this.rpcMethodNoticeEl) {
|
||||
this.rpcMethodNoticeEl.remove();
|
||||
this.rpcMethodNoticeEl = null;
|
||||
}
|
||||
|
||||
if (this.endpoint.type !== 'rest' && this.endpoint.type !== 'rpc') {
|
||||
if (this.endpoint.type === 'rpc') {
|
||||
this.endpoint.method = 'POST';
|
||||
this.rpcMethodNoticeEl = containerEl.createDiv();
|
||||
new Setting(this.rpcMethodNoticeEl)
|
||||
.setName('Method')
|
||||
.setDesc('JSON-RPC requests always use POST for compatibility across Obsidian desktop and mobile.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.endpoint.type !== 'rest') {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1290,7 +1300,8 @@ class EndpointEditorModal extends Modal {
|
|||
const actions = contentEl.createEl('div', { cls: 'data-fetcher-endpoint-editor-actions' });
|
||||
actions.createEl('button', { text: 'Cancel' }).addEventListener('click', () => this.close());
|
||||
actions.createEl('button', { text: 'Save', cls: 'mod-cta' }).addEventListener('click', () => {
|
||||
if (!this.endpoint.alias?.trim()) {
|
||||
const normalizedAlias = this.endpoint.alias?.trim();
|
||||
if (!normalizedAlias) {
|
||||
new Notice('Alias is required.');
|
||||
return;
|
||||
}
|
||||
|
|
@ -1298,13 +1309,17 @@ class EndpointEditorModal extends Modal {
|
|||
new Notice('URL is required.');
|
||||
return;
|
||||
}
|
||||
if (this.existingAliases.has(normalizedAlias)) {
|
||||
new Notice(`Alias "${normalizedAlias}" already exists.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.endpoint.type === 'graphql' || this.endpoint.type === 'grpc') {
|
||||
if (this.endpoint.type === 'graphql' || this.endpoint.type === 'grpc' || this.endpoint.type === 'rpc') {
|
||||
this.endpoint.method = 'POST';
|
||||
}
|
||||
|
||||
this.onSubmit({
|
||||
alias: this.endpoint.alias.trim(),
|
||||
alias: normalizedAlias,
|
||||
type: this.endpoint.type,
|
||||
url: this.endpoint.url.trim(),
|
||||
method: this.endpoint.method || 'GET',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "data-fetcher",
|
||||
"name": "Data Fetcher",
|
||||
"version": "1.1.2",
|
||||
"version": "1.1.3",
|
||||
"minAppVersion": "1.8.3",
|
||||
"description": "Fetch data from multiple sources (REST APIs, RPC, gRPC, GraphQL) and insert results into notes",
|
||||
"author": "qf3l3k",
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "obsidian-api-fetcher",
|
||||
"version": "1.1.2",
|
||||
"version": "1.1.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-api-fetcher",
|
||||
"version": "1.1.2",
|
||||
"version": "1.1.3",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.6",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "obsidian-api-fetcher",
|
||||
"version": "1.1.2",
|
||||
"version": "1.1.3",
|
||||
"description": "Fetch data from multiple sources (REST APIs, RPC, gRPC, GraphQL) and insert results into notes",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -33,9 +33,11 @@ export class CacheManager {
|
|||
private generateCacheKey(params: QueryParams): string {
|
||||
// Create a unique key based on query parameters
|
||||
const stringToHash = JSON.stringify({
|
||||
endpoint: params.endpoint,
|
||||
url: params.url,
|
||||
type: params.type,
|
||||
method: params.method,
|
||||
headers: params.headers,
|
||||
body: params.body,
|
||||
query: params.query,
|
||||
variables: params.variables
|
||||
|
|
|
|||
|
|
@ -22,6 +22,17 @@ export interface QueryResult {
|
|||
error?: string;
|
||||
}
|
||||
|
||||
function formatRequestError(error: unknown): string {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const normalized = message.toLowerCase();
|
||||
|
||||
if (normalized.includes('resource exceeds maximum size')) {
|
||||
return 'Response too large for this device. Reduce payload size, add filters/limits, or use a proxy endpoint.';
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
function parseOutputFormat(value: string): 'json' | 'table' {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (normalized === 'json' || normalized === 'table') {
|
||||
|
|
@ -222,7 +233,7 @@ export async function executeQuery(params: QueryParams): Promise<QueryResult> {
|
|||
return {
|
||||
data: null,
|
||||
timestamp: Date.now(),
|
||||
error: error.message
|
||||
error: formatRequestError(error)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -333,7 +344,7 @@ async function executeRpcQuery(params: QueryParams): Promise<any> {
|
|||
|
||||
const requestParams: RequestUrlParam = {
|
||||
url: params.url,
|
||||
method: params.method || 'POST',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(params.headers || {})
|
||||
|
|
|
|||
|
|
@ -6,5 +6,6 @@
|
|||
"1.0.9": "1.8.3",
|
||||
"1.1.0": "1.8.3",
|
||||
"1.1.1": "1.8.3",
|
||||
"1.1.2": "1.8.3"
|
||||
"1.1.2": "1.8.3",
|
||||
"1.1.3": "1.8.3"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue