Different api versions update

- Now plugin supports both v2 and v3 Jira API
  - Some preparation for solving bottleneck problem
This commit is contained in:
Alamion 2025-09-20 21:33:36 +03:00
parent ef9c7259fa
commit 5ae08dfcce
No known key found for this signature in database
GPG key ID: E5712F673595B31D
8 changed files with 135 additions and 18 deletions

View file

@ -1,7 +1,7 @@
{ {
"id": "jira-sync", "id": "jira-sync",
"name": "Jira Issue Manager", "name": "Jira Issue Manager",
"version": "1.3.0", "version": "1.3.1",
"minAppVersion": "1.7.7", "minAppVersion": "1.7.7",
"description": "Get Jira issues, create and update them. Issue status and worklog management.", "description": "Get Jira issues, create and update them. Issue status and worklog management.",
"author": "Alamion", "author": "Alamion",

View file

@ -1,6 +1,6 @@
{ {
"name": "obsidian-sample-plugin", "name": "obsidian-sample-plugin",
"version": "1.3.0", "version": "1.3.1",
"description": "This is a sample plugin for Obsidian (https://obsidian.md)", "description": "This is a sample plugin for Obsidian (https://obsidian.md)",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {

View file

@ -16,7 +16,7 @@ export async function baseRequest(
? "?" + new URLSearchParams(params).toString() ? "?" + 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 = { const requestParams = {
url, url,
method: method, method: method,
@ -58,3 +58,4 @@ export function sanitizeObject(obj: any): any {
return obj; return obj;
} }

View file

@ -2,6 +2,8 @@ import JiraPlugin from "../main";
import {JiraIssue, JiraTransitionType} from "../interfaces"; import {JiraIssue, JiraTransitionType} from "../interfaces";
import {baseRequest, sanitizeObject} from "./base"; import {baseRequest, sanitizeObject} from "./base";
import {Notice} from "obsidian"; import {Notice} from "obsidian";
import {chunkArray, createLimiter} from "../tools/asyncLimiter";
/** /**
* Fetch an issue from Jira by its key * Fetch an issue from Jira by its key
@ -10,8 +12,7 @@ import {Notice} from "obsidian";
* @returns The issue data * @returns The issue data
*/ */
export async function fetchIssue(plugin: JiraPlugin, issueKey: string): Promise<JiraIssue> { export async function fetchIssue(plugin: JiraPlugin, issueKey: string): Promise<JiraIssue> {
const additional_url_path = `/issue/${issueKey}` return await baseRequest(plugin, 'get', `/issue/${issueKey}`) as Promise<JiraIssue>;
return await baseRequest(plugin, 'get', additional_url_path) as Promise<JiraIssue>;
} }
/** /**
@ -43,6 +44,30 @@ export async function fetchIssuesByJQL(
return issues as JiraIssue[]; return issues as JiraIssue[];
} }
export async function fetchIssuesByJQLParallel(
plugin: JiraPlugin,
jql: string,
limit?: number,
fields?: string[]
): Promise<JiraIssue[]> {
const test = await fetchIssuesByJQLRaw(plugin, jql, 1, fields);
const totalAvailable = test.total;
const actualLimit = Math.min(limit || totalAvailable, totalAvailable);
const tasks: (() => Promise<JiraIssue[]>)[] = [];
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.). * Fetch issues by JQL and return the raw search response (includes total, startAt, etc.).
* Useful for previewing results and counts. * Useful for previewing results and counts.
@ -54,14 +79,13 @@ export async function fetchIssuesByJQLRaw(
fields?: string[], fields?: string[],
startAt?: number startAt?: number
): Promise<any> { ): Promise<any> {
const additional_url_path = `/search`;
const body = JSON.stringify(sanitizeObject({ const body = JSON.stringify(sanitizeObject({
jql, jql,
maxResults, maxResults,
startAt, startAt,
fields: fields && fields.length > 0 ? fields : undefined, 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 * @returns The issue data
*/ */
export async function fetchIssueTransitions(plugin: JiraPlugin, issueKey: string): Promise<JiraTransitionType[]> { export async function fetchIssueTransitions(plugin: JiraPlugin, issueKey: string): Promise<JiraTransitionType[]> {
const additional_url_path = `/issue/${issueKey}/transitions` const result = await baseRequest(plugin, 'get', `/issue/${issueKey}/transitions`);
const result = await baseRequest(plugin, 'get', additional_url_path);
return result.transitions.map(({ id, name, to }: { id: string; name: string; to: { name: string }}) => ({ return result.transitions.map(({ id, name, to }: { id: string; name: string; to: { name: string }}) => ({
id, id,
action: name, action: name,
@ -92,10 +115,22 @@ export async function updateJiraIssue(
fields: Record<string, any> fields: Record<string, any>
): Promise<JiraIssue> { ): Promise<JiraIssue> {
const body = JSON.stringify({ fields }) const body = JSON.stringify({ fields })
const additional_url_path = `/issue/${issueKey}` return await baseRequest(plugin, 'put', `/issue/${issueKey}`, body) as Promise<JiraIssue>;
return await baseRequest(plugin, 'put', additional_url_path, body) as Promise<JiraIssue>;
} }
export async function bulkUpdateJiraIssues(
plugin: JiraPlugin,
updates: { issueKey: string, fields: Record<string, any> }[]
): Promise<any[]> {
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 * Create a new issue in Jira
* @param plugin The plugin instance * @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"); throw new Error("Missing required fields: project, issuetype, and summary are required");
} }
const body = JSON.stringify({ fields }) const body = JSON.stringify({ fields })
const additional_url_path = `/issue/` return await baseRequest(plugin, 'post', `/issue/`, body) as Promise<JiraIssue>;
return await baseRequest(plugin, 'post', additional_url_path, body) as Promise<JiraIssue>;
} }
export async function bulkCreateJiraIssues(
plugin: JiraPlugin,
issues: Record<string, any>[]
): Promise<any[]> {
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 * Update an issue status in Jira
* @param plugin The plugin instance * @param plugin The plugin instance
@ -127,10 +177,10 @@ export async function updateJiraStatus(
status: string status: string
): Promise<JiraIssue> { ): Promise<JiraIssue> {
const body = JSON.stringify({ transition: { id: status } }) const body = JSON.stringify({ transition: { id: status } })
const additional_url_path = `/issue/${issueKey}/transitions` return await baseRequest(plugin, 'post', `/issue/${issueKey}/transitions`, body) as Promise<JiraIssue>;
return await baseRequest(plugin, 'post', additional_url_path, body) as Promise<JiraIssue>;
} }
/** /**
* Add a work log entry to a Jira issue * Add a work log entry to a Jira issue
*/ */
@ -147,9 +197,7 @@ export async function addWorkLog(
started: startedAt, started: startedAt,
comment comment
}; };
const response = await baseRequest(plugin, 'post', `/issue/${issueKey}/worklog`, JSON.stringify(payload));
const additional_url_path = `/issue/${issueKey}/worklog`;
const response = await baseRequest(plugin, 'post', additional_url_path, JSON.stringify(payload));
if (showNotice) { if (showNotice) {
new Notice(`Work log added successfully to ${issueKey}`); new Notice(`Work log added successfully to ${issueKey}`);
@ -157,3 +205,16 @@ export async function addWorkLog(
return response; return response;
} }
export async function bulkAddWorkLog(
plugin: JiraPlugin,
worklogs: { issueKey: string, timeSpent: string, startedAt: string, comment: string }[],
showNotice: boolean = true
): Promise<any[]> {
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);
}

View file

@ -5,6 +5,14 @@ url:
desc: Your Jira instance URL desc: Your Jira instance URL
def: https://yourcompany.atlassian.net 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: auth:
title: Authentication method title: Authentication method
desc: Choose how to authenticate with Jira desc: Choose how to authenticate with Jira

View file

@ -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 // Auth Method Select
new Setting(containerEl) new Setting(containerEl)
.setName(t("auth.title")) .setName(t("auth.title"))

View file

@ -9,6 +9,7 @@ export interface JiraSettings {
email: string; email: string;
password: string; password: string;
jiraUrl: string; jiraUrl: string;
apiVersion: string;
issuesFolder: string; issuesFolder: string;
sessionCookieName: string; sessionCookieName: string;
@ -40,6 +41,7 @@ export const DEFAULT_SETTINGS: JiraSettings = {
email: "", email: "",
password: "", password: "",
jiraUrl: "", jiraUrl: "",
apiVersion: "2",
issuesFolder: "jira-issues", issuesFolder: "jira-issues",
sessionCookieName: "JSESSIONID", sessionCookieName: "JSESSIONID",

31
src/tools/asyncLimiter.ts Normal file
View file

@ -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<T>(fn: () => Promise<T>): Promise<T> {
if (activeCount >= concurrency) {
await new Promise<void>(resolve => queue.push(resolve));
}
activeCount++;
try {
return await fn();
} finally {
next();
}
};
}
export function chunkArray<T>(arr: T[], size: number): T[][] {
return Array.from({length: Math.ceil(arr.length / size)}, (_, i) =>
arr.slice(i * size, i * size + size)
);
}