mirror of
https://github.com/alamion/obsidian-jira-sync.git
synced 2026-07-22 05:43:04 +00:00
Different api versions update
- Now plugin supports both v2 and v3 Jira API - Some preparation for solving bottleneck problem
This commit is contained in:
parent
ef9c7259fa
commit
5ae08dfcce
8 changed files with 135 additions and 18 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<JiraIssue> {
|
||||
const additional_url_path = `/issue/${issueKey}`
|
||||
return await baseRequest(plugin, 'get', additional_url_path) as Promise<JiraIssue>;
|
||||
return await baseRequest(plugin, 'get', `/issue/${issueKey}`) as Promise<JiraIssue>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -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<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.).
|
||||
* Useful for previewing results and counts.
|
||||
|
|
@ -54,14 +79,13 @@ export async function fetchIssuesByJQLRaw(
|
|||
fields?: string[],
|
||||
startAt?: number
|
||||
): Promise<any> {
|
||||
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<JiraTransitionType[]> {
|
||||
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<string, any>
|
||||
): Promise<JiraIssue> {
|
||||
const body = JSON.stringify({ fields })
|
||||
const additional_url_path = `/issue/${issueKey}`
|
||||
return await baseRequest(plugin, 'put', additional_url_path, body) as Promise<JiraIssue>;
|
||||
return await baseRequest(plugin, 'put', `/issue/${issueKey}`, 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
|
||||
* @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<JiraIssue>;
|
||||
return await baseRequest(plugin, 'post', `/issue/`, 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
|
||||
* @param plugin The plugin instance
|
||||
|
|
@ -127,10 +177,10 @@ export async function updateJiraStatus(
|
|||
status: string
|
||||
): Promise<JiraIssue> {
|
||||
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<JiraIssue>;
|
||||
return await baseRequest(plugin, 'post', `/issue/${issueKey}/transitions`, body) as Promise<JiraIssue>;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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<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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
31
src/tools/asyncLimiter.ts
Normal file
31
src/tools/asyncLimiter.ts
Normal 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)
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue