From 5ae08dfccee4aa0ddba4dc9efe7e80fb66cbf37d Mon Sep 17 00:00:00 2001 From: Alamion Date: Sat, 20 Sep 2025 21:33:36 +0300 Subject: [PATCH] Different api versions update - Now plugin supports both v2 and v3 Jira API - Some preparation for solving bottleneck problem --- manifest.json | 2 +- package.json | 2 +- src/api/base.ts | 3 +- src/api/issues.ts | 91 ++++++++++++++++--- .../source/en/settings/connection.yaml | 8 ++ .../components/ConnectionSettingsComponent.ts | 14 +++ src/settings/default.ts | 2 + src/tools/asyncLimiter.ts | 31 +++++++ 8 files changed, 135 insertions(+), 18 deletions(-) create mode 100644 src/tools/asyncLimiter.ts diff --git a/manifest.json b/manifest.json index f2858a2..03ed0e8 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "jira-sync", "name": "Jira Issue Manager", - "version": "1.3.0", + "version": "1.3.1", "minAppVersion": "1.7.7", "description": "Get Jira issues, create and update them. Issue status and worklog management.", "author": "Alamion", diff --git a/package.json b/package.json index 9086efa..6eb0ac4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-sample-plugin", - "version": "1.3.0", + "version": "1.3.1", "description": "This is a sample plugin for Obsidian (https://obsidian.md)", "main": "main.js", "scripts": { diff --git a/src/api/base.ts b/src/api/base.ts index e4146f9..cc4ddcd 100644 --- a/src/api/base.ts +++ b/src/api/base.ts @@ -16,7 +16,7 @@ export async function baseRequest( ? "?" + new URLSearchParams(params).toString() : ""; - const url = `${plugin.settings.jiraUrl}/rest/api/2${additional_url_path}${queryString}`; + const url = `${plugin.settings.jiraUrl}/rest/api/${plugin.settings.apiVersion}${additional_url_path}${queryString}`; const requestParams = { url, method: method, @@ -58,3 +58,4 @@ export function sanitizeObject(obj: any): any { return obj; } + diff --git a/src/api/issues.ts b/src/api/issues.ts index f65252e..69e36d6 100644 --- a/src/api/issues.ts +++ b/src/api/issues.ts @@ -2,6 +2,8 @@ import JiraPlugin from "../main"; import {JiraIssue, JiraTransitionType} from "../interfaces"; import {baseRequest, sanitizeObject} from "./base"; import {Notice} from "obsidian"; +import {chunkArray, createLimiter} from "../tools/asyncLimiter"; + /** * Fetch an issue from Jira by its key @@ -10,8 +12,7 @@ import {Notice} from "obsidian"; * @returns The issue data */ export async function fetchIssue(plugin: JiraPlugin, issueKey: string): Promise { - const additional_url_path = `/issue/${issueKey}` - return await baseRequest(plugin, 'get', additional_url_path) as Promise; + return await baseRequest(plugin, 'get', `/issue/${issueKey}`) as Promise; } /** @@ -43,6 +44,30 @@ export async function fetchIssuesByJQL( return issues as JiraIssue[]; } +export async function fetchIssuesByJQLParallel( + plugin: JiraPlugin, + jql: string, + limit?: number, + fields?: string[] +): Promise { + const test = await fetchIssuesByJQLRaw(plugin, jql, 1, fields); + const totalAvailable = test.total; + const actualLimit = Math.min(limit || totalAvailable, totalAvailable); + + const tasks: (() => Promise)[] = []; + for (let startAt = 0; startAt < actualLimit; startAt += 1000) { + tasks.push(async () => { + const result = await fetchIssuesByJQLRaw(plugin, jql, 1000, fields, startAt); + return result.issues; + }); + } + + const limitConcurrency = createLimiter(5); + const results = await Promise.all(tasks.map(t => limitConcurrency(t))); + return results.flat(); +} + + /** * Fetch issues by JQL and return the raw search response (includes total, startAt, etc.). * Useful for previewing results and counts. @@ -54,14 +79,13 @@ export async function fetchIssuesByJQLRaw( fields?: string[], startAt?: number ): Promise { - const additional_url_path = `/search`; const body = JSON.stringify(sanitizeObject({ jql, maxResults, startAt, fields: fields && fields.length > 0 ? fields : undefined, })); - return await baseRequest(plugin, 'post', additional_url_path, body); + return await baseRequest(plugin, 'post', `/search`, body); } /** @@ -71,8 +95,7 @@ export async function fetchIssuesByJQLRaw( * @returns The issue data */ export async function fetchIssueTransitions(plugin: JiraPlugin, issueKey: string): Promise { - const additional_url_path = `/issue/${issueKey}/transitions` - const result = await baseRequest(plugin, 'get', additional_url_path); + const result = await baseRequest(plugin, 'get', `/issue/${issueKey}/transitions`); return result.transitions.map(({ id, name, to }: { id: string; name: string; to: { name: string }}) => ({ id, action: name, @@ -92,10 +115,22 @@ export async function updateJiraIssue( fields: Record ): Promise { const body = JSON.stringify({ fields }) - const additional_url_path = `/issue/${issueKey}` - return await baseRequest(plugin, 'put', additional_url_path, body) as Promise; + return await baseRequest(plugin, 'put', `/issue/${issueKey}`, body) as Promise; } + +export async function bulkUpdateJiraIssues( + plugin: JiraPlugin, + updates: { issueKey: string, fields: Record }[] +): Promise { + const limit = createLimiter(5); + const promises = updates.map(update => + limit(() => updateJiraIssue(plugin, update.issueKey, update.fields)) + ); + return await Promise.allSettled(promises); +} + + /** * Create a new issue in Jira * @param plugin The plugin instance @@ -111,10 +146,25 @@ export async function createJiraIssue( throw new Error("Missing required fields: project, issuetype, and summary are required"); } const body = JSON.stringify({ fields }) - const additional_url_path = `/issue/` - return await baseRequest(plugin, 'post', additional_url_path, body) as Promise; + return await baseRequest(plugin, 'post', `/issue/`, body) as Promise; } + +export async function bulkCreateJiraIssues( + plugin: JiraPlugin, + issues: Record[] +): Promise { + const chunks = chunkArray(issues, 50); + let results: any[] = []; + for (const chunk of chunks) { + const body = JSON.stringify({ issueUpdates: chunk }); + const res = await baseRequest(plugin, 'post', '/issue/bulk', body); + results.push(res); + } + return results; +} + + /** * Update an issue status in Jira * @param plugin The plugin instance @@ -127,10 +177,10 @@ export async function updateJiraStatus( status: string ): Promise { const body = JSON.stringify({ transition: { id: status } }) - const additional_url_path = `/issue/${issueKey}/transitions` - return await baseRequest(plugin, 'post', additional_url_path, body) as Promise; + return await baseRequest(plugin, 'post', `/issue/${issueKey}/transitions`, body) as Promise; } + /** * Add a work log entry to a Jira issue */ @@ -147,9 +197,7 @@ export async function addWorkLog( started: startedAt, comment }; - - const additional_url_path = `/issue/${issueKey}/worklog`; - const response = await baseRequest(plugin, 'post', additional_url_path, JSON.stringify(payload)); + const response = await baseRequest(plugin, 'post', `/issue/${issueKey}/worklog`, JSON.stringify(payload)); if (showNotice) { new Notice(`Work log added successfully to ${issueKey}`); @@ -157,3 +205,16 @@ export async function addWorkLog( return response; } + + +export async function bulkAddWorkLog( + plugin: JiraPlugin, + worklogs: { issueKey: string, timeSpent: string, startedAt: string, comment: string }[], + showNotice: boolean = true +): Promise { + const limit = createLimiter(5); + const promises = worklogs.map(worklog => + limit(() => addWorkLog(plugin, worklog.issueKey, worklog.timeSpent, worklog.startedAt, worklog.comment, showNotice)) + ); + return await Promise.allSettled(promises); +} diff --git a/src/localization/source/en/settings/connection.yaml b/src/localization/source/en/settings/connection.yaml index af949a2..1e2f36a 100644 --- a/src/localization/source/en/settings/connection.yaml +++ b/src/localization/source/en/settings/connection.yaml @@ -5,6 +5,14 @@ url: desc: Your Jira instance URL def: https://yourcompany.atlassian.net +api_version: + title: Jira API version + desc: The version of the Jira API to use + options: + 1: 1 + 2: 2 + 3: 3 + auth: title: Authentication method desc: Choose how to authenticate with Jira diff --git a/src/settings/components/ConnectionSettingsComponent.ts b/src/settings/components/ConnectionSettingsComponent.ts index 9aca905..5f3cc62 100644 --- a/src/settings/components/ConnectionSettingsComponent.ts +++ b/src/settings/components/ConnectionSettingsComponent.ts @@ -34,6 +34,20 @@ export class ConnectionSettingsComponent implements SettingsComponent { }) ); + new Setting(containerEl) + .setName(t("api_version.title")) + .setDesc(t("api_version.desc")) + .addDropdown((cb) => + cb + .addOption("2", t("api_version.options.2")) + .addOption("3", t("api_version.options.3")) + .setValue(plugin.settings.apiVersion) + .onChange(async (value: "2" | "3") => { + plugin.settings.apiVersion = value; + await plugin.saveSettings(); + }) + ) + // Auth Method Select new Setting(containerEl) .setName(t("auth.title")) diff --git a/src/settings/default.ts b/src/settings/default.ts index 8912822..2b30338 100644 --- a/src/settings/default.ts +++ b/src/settings/default.ts @@ -9,6 +9,7 @@ export interface JiraSettings { email: string; password: string; jiraUrl: string; + apiVersion: string; issuesFolder: string; sessionCookieName: string; @@ -40,6 +41,7 @@ export const DEFAULT_SETTINGS: JiraSettings = { email: "", password: "", jiraUrl: "", + apiVersion: "2", issuesFolder: "jira-issues", sessionCookieName: "JSESSIONID", diff --git a/src/tools/asyncLimiter.ts b/src/tools/asyncLimiter.ts new file mode 100644 index 0000000..cb86f0c --- /dev/null +++ b/src/tools/asyncLimiter.ts @@ -0,0 +1,31 @@ + +export function createLimiter(concurrency: number) { + let activeCount = 0; + const queue: (() => void)[] = []; + + const next = () => { + activeCount--; + if (queue.length > 0) { + const fn = queue.shift(); + if (fn) fn(); + } + }; + + return async function limit(fn: () => Promise): Promise { + if (activeCount >= concurrency) { + await new Promise(resolve => queue.push(resolve)); + } + activeCount++; + try { + return await fn(); + } finally { + next(); + } + }; +} + +export function chunkArray(arr: T[], size: number): T[][] { + return Array.from({length: Math.ceil(arr.length / size)}, (_, i) => + arr.slice(i * size, i * size + size) + ); +}