release: 1.1.3

This commit is contained in:
qf3l3k 2026-03-21 09:56:17 +01:00
parent b7baa0b6e6
commit 2a14c38b3b
9 changed files with 106 additions and 64 deletions

View file

@ -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

View file

@ -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
View file

@ -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',

View file

@ -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
View file

@ -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",

View file

@ -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": {

View file

@ -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

View file

@ -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 || {})

View file

@ -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"
}