Compare commits
1 Commits
feature/re
...
fix/jira-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a179f19a1 |
@@ -276,14 +276,15 @@ function jiraDelete(urlPath, options) {
|
|||||||
* @param {string[]} [fields] - Jira field names to return
|
* @param {string[]} [fields] - Jira field names to return
|
||||||
*/
|
*/
|
||||||
async function getIssue(issueKey, fields) {
|
async function getIssue(issueKey, fields) {
|
||||||
const fieldList = (fields || DEFAULT_FIELDS).join(',');
|
const jql = `key = "${issueKey}" AND project = ${JIRA_PROJECT_KEY}`;
|
||||||
const res = await jiraGet(
|
const result = await searchIssues(jql, { fields: fields || DEFAULT_FIELDS, maxResults: 1, startAt: 0 });
|
||||||
`/rest/api/2/issue/${encodeURIComponent(issueKey)}?fields=${encodeURIComponent(fieldList)}`
|
if (result.ok && result.data.issues && result.data.issues.length > 0) {
|
||||||
);
|
return { ok: true, data: result.data.issues[0] };
|
||||||
if (res.status === 200) {
|
|
||||||
return { ok: true, data: JSON.parse(res.body) };
|
|
||||||
}
|
}
|
||||||
return { ok: false, status: res.status, body: res.body, rateLimited: res.rateLimited };
|
if (result.ok && (!result.data.issues || result.data.issues.length === 0)) {
|
||||||
|
return { ok: false, status: 404, body: 'Issue not found' };
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -303,7 +304,7 @@ async function searchIssuesByKeys(issueKeys, opts) {
|
|||||||
// or similar, but key-based search is inherently scoped. We add updated
|
// or similar, but key-based search is inherently scoped. We add updated
|
||||||
// clause for compliance.
|
// clause for compliance.
|
||||||
const keyList = issueKeys.map(k => `"${k}"`).join(', ');
|
const keyList = issueKeys.map(k => `"${k}"`).join(', ');
|
||||||
const jql = `key in (${keyList}) AND updated >= -24h`;
|
const jql = `key in (${keyList}) AND updated >= -24h AND project = ${JIRA_PROJECT_KEY}`;
|
||||||
const fields = (opts && opts.fields) || DEFAULT_FIELDS;
|
const fields = (opts && opts.fields) || DEFAULT_FIELDS;
|
||||||
const maxResults = Math.min((opts && opts.maxResults) || 1000, 1000);
|
const maxResults = Math.min((opts && opts.maxResults) || 1000, 1000);
|
||||||
|
|
||||||
@@ -327,8 +328,10 @@ async function searchIssues(jql, opts) {
|
|||||||
const maxResults = Math.min((opts && opts.maxResults) || 1000, 1000);
|
const maxResults = Math.min((opts && opts.maxResults) || 1000, 1000);
|
||||||
const fields = (opts && opts.fields) || DEFAULT_FIELDS;
|
const fields = (opts && opts.fields) || DEFAULT_FIELDS;
|
||||||
|
|
||||||
const body = { jql, startAt, maxResults, fields };
|
const fieldList = encodeURIComponent(fields.join(','));
|
||||||
const res = await jiraPost('/rest/api/2/search', body);
|
const encodedJql = encodeURIComponent(jql);
|
||||||
|
const queryString = `?jql=${encodedJql}&fields=${fieldList}&maxResults=${maxResults}&startAt=${startAt}`;
|
||||||
|
const res = await jiraGet('/rest/api/2/search' + queryString);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
return { ok: true, data: JSON.parse(res.body) };
|
return { ok: true, data: JSON.parse(res.body) };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ async function testJqlSearch() {
|
|||||||
assert(projectKey, 'JIRA_PROJECT_KEY must be set');
|
assert(projectKey, 'JIRA_PROJECT_KEY must be set');
|
||||||
|
|
||||||
// Use a broad time window to ensure results even on a quiet project
|
// Use a broad time window to ensure results even on a quiet project
|
||||||
const jql = `project = ${projectKey} ORDER BY updated DESC`;
|
const jql = `project = ${projectKey} AND updated >= -24h ORDER BY updated DESC`;
|
||||||
logInfo('Searching with JQL:', jql);
|
logInfo('Searching with JQL:', jql);
|
||||||
|
|
||||||
const result = await jiraApi.searchIssues(jql, { maxResults: 10 });
|
const result = await jiraApi.searchIssues(jql, { maxResults: 10 });
|
||||||
@@ -306,6 +306,8 @@ async function testBulkKeySearch() {
|
|||||||
const result = await jiraApi.searchIssuesByKeys(keys);
|
const result = await jiraApi.searchIssuesByKeys(keys);
|
||||||
assert(result.ok, 'Bulk key search should succeed. Got HTTP ' + (result.status || '') + ': ' + ((result.body || '').substring(0, 500)));
|
assert(result.ok, 'Bulk key search should succeed. Got HTTP ' + (result.status || '') + ': ' + ((result.body || '').substring(0, 500)));
|
||||||
|
|
||||||
|
logInfo('Bulk search uses project-scoped JQL with project = ' + jiraApi.JIRA_PROJECT_KEY);
|
||||||
|
|
||||||
const found = (result.data.issues || []).map(i => i.key);
|
const found = (result.data.issues || []).map(i => i.key);
|
||||||
logInfo('Found issues:', found);
|
logInfo('Found issues:', found);
|
||||||
assert(found.includes(createdIssueKey), 'Should find the created issue');
|
assert(found.includes(createdIssueKey), 'Should find the created issue');
|
||||||
@@ -350,7 +352,7 @@ async function main() {
|
|||||||
// Run tests in order — later tests depend on the created issue
|
// Run tests in order — later tests depend on the created issue
|
||||||
if (await runTest('1. Connection Test (GET /myself)', testConnection)) passed++; else failed++;
|
if (await runTest('1. Connection Test (GET /myself)', testConnection)) passed++; else failed++;
|
||||||
if (await runTest('2. Create Issue (POST /issue)', testCreateIssue)) passed++; else failed++;
|
if (await runTest('2. Create Issue (POST /issue)', testCreateIssue)) passed++; else failed++;
|
||||||
if (await runTest('3. Get Single Issue (GET /issue/{key})', testGetIssue)) passed++; else failed++;
|
if (await runTest('3. Get Single Issue (JQL search)', testGetIssue)) passed++; else failed++;
|
||||||
if (await runTest('4. Update Issue (PUT /issue/{key})', testUpdateIssue)) passed++; else failed++;
|
if (await runTest('4. Update Issue (PUT /issue/{key})', testUpdateIssue)) passed++; else failed++;
|
||||||
if (await runTest('5. Add Comment (POST /issue/{key}/comment)', testAddComment)) passed++; else failed++;
|
if (await runTest('5. Add Comment (POST /issue/{key}/comment)', testAddComment)) passed++; else failed++;
|
||||||
|
|
||||||
@@ -362,7 +364,7 @@ async function main() {
|
|||||||
await testTransitionIssue(transitions);
|
await testTransitionIssue(transitions);
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
if (await runTest('8. JQL Search (POST /search)', testJqlSearch)) passed++; else failed++;
|
if (await runTest('8. JQL Search (GET /search)', testJqlSearch)) passed++; else failed++;
|
||||||
if (await runTest('9. Bulk Key Search (searchIssuesByKeys)', testBulkKeySearch)) passed++; else failed++;
|
if (await runTest('9. Bulk Key Search (searchIssuesByKeys)', testBulkKeySearch)) passed++; else failed++;
|
||||||
if (await runTest('10. Rate Limit Status', testRateLimitStatus)) passed++; else failed++;
|
if (await runTest('10. Rate Limit Status', testRateLimitStatus)) passed++; else failed++;
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ All API calls are made from a single Node.js backend process. The integration us
|
|||||||
| Inter-request delay — writes | 2 seconds minimum between PUT/POST/DELETE requests |
|
| Inter-request delay — writes | 2 seconds minimum between PUT/POST/DELETE requests |
|
||||||
| Explicit field lists | Every GET includes `?fields=` parameter; `/rest/api/2/field` is blocked |
|
| Explicit field lists | Every GET includes `?fields=` parameter; `/rest/api/2/field` is blocked |
|
||||||
| No bulk updates | Issues are updated one at a time; `/rest/api/2/issue/bulk` is blocked |
|
| No bulk updates | Issues are updated one at a time; `/rest/api/2/issue/bulk` is blocked |
|
||||||
| Bulk reads via JQL | Multi-ticket sync uses a single `POST /rest/api/2/search` with JQL, not per-issue GETs |
|
| Bulk reads via JQL | Multi-ticket sync uses a single `GET /rest/api/2/search` with JQL query parameters, not per-issue GETs |
|
||||||
| JQL scoping | All recurring JQL queries include `updated >= -Xh` clause |
|
| Single-issue fetch via JQL | `GET /rest/api/2/search?jql=key="KEY" AND project=<KEY>&fields=...&maxResults=1` |
|
||||||
|
| JQL scoping | All recurring JQL queries include `updated >= -Xh` clause and `project = <KEY>` scoping |
|
||||||
| `maxResults` cap | Search queries capped at 1 000 results per page |
|
| `maxResults` cap | Search queries capped at 1 000 results per page |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -52,11 +53,11 @@ All API calls are made from a single Node.js backend process. The integration us
|
|||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **Endpoint** | `GET /rest/api/2/issue/{issueKey}?fields=summary,status,assignee,created,updated,priority,issuetype,project,resolution` |
|
| **Endpoint** | `GET /rest/api/2/search?jql=key="ISSUE-KEY" AND project=<KEY>&fields=summary,status,assignee,created,updated,priority,issuetype,project,resolution&maxResults=1` |
|
||||||
| **Trigger** | User clicks "Sync" on a single Jira ticket row |
|
| **Trigger** | User clicks "Sync" on a single Jira ticket row |
|
||||||
| **Frequency** | Manual, estimated 10–30 per day |
|
| **Frequency** | Manual, estimated 10–30 per day |
|
||||||
| **Purpose** | Refresh a single ticket's status and summary from Jira |
|
| **Purpose** | Refresh a single ticket's status and summary from Jira via JQL search |
|
||||||
| **Notes** | Fields are always specified explicitly per Charter requirement |
|
| **Notes** | Uses JQL-based lookup instead of single-issue GET per Charter compliance. Fields are always specified explicitly. |
|
||||||
|
|
||||||
### 4. Update Issue
|
### 4. Update Issue
|
||||||
|
|
||||||
@@ -99,23 +100,23 @@ All API calls are made from a single Node.js backend process. The integration us
|
|||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **Endpoint** | `POST /rest/api/2/search` |
|
| **Endpoint** | `GET /rest/api/2/search?jql=...&fields=...&maxResults=...&startAt=...` |
|
||||||
| **Trigger** | Admin clicks "Sync All" on the Jira tickets panel |
|
| **Trigger** | Admin clicks "Sync All" on the Jira tickets panel |
|
||||||
| **Frequency** | Manual, estimated 1–3 times per day |
|
| **Frequency** | Manual, estimated 1–3 times per day |
|
||||||
| **Purpose** | Bulk-refresh all tracked tickets in a single request instead of per-issue GETs |
|
| **Purpose** | Bulk-refresh all tracked tickets in a single request instead of per-issue GETs |
|
||||||
| **JQL pattern** | `key in ("KEY-1", "KEY-2", ...) AND updated >= -24h` |
|
| **JQL pattern** | `key in ("KEY-1", "KEY-2", ...) AND updated >= -24h AND project = <KEY>` |
|
||||||
| **Fields requested** | `summary, status, assignee, created, updated, priority, issuetype, project, resolution` |
|
| **Fields requested** | `summary, status, assignee, created, updated, priority, issuetype, project, resolution` |
|
||||||
| **Batch size** | 100 keys per JQL query; multiple batches if needed |
|
| **Batch size** | 100 keys per JQL query; multiple batches if needed |
|
||||||
| **Notes** | Stops early if rate limit budget is running low (burst remaining <= 5 or daily remaining <= 10) |
|
| **Notes** | Uses GET with URL-encoded query parameters per Charter compliance. Stops early if rate limit budget is running low (burst remaining <= 5 or daily remaining <= 10) |
|
||||||
|
|
||||||
### 9. Issue Lookup
|
### 9. Issue Lookup
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **Endpoint** | `GET /rest/api/2/issue/{issueKey}?fields=...` |
|
| **Endpoint** | `GET /rest/api/2/search?jql=key="ISSUE-KEY" AND project=<KEY>&fields=...&maxResults=1` |
|
||||||
| **Trigger** | User looks up a Jira issue by key from the dashboard search |
|
| **Trigger** | User looks up a Jira issue by key from the dashboard search |
|
||||||
| **Frequency** | Manual, estimated 5–15 per day |
|
| **Frequency** | Manual, estimated 5–15 per day |
|
||||||
| **Purpose** | Quick lookup of any Jira issue to view its current state |
|
| **Purpose** | Quick lookup of any Jira issue to view its current state via JQL search |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -130,7 +131,7 @@ All API calls are made from a single Node.js backend process. The integration us
|
|||||||
| Add comment | 5–15 | POST | 2s |
|
| Add comment | 5–15 | POST | 2s |
|
||||||
| Get transitions | 5–10 | GET | 1s |
|
| Get transitions | 5–10 | GET | 1s |
|
||||||
| Transition issue | 5–10 | POST | 2s |
|
| Transition issue | 5–10 | POST | 2s |
|
||||||
| JQL search (sync) | 1–5 | POST | 2s |
|
| JQL search (sync) | 1–5 | GET | 1s |
|
||||||
| Issue lookup | 5–15 | GET | 1s |
|
| Issue lookup | 5–15 | GET | 1s |
|
||||||
| **Total estimated** | **43–120** | | |
|
| **Total estimated** | **43–120** | | |
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user