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",
|
"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",
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"))
|
||||||
|
|
|
||||||
|
|
@ -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
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