diff --git a/backend/helpers/jiraApi.js b/backend/helpers/jiraApi.js index 9ec878f..ce5e89c 100644 --- a/backend/helpers/jiraApi.js +++ b/backend/helpers/jiraApi.js @@ -276,14 +276,15 @@ function jiraDelete(urlPath, options) { * @param {string[]} [fields] - Jira field names to return */ async function getIssue(issueKey, fields) { - const fieldList = (fields || DEFAULT_FIELDS).join(','); - const res = await jiraGet( - `/rest/api/2/issue/${encodeURIComponent(issueKey)}?fields=${encodeURIComponent(fieldList)}` - ); - if (res.status === 200) { - return { ok: true, data: JSON.parse(res.body) }; + const jql = `key = "${issueKey}" AND project = ${JIRA_PROJECT_KEY}`; + const result = await searchIssues(jql, { fields: fields || DEFAULT_FIELDS, maxResults: 1, startAt: 0 }); + if (result.ok && result.data.issues && result.data.issues.length > 0) { + return { ok: true, data: result.data.issues[0] }; } - 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 // clause for compliance. 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 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 fields = (opts && opts.fields) || DEFAULT_FIELDS; - const body = { jql, startAt, maxResults, fields }; - const res = await jiraPost('/rest/api/2/search', body); + const fieldList = encodeURIComponent(fields.join(',')); + 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) { return { ok: true, data: JSON.parse(res.body) }; } diff --git a/backend/scripts/jira-uat-test.js b/backend/scripts/jira-uat-test.js index e9424af..62f92bb 100644 --- a/backend/scripts/jira-uat-test.js +++ b/backend/scripts/jira-uat-test.js @@ -273,7 +273,7 @@ async function testJqlSearch() { assert(projectKey, 'JIRA_PROJECT_KEY must be set'); // 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); const result = await jiraApi.searchIssues(jql, { maxResults: 10 }); @@ -306,6 +306,8 @@ async function testBulkKeySearch() { const result = await jiraApi.searchIssuesByKeys(keys); 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); logInfo('Found issues:', found); 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 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('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('5. Add Comment (POST /issue/{key}/comment)', testAddComment)) passed++; else failed++; @@ -362,7 +364,7 @@ async function main() { await testTransitionIssue(transitions); })) 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('10. Rate Limit Status', testRateLimitStatus)) passed++; else failed++; diff --git a/docs/jira-api-use-cases.md b/docs/jira-api-use-cases.md index 9be6d57..ce5dab8 100644 --- a/docs/jira-api-use-cases.md +++ b/docs/jira-api-use-cases.md @@ -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 | | 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 | -| Bulk reads via JQL | Multi-ticket sync uses a single `POST /rest/api/2/search` with JQL, not per-issue GETs | -| JQL scoping | All recurring JQL queries include `updated >= -Xh` clause | +| Bulk reads via JQL | Multi-ticket sync uses a single `GET /rest/api/2/search` with JQL query parameters, not per-issue GETs | +| Single-issue fetch via JQL | `GET /rest/api/2/search?jql=key="KEY" AND project=&fields=...&maxResults=1` | +| JQL scoping | All recurring JQL queries include `updated >= -Xh` clause and `project = ` scoping | | `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=&fields=summary,status,assignee,created,updated,priority,issuetype,project,resolution&maxResults=1` | | **Trigger** | User clicks "Sync" on a single Jira ticket row | | **Frequency** | Manual, estimated 10–30 per day | -| **Purpose** | Refresh a single ticket's status and summary from Jira | -| **Notes** | Fields are always specified explicitly per Charter requirement | +| **Purpose** | Refresh a single ticket's status and summary from Jira via JQL search | +| **Notes** | Uses JQL-based lookup instead of single-issue GET per Charter compliance. Fields are always specified explicitly. | ### 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 | | **Frequency** | Manual, estimated 1–3 times per day | | **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 = ` | | **Fields requested** | `summary, status, assignee, created, updated, priority, issuetype, project, resolution` | | **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 | | | |---|---| -| **Endpoint** | `GET /rest/api/2/issue/{issueKey}?fields=...` | +| **Endpoint** | `GET /rest/api/2/search?jql=key="ISSUE-KEY" AND project=&fields=...&maxResults=1` | | **Trigger** | User looks up a Jira issue by key from the dashboard search | | **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 | | Get transitions | 5–10 | GET | 1s | | 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 | | **Total estimated** | **43–120** | | |