Switch Jira API calls to GET-based JQL search with project scoping

- getIssue now uses GET /rest/api/2/search with JQL instead of
  GET /rest/api/2/issue/{key} for Charter compliance
- searchIssues switched from POST to GET with URL-encoded query params
- searchIssuesByKeys adds project scoping to JQL clause
- Updated UAT tests and API use-case docs to match
This commit is contained in:
root
2026-04-29 14:12:04 +00:00
parent 4f960d0866
commit 7a179f19a1
3 changed files with 30 additions and 24 deletions

View File

@@ -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) };
} }

View File

@@ -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++;

View File

@@ -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 1030 per day | | **Frequency** | Manual, estimated 1030 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 13 times per day | | **Frequency** | Manual, estimated 13 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 515 per day | | **Frequency** | Manual, estimated 515 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 | 515 | POST | 2s | | Add comment | 515 | POST | 2s |
| Get transitions | 510 | GET | 1s | | Get transitions | 510 | GET | 1s |
| Transition issue | 510 | POST | 2s | | Transition issue | 510 | POST | 2s |
| JQL search (sync) | 15 | POST | 2s | | JQL search (sync) | 15 | GET | 1s |
| Issue lookup | 515 | GET | 1s | | Issue lookup | 515 | GET | 1s |
| **Total estimated** | **43120** | | | | **Total estimated** | **43120** | | |