@gitkraken/core-gitlens 0.5.114 → 0.5.116
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -1
- package/dist/plus/git-github/api/github.d.ts +3 -2
- package/dist/plus/git-github/api/github.d.ts.map +1 -1
- package/dist/plus/git-github/api/github.js +37 -37
- package/dist/plus/git-github/api/github.js.map +1 -1
- package/dist/plus/integrations/collectionMetadata.d.ts +2 -0
- package/dist/plus/integrations/collectionMetadata.d.ts.map +1 -1
- package/dist/plus/integrations/collectionMetadata.js +17 -1
- package/dist/plus/integrations/collectionMetadata.js.map +1 -1
- package/dist/plus/integrations/context.d.ts +4 -1
- package/dist/plus/integrations/context.d.ts.map +1 -1
- package/dist/plus/integrations/index.d.ts +1 -0
- package/dist/plus/integrations/index.d.ts.map +1 -1
- package/dist/plus/integrations/integrationService.d.ts +9 -1
- package/dist/plus/integrations/integrationService.d.ts.map +1 -1
- package/dist/plus/integrations/integrationService.js +8 -4
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/manager.d.ts +65 -6
- package/dist/plus/integrations/manager.d.ts.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.d.ts +22 -26
- package/dist/plus/integrations/models/gitHostIntegration.d.ts.map +1 -1
- package/dist/plus/integrations/models/gitHostIntegration.js +92 -47
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
- package/dist/plus/integrations/models/integration.d.ts +9 -0
- package/dist/plus/integrations/models/integration.d.ts.map +1 -1
- package/dist/plus/integrations/models/integration.js +52 -19
- package/dist/plus/integrations/models/integration.js.map +1 -1
- package/dist/plus/integrations/models/issueCache.d.ts +16 -0
- package/dist/plus/integrations/models/issueCache.d.ts.map +1 -0
- package/dist/plus/integrations/models/issueCache.js +9 -0
- package/dist/plus/integrations/models/issueCache.js.map +1 -0
- package/dist/plus/integrations/models/issueReads.d.ts +13 -0
- package/dist/plus/integrations/models/issueReads.d.ts.map +1 -1
- package/dist/plus/integrations/models/issuesIntegration.d.ts +11 -13
- package/dist/plus/integrations/models/issuesIntegration.d.ts.map +1 -1
- package/dist/plus/integrations/models/issuesIntegration.js +15 -1
- package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOps.d.ts +3 -5
- package/dist/plus/integrations/providers/azureDevOps.d.ts.map +1 -1
- package/dist/plus/integrations/providers/azureDevOps.js +9 -8
- package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket-server.d.ts +3 -5
- package/dist/plus/integrations/providers/bitbucket-server.d.ts.map +1 -1
- package/dist/plus/integrations/providers/bitbucket-server.js +2 -2
- package/dist/plus/integrations/providers/bitbucket-server.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket.d.ts +3 -5
- package/dist/plus/integrations/providers/bitbucket.d.ts.map +1 -1
- package/dist/plus/integrations/providers/bitbucket.js +2 -1
- package/dist/plus/integrations/providers/bitbucket.js.map +1 -1
- package/dist/plus/integrations/providers/github.d.ts +3 -5
- package/dist/plus/integrations/providers/github.d.ts.map +1 -1
- package/dist/plus/integrations/providers/github.js +3 -3
- package/dist/plus/integrations/providers/github.js.map +1 -1
- package/dist/plus/integrations/providers/gitlab.d.ts +3 -5
- package/dist/plus/integrations/providers/gitlab.d.ts.map +1 -1
- package/dist/plus/integrations/providers/gitlab.js +2 -2
- package/dist/plus/integrations/providers/gitlab.js.map +1 -1
- package/dist/plus/integrations/providers/jira.d.ts +3 -7
- package/dist/plus/integrations/providers/jira.d.ts.map +1 -1
- package/dist/plus/integrations/providers/jira.js +35 -22
- package/dist/plus/integrations/providers/jira.js.map +1 -1
- package/dist/plus/integrations/providers/jiraIssueByKey.d.ts +7 -0
- package/dist/plus/integrations/providers/jiraIssueByKey.d.ts.map +1 -0
- package/dist/plus/integrations/providers/jiraIssueByKey.js +102 -0
- package/dist/plus/integrations/providers/jiraIssueByKey.js.map +1 -0
- package/dist/plus/integrations/providers/linear.d.ts +3 -7
- package/dist/plus/integrations/providers/linear.d.ts.map +1 -1
- package/dist/plus/integrations/providers/linear.js +23 -22
- package/dist/plus/integrations/providers/linear.js.map +1 -1
- package/dist/plus/integrations/providers/models.d.ts +3 -1
- package/dist/plus/integrations/providers/models.d.ts.map +1 -1
- package/dist/plus/integrations/providers/models.js +10 -4
- package/dist/plus/integrations/providers/models.js.map +1 -1
- package/dist/plus/integrations/providers/providerErrors.d.ts +7 -0
- package/dist/plus/integrations/providers/providerErrors.d.ts.map +1 -0
- package/dist/plus/integrations/providers/providerErrors.js +91 -0
- package/dist/plus/integrations/providers/providerErrors.js.map +1 -0
- package/dist/plus/integrations/providers/providersApi.d.ts +3 -2
- package/dist/plus/integrations/providers/providersApi.d.ts.map +1 -1
- package/dist/plus/integrations/providers/providersApi.js +18 -52
- package/dist/plus/integrations/providers/providersApi.js.map +1 -1
- package/dist/plus/integrations/providers/trello.d.ts +2 -7
- package/dist/plus/integrations/providers/trello.d.ts.map +1 -1
- package/dist/plus/integrations/providers/trello.js +3 -1
- package/dist/plus/integrations/providers/trello.js.map +1 -1
- package/dist/plus/integrations/providers/utils/providerPaging.d.ts +2 -0
- package/dist/plus/integrations/providers/utils/providerPaging.d.ts.map +1 -1
- package/dist/plus/integrations/providers/utils/providerPaging.js +12 -7
- package/dist/plus/integrations/providers/utils/providerPaging.js.map +1 -1
- package/dist/plus/integrations/reads/broaden.d.ts.map +1 -1
- package/dist/plus/integrations/reads/broaden.js +26 -14
- package/dist/plus/integrations/reads/broaden.js.map +1 -1
- package/dist/plus/integrations/reads/counts.d.ts.map +1 -1
- package/dist/plus/integrations/reads/counts.js +39 -15
- package/dist/plus/integrations/reads/counts.js.map +1 -1
- package/dist/plus/integrations/reads/filters.d.ts +33 -11
- package/dist/plus/integrations/reads/filters.d.ts.map +1 -1
- package/dist/plus/integrations/reads/filters.js +114 -25
- package/dist/plus/integrations/reads/filters.js.map +1 -1
- package/dist/plus/integrations/reads/issueTracker.d.ts.map +1 -1
- package/dist/plus/integrations/reads/issueTracker.js +23 -17
- package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
- package/dist/plus/integrations/reads/searchIssues.d.ts +1 -5
- package/dist/plus/integrations/reads/searchIssues.d.ts.map +1 -1
- package/dist/plus/integrations/reads/searchIssues.js +12 -6
- package/dist/plus/integrations/reads/searchIssues.js.map +1 -1
- package/dist/plus/integrations/reads/searchPullRequests.d.ts +1 -0
- package/dist/plus/integrations/reads/searchPullRequests.d.ts.map +1 -1
- package/dist/plus/integrations/reads/searchPullRequests.js +8 -3
- package/dist/plus/integrations/reads/searchPullRequests.js.map +1 -1
- package/dist/plus/integrations/reads/trackerIssue.d.ts +21 -0
- package/dist/plus/integrations/reads/trackerIssue.d.ts.map +1 -0
- package/dist/plus/integrations/reads/trackerIssue.js +51 -0
- package/dist/plus/integrations/reads/trackerIssue.js.map +1 -0
- package/dist/plus/integrations/reads/warnings.d.ts +18 -15
- package/dist/plus/integrations/reads/warnings.d.ts.map +1 -1
- package/dist/plus/integrations/reads/warnings.js +37 -1
- package/dist/plus/integrations/reads/warnings.js.map +1 -1
- package/dist/plus/integrations/results.d.ts +4 -20
- package/dist/plus/integrations/results.d.ts.map +1 -1
- package/dist/plus/integrations/results.js.map +1 -1
- package/dist/utils/promise.d.ts +2 -0
- package/dist/utils/promise.d.ts.map +1 -1
- package/dist/utils/promise.js +11 -0
- package/dist/utils/promise.js.map +1 -1
- package/docs/integrations.md +63 -28
- package/docs/kepler-read-api-parity.md +13 -0
- package/package.json +2 -2
- package/src/plus/git-github/api/github.ts +54 -60
- package/src/plus/integrations/collectionMetadata.ts +23 -1
- package/src/plus/integrations/context.ts +1 -1
- package/src/plus/integrations/index.ts +1 -0
- package/src/plus/integrations/integrationService.ts +14 -4
- package/src/plus/integrations/manager.ts +65 -6
- package/src/plus/integrations/models/gitHostIntegration.ts +117 -129
- package/src/plus/integrations/models/integration.ts +56 -22
- package/src/plus/integrations/models/issueCache.ts +29 -0
- package/src/plus/integrations/models/issueReads.ts +15 -0
- package/src/plus/integrations/models/issuesIntegration.ts +38 -8
- package/src/plus/integrations/providers/azureDevOps.ts +16 -13
- package/src/plus/integrations/providers/bitbucket-server.ts +4 -6
- package/src/plus/integrations/providers/bitbucket.ts +3 -4
- package/src/plus/integrations/providers/github.ts +5 -7
- package/src/plus/integrations/providers/gitlab.ts +4 -6
- package/src/plus/integrations/providers/jira.ts +52 -26
- package/src/plus/integrations/providers/jiraIssueByKey.ts +155 -0
- package/src/plus/integrations/providers/linear.ts +38 -25
- package/src/plus/integrations/providers/models.ts +17 -5
- package/src/plus/integrations/providers/providerErrors.ts +115 -0
- package/src/plus/integrations/providers/providersApi.ts +26 -68
- package/src/plus/integrations/providers/trello.ts +5 -4
- package/src/plus/integrations/providers/utils/providerPaging.ts +14 -7
- package/src/plus/integrations/reads/broaden.ts +35 -16
- package/src/plus/integrations/reads/counts.ts +62 -15
- package/src/plus/integrations/reads/filters.ts +131 -29
- package/src/plus/integrations/reads/issueTracker.ts +26 -24
- package/src/plus/integrations/reads/searchIssues.ts +23 -13
- package/src/plus/integrations/reads/searchPullRequests.ts +16 -2
- package/src/plus/integrations/reads/trackerIssue.ts +105 -0
- package/src/plus/integrations/reads/warnings.ts +42 -17
- package/src/plus/integrations/results.ts +4 -20
- package/src/utils/promise.ts +15 -0
|
@@ -10,7 +10,7 @@ import type { ProviderAuthenticationSession } from '../authentication/models.js'
|
|
|
10
10
|
import { toTokenWithInfo } from '../authentication/models.js';
|
|
11
11
|
import { throwIfCallerContractError, toCollectionScopeFailure } from '../collectionMetadata.js';
|
|
12
12
|
import { IssuesCloudHostIntegrationId } from '../constants.js';
|
|
13
|
-
import type { IssuesForProjectOptions } from '../models/issueReads.js';
|
|
13
|
+
import type { IssuesForProjectOptions, ProjectIssuesDrain } from '../models/issueReads.js';
|
|
14
14
|
import { IssuesIntegration } from '../models/issuesIntegration.js';
|
|
15
15
|
import type { ProviderApiCollectionResult, ProviderIssue } from './models.js';
|
|
16
16
|
import { IssueFilter, providersMetadata, toAccount, toIssueShape } from './models.js';
|
|
@@ -20,6 +20,12 @@ const metadata = providersMetadata[IssuesCloudHostIntegrationId.Jira];
|
|
|
20
20
|
const authProvider = Object.freeze({ id: metadata.id, scopes: metadata.scopes });
|
|
21
21
|
const maxPagesPerRequest = 10;
|
|
22
22
|
|
|
23
|
+
type JiraProjectIssuesDrain<T> = {
|
|
24
|
+
issues: T[];
|
|
25
|
+
status: 'complete' | 'backstop' | 'incomplete';
|
|
26
|
+
metadata?: CollectionMetadata;
|
|
27
|
+
};
|
|
28
|
+
|
|
23
29
|
export type JiraBaseDescriptor = IssueResourceDescriptor;
|
|
24
30
|
|
|
25
31
|
export interface JiraOrganizationDescriptor extends JiraBaseDescriptor {
|
|
@@ -244,16 +250,11 @@ export class JiraIntegration extends IssuesIntegration<IssuesCloudHostIntegratio
|
|
|
244
250
|
session: ProviderAuthenticationSession,
|
|
245
251
|
project: JiraProjectDescriptor,
|
|
246
252
|
options?: IssuesForProjectOptions,
|
|
247
|
-
): Promise<
|
|
253
|
+
): Promise<ProjectIssuesDrain | undefined> {
|
|
248
254
|
const tokenWithInfo = toTokenWithInfo(this.id, session);
|
|
249
255
|
|
|
250
256
|
const api = await this.getProvidersApi();
|
|
251
257
|
|
|
252
|
-
// Drain every page for a project read (bounded by a defensive backstop): the paged wrapper preserves the
|
|
253
|
-
// SDK's cursor, unlike the plain read which silently caps at the first page. `filter` undefined = the
|
|
254
|
-
// unscoped project read. Reports `truncated` when the drain stopped at the backstop with more pages
|
|
255
|
-
// still available, and records a structured failure if a page-level error discarded the rest of the drain,
|
|
256
|
-
// so the facade can warn + set fetchFailed while preserving the already-fetched prefix.
|
|
257
258
|
const projectScope = {
|
|
258
259
|
providerId: this.id,
|
|
259
260
|
resourceId: project.resourceId,
|
|
@@ -263,10 +264,10 @@ export class JiraIntegration extends IssuesIntegration<IssuesCloudHostIntegratio
|
|
|
263
264
|
authorLogin?: string;
|
|
264
265
|
assigneeLogins?: string[];
|
|
265
266
|
mentionLogin?: string;
|
|
266
|
-
}): Promise<
|
|
267
|
+
}): Promise<JiraProjectIssuesDrain<ProviderIssue>> => {
|
|
267
268
|
const collected: ProviderIssue[] = [];
|
|
268
269
|
let cursor: string | undefined;
|
|
269
|
-
let
|
|
270
|
+
let status: JiraProjectIssuesDrain<ProviderIssue>['status'] = 'complete';
|
|
270
271
|
let metadata: CollectionMetadata | undefined;
|
|
271
272
|
for (let i = 0; i < maxPagesPerRequest; i++) {
|
|
272
273
|
let result: Awaited<ReturnType<typeof api.getIssuesForProjectPaged>> | undefined;
|
|
@@ -283,7 +284,7 @@ export class JiraIntegration extends IssuesIntegration<IssuesCloudHostIntegratio
|
|
|
283
284
|
// than an empty partial success.
|
|
284
285
|
if (collected.length === 0) throw ex;
|
|
285
286
|
|
|
286
|
-
|
|
287
|
+
status = 'incomplete';
|
|
287
288
|
metadata = mergeCollectionMetadata(metadata, {
|
|
288
289
|
completeness: 'partial',
|
|
289
290
|
failures: [toCollectionScopeFailure(projectScope, ex)],
|
|
@@ -293,7 +294,7 @@ export class JiraIntegration extends IssuesIntegration<IssuesCloudHostIntegratio
|
|
|
293
294
|
if (result == null) {
|
|
294
295
|
if (cursor == null) break;
|
|
295
296
|
|
|
296
|
-
|
|
297
|
+
status = 'incomplete';
|
|
297
298
|
metadata = mergeCollectionMetadata(metadata, {
|
|
298
299
|
completeness: 'partial',
|
|
299
300
|
failures: [
|
|
@@ -312,7 +313,7 @@ export class JiraIntegration extends IssuesIntegration<IssuesCloudHostIntegratio
|
|
|
312
313
|
// The provider claims more pages but gave no advancing cursor: we can't continue, so the drain
|
|
313
314
|
// is incomplete — flag it rather than silently stopping (matches drainPullRequests/Repositories).
|
|
314
315
|
if (result.nextCursor == null || result.nextCursor === cursor) {
|
|
315
|
-
|
|
316
|
+
status = 'incomplete';
|
|
316
317
|
metadata = mergeCollectionMetadata(metadata, {
|
|
317
318
|
completeness: 'partial',
|
|
318
319
|
failures: [
|
|
@@ -326,18 +327,21 @@ export class JiraIntegration extends IssuesIntegration<IssuesCloudHostIntegratio
|
|
|
326
327
|
}
|
|
327
328
|
|
|
328
329
|
cursor = result.nextCursor;
|
|
329
|
-
// More pages remain but we're at the last allowed iteration: the drain is incomplete.
|
|
330
330
|
if (i === maxPagesPerRequest - 1) {
|
|
331
|
-
|
|
331
|
+
status = 'backstop';
|
|
332
332
|
}
|
|
333
333
|
}
|
|
334
|
-
return {
|
|
334
|
+
return {
|
|
335
|
+
issues: collected,
|
|
336
|
+
status: status,
|
|
337
|
+
metadata: metadata,
|
|
338
|
+
};
|
|
335
339
|
};
|
|
336
340
|
|
|
337
341
|
const getSearchedUserIssuesForFilter = async (
|
|
338
342
|
user: string,
|
|
339
343
|
filter: IssueFilter,
|
|
340
|
-
): Promise<
|
|
344
|
+
): Promise<JiraProjectIssuesDrain<IssueShape>> => {
|
|
341
345
|
const result = await drainIssues({
|
|
342
346
|
authorLogin: filter === IssueFilter.Author ? user : undefined,
|
|
343
347
|
assigneeLogins: filter === IssueFilter.Assignee ? [user] : undefined,
|
|
@@ -348,7 +352,7 @@ export class JiraIntegration extends IssuesIntegration<IssuesCloudHostIntegratio
|
|
|
348
352
|
issues: result.issues
|
|
349
353
|
.map(issue => toIssueShape(issue, this))
|
|
350
354
|
.filter((r): r is IssueShape => r !== undefined),
|
|
351
|
-
|
|
355
|
+
status: result.status,
|
|
352
356
|
metadata: result.metadata,
|
|
353
357
|
};
|
|
354
358
|
};
|
|
@@ -415,7 +419,7 @@ export class JiraIntegration extends IssuesIntegration<IssuesCloudHostIntegratio
|
|
|
415
419
|
continue;
|
|
416
420
|
}
|
|
417
421
|
|
|
418
|
-
if (outcome.value.
|
|
422
|
+
if (outcome.value.status !== 'complete') {
|
|
419
423
|
truncated = true;
|
|
420
424
|
}
|
|
421
425
|
if (outcome.value.metadata != null) {
|
|
@@ -428,17 +432,23 @@ export class JiraIntegration extends IssuesIntegration<IssuesCloudHostIntegratio
|
|
|
428
432
|
}
|
|
429
433
|
}
|
|
430
434
|
|
|
431
|
-
return
|
|
435
|
+
return truncated
|
|
436
|
+
? { values: [...resultsById.values()], truncated: true, recovery: 'none', metadata: metadata }
|
|
437
|
+
: { values: [...resultsById.values()], truncated: false, metadata: metadata };
|
|
432
438
|
}
|
|
433
439
|
|
|
434
440
|
const unscoped = await drainIssues({});
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
441
|
+
const values = unscoped.issues
|
|
442
|
+
.map(issue => toIssueShape(issue, this))
|
|
443
|
+
.filter((result): result is IssueShape => result !== undefined);
|
|
444
|
+
return unscoped.status !== 'complete'
|
|
445
|
+
? {
|
|
446
|
+
values: values,
|
|
447
|
+
truncated: true,
|
|
448
|
+
recovery: unscoped.status === 'backstop' ? 'narrow-scope' : 'none',
|
|
449
|
+
metadata: unscoped.metadata,
|
|
450
|
+
}
|
|
451
|
+
: { values: values, truncated: false, metadata: unscoped.metadata };
|
|
442
452
|
}
|
|
443
453
|
|
|
444
454
|
protected override async searchProviderMyIssues(
|
|
@@ -516,6 +526,22 @@ export class JiraIntegration extends IssuesIntegration<IssuesCloudHostIntegratio
|
|
|
516
526
|
return issue != null ? { ...issue, type: 'issue' } : undefined;
|
|
517
527
|
}
|
|
518
528
|
|
|
529
|
+
protected override async getProviderIssueByResourceId(
|
|
530
|
+
session: ProviderAuthenticationSession,
|
|
531
|
+
resourceId: string,
|
|
532
|
+
id: string,
|
|
533
|
+
resourceUrl: string | undefined,
|
|
534
|
+
): Promise<Issue | undefined> {
|
|
535
|
+
if (resourceUrl == null) {
|
|
536
|
+
throw new Error('Jira direct issue reads require a resource URL');
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const api = await this.getProvidersApi();
|
|
540
|
+
const apiResult = await api.getJiraIssueByKey(toTokenWithInfo(this.id, session), resourceId, resourceUrl, id);
|
|
541
|
+
const issue = apiResult != null ? toIssueShape(apiResult, this, { reliableStateCategory: true }) : undefined;
|
|
542
|
+
return issue != null ? { ...issue, type: 'issue' } : undefined;
|
|
543
|
+
}
|
|
544
|
+
|
|
519
545
|
protected override async providerOnConnect(): Promise<void> {
|
|
520
546
|
this._autolinks = undefined;
|
|
521
547
|
if (this._session == null) return;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { ProviderAccount, ProviderIssue, ProviderRequestFunction } from './models.js';
|
|
2
|
+
|
|
3
|
+
const jiraCloudApiUrl = 'https://api.atlassian.com/ex/jira';
|
|
4
|
+
const jiraIssueByKeyFields = [
|
|
5
|
+
'assignee',
|
|
6
|
+
'comment',
|
|
7
|
+
'created',
|
|
8
|
+
'creator',
|
|
9
|
+
'description',
|
|
10
|
+
'issuetype',
|
|
11
|
+
'labels',
|
|
12
|
+
'project',
|
|
13
|
+
'status',
|
|
14
|
+
'summary',
|
|
15
|
+
'updated',
|
|
16
|
+
'votes',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
type JiraIssueMemberResponse = {
|
|
20
|
+
accountId?: string;
|
|
21
|
+
displayName?: string | null;
|
|
22
|
+
emailAddress?: string | null;
|
|
23
|
+
avatarUrls?: Record<string, string | undefined>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type JiraIssueStatusResponse = {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
statusCategory: {
|
|
30
|
+
colorName: string;
|
|
31
|
+
key: string;
|
|
32
|
+
name: string;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type JiraIssueByKeyResponse = {
|
|
37
|
+
id: string;
|
|
38
|
+
key: string;
|
|
39
|
+
fields: {
|
|
40
|
+
assignee?: JiraIssueMemberResponse | null;
|
|
41
|
+
comment?: { comments?: unknown[]; total?: number };
|
|
42
|
+
created: string;
|
|
43
|
+
creator?: JiraIssueMemberResponse | null;
|
|
44
|
+
description?: string | null;
|
|
45
|
+
issuetype?: { name?: string | null };
|
|
46
|
+
labels?: string[];
|
|
47
|
+
project?: { id?: string | null; key?: string | null; name?: string | null };
|
|
48
|
+
status: JiraIssueStatusResponse;
|
|
49
|
+
summary: string;
|
|
50
|
+
updated: string;
|
|
51
|
+
votes?: { votes?: number | null };
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function toAccount(member: JiraIssueMemberResponse | null | undefined): ProviderAccount | null {
|
|
56
|
+
if (member == null) return null;
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
id: member.accountId ?? '',
|
|
60
|
+
name: member.displayName ?? null,
|
|
61
|
+
username: member.displayName ?? null,
|
|
62
|
+
email: member.emailAddress ?? null,
|
|
63
|
+
avatarUrl: member.avatarUrls?.['48x48'] ?? null,
|
|
64
|
+
url: null,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function toStatusCategory(key: string): 'TO_DO' | 'IN_PROGRESS' | 'DONE' {
|
|
69
|
+
switch (key.toLowerCase()) {
|
|
70
|
+
case 'new':
|
|
71
|
+
return 'TO_DO';
|
|
72
|
+
case 'indeterminate':
|
|
73
|
+
return 'IN_PROGRESS';
|
|
74
|
+
case 'done':
|
|
75
|
+
return 'DONE';
|
|
76
|
+
default:
|
|
77
|
+
return 'TO_DO';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function toIssueWebUrl(resourceUrl: string, key: string): string {
|
|
82
|
+
const url = new URL(resourceUrl);
|
|
83
|
+
url.pathname = `${url.pathname.replace(/\/+$/, '')}/browse/${encodeURIComponent(key)}`;
|
|
84
|
+
url.search = '';
|
|
85
|
+
url.hash = '';
|
|
86
|
+
return url.toString();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function fromJiraIssueByKey(issue: JiraIssueByKeyResponse, resourceId: string, resourceUrl: string): ProviderIssue {
|
|
90
|
+
const assignee = toAccount(issue.fields.assignee);
|
|
91
|
+
const project = issue.fields.project;
|
|
92
|
+
const status = issue.fields.status;
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
id: issue.id,
|
|
96
|
+
commentCount: issue.fields.comment?.total ?? issue.fields.comment?.comments?.length ?? null,
|
|
97
|
+
number: issue.key,
|
|
98
|
+
title: issue.fields.summary,
|
|
99
|
+
url: toIssueWebUrl(resourceUrl, issue.key),
|
|
100
|
+
closedDate: null,
|
|
101
|
+
createdDate: new Date(issue.fields.created),
|
|
102
|
+
author: toAccount(issue.fields.creator),
|
|
103
|
+
updatedDate: new Date(issue.fields.updated),
|
|
104
|
+
assignees: assignee != null ? [assignee] : [],
|
|
105
|
+
description: issue.fields.description ?? null,
|
|
106
|
+
repository: null,
|
|
107
|
+
project:
|
|
108
|
+
project != null
|
|
109
|
+
? {
|
|
110
|
+
name: project.name ?? '',
|
|
111
|
+
resourceId: resourceId,
|
|
112
|
+
key: project.key ?? null,
|
|
113
|
+
namespace: null,
|
|
114
|
+
id: project.id ?? null,
|
|
115
|
+
}
|
|
116
|
+
: undefined,
|
|
117
|
+
state: {
|
|
118
|
+
id: status.id,
|
|
119
|
+
name: status.name,
|
|
120
|
+
color: status.statusCategory.colorName,
|
|
121
|
+
category: toStatusCategory(status.statusCategory.key),
|
|
122
|
+
},
|
|
123
|
+
type: issue.fields.issuetype?.name ?? null,
|
|
124
|
+
upvoteCount: issue.fields.votes?.votes ?? null,
|
|
125
|
+
labels: (issue.fields.labels ?? []).map(label => ({
|
|
126
|
+
color: null,
|
|
127
|
+
description: null,
|
|
128
|
+
id: null,
|
|
129
|
+
name: label,
|
|
130
|
+
})),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The SDK's Jira point read performs resource and field discovery before the issue GET. This focused path uses
|
|
136
|
+
* caller-supplied resource identity so a cold lookup remains one upstream request.
|
|
137
|
+
*/
|
|
138
|
+
export async function requestJiraIssueByKey(
|
|
139
|
+
request: ProviderRequestFunction,
|
|
140
|
+
accessToken: string,
|
|
141
|
+
resourceId: string,
|
|
142
|
+
resourceUrl: string,
|
|
143
|
+
key: string,
|
|
144
|
+
): Promise<ProviderIssue> {
|
|
145
|
+
const url = new URL(
|
|
146
|
+
`${jiraCloudApiUrl}/${encodeURIComponent(resourceId)}/rest/api/2/issue/${encodeURIComponent(key)}`,
|
|
147
|
+
);
|
|
148
|
+
url.searchParams.set('fields', jiraIssueByKeyFields.join(','));
|
|
149
|
+
|
|
150
|
+
const response = await request<JiraIssueByKeyResponse>({
|
|
151
|
+
url: url.toString(),
|
|
152
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
153
|
+
});
|
|
154
|
+
return fromJiraIssueByKey(response.body, resourceId, resourceUrl);
|
|
155
|
+
}
|
|
@@ -12,7 +12,12 @@ import { toTokenWithInfo } from '../authentication/models.js';
|
|
|
12
12
|
import { toCollectionScopeFailure } from '../collectionMetadata.js';
|
|
13
13
|
import { IssuesCloudHostIntegrationId } from '../constants.js';
|
|
14
14
|
import { IntegrationReadUnavailableError } from '../errors.js';
|
|
15
|
-
import type {
|
|
15
|
+
import type {
|
|
16
|
+
AccountWideIssuesResult,
|
|
17
|
+
IssuesForProjectOptions,
|
|
18
|
+
ProjectIssuesDrain,
|
|
19
|
+
SearchMyIssuesOptions,
|
|
20
|
+
} from '../models/issueReads.js';
|
|
16
21
|
import { IssuesIntegration } from '../models/issuesIntegration.js';
|
|
17
22
|
import type { ProviderApiCollectionResult, ProviderIssue } from './models.js';
|
|
18
23
|
import { fromProviderIssue, providersMetadata, toIssueShape } from './models.js';
|
|
@@ -219,7 +224,7 @@ export class LinearIntegration extends IssuesIntegration<IssuesCloudHostIntegrat
|
|
|
219
224
|
session: ProviderAuthenticationSession,
|
|
220
225
|
project: ResourceDescriptor,
|
|
221
226
|
options?: IssuesForProjectOptions,
|
|
222
|
-
): Promise<
|
|
227
|
+
): Promise<ProjectIssuesDrain | undefined> {
|
|
223
228
|
if (!isIssueResourceDescriptor(project)) return undefined;
|
|
224
229
|
|
|
225
230
|
const api = await this.getProvidersApi();
|
|
@@ -256,6 +261,8 @@ export class LinearIntegration extends IssuesIntegration<IssuesCloudHostIntegrat
|
|
|
256
261
|
requestCount += 1;
|
|
257
262
|
hasMore = result.paging?.more ?? false;
|
|
258
263
|
const nextCursor = result.paging?.cursor;
|
|
264
|
+
truncated ||= result.paging?.truncated === true;
|
|
265
|
+
collectionMetadata = mergeCollectionMetadata(collectionMetadata, result.metadata);
|
|
259
266
|
for (const issue of result.values) {
|
|
260
267
|
const shape = toIssueShape(issue, this);
|
|
261
268
|
if (shape != null) {
|
|
@@ -270,7 +277,6 @@ export class LinearIntegration extends IssuesIntegration<IssuesCloudHostIntegrat
|
|
|
270
277
|
}
|
|
271
278
|
|
|
272
279
|
cursor = nextCursor;
|
|
273
|
-
// More pages remain but we've hit the backstop: the drain is incomplete.
|
|
274
280
|
if (hasMore && requestCount >= maxPagesPerRequest) {
|
|
275
281
|
truncated = true;
|
|
276
282
|
}
|
|
@@ -294,14 +300,16 @@ export class LinearIntegration extends IssuesIntegration<IssuesCloudHostIntegrat
|
|
|
294
300
|
'could not resolve the current user to scope issues to',
|
|
295
301
|
);
|
|
296
302
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
metadata: collectionMetadata
|
|
301
|
-
|
|
303
|
+
|
|
304
|
+
const values = issues.filter(issue => issue.assignees?.some(a => a.id === viewerId));
|
|
305
|
+
return truncated
|
|
306
|
+
? { values: values, truncated: true, recovery: 'none', metadata: collectionMetadata }
|
|
307
|
+
: { values: values, truncated: false, metadata: collectionMetadata };
|
|
302
308
|
}
|
|
303
309
|
|
|
304
|
-
return
|
|
310
|
+
return truncated
|
|
311
|
+
? { values: issues, truncated: true, recovery: 'none', metadata: collectionMetadata }
|
|
312
|
+
: { values: issues, truncated: false, metadata: collectionMetadata };
|
|
305
313
|
}
|
|
306
314
|
|
|
307
315
|
override get id(): IssuesCloudHostIntegrationId.Linear {
|
|
@@ -433,30 +441,35 @@ export class LinearIntegration extends IssuesIntegration<IssuesCloudHostIntegrat
|
|
|
433
441
|
return result && fromProviderIssue(result, this);
|
|
434
442
|
}
|
|
435
443
|
|
|
444
|
+
protected override async getProviderIssueByResourceId(
|
|
445
|
+
session: ProviderAuthenticationSession,
|
|
446
|
+
resourceId: string,
|
|
447
|
+
id: string,
|
|
448
|
+
_resourceUrl: string | undefined,
|
|
449
|
+
): Promise<Issue | undefined> {
|
|
450
|
+
const api = await this.getProvidersApi();
|
|
451
|
+
const result = await api.getIssue(toTokenWithInfo(this.id, session), {
|
|
452
|
+
resourceId: resourceId,
|
|
453
|
+
number: id,
|
|
454
|
+
});
|
|
455
|
+
return result && fromProviderIssue(result, this);
|
|
456
|
+
}
|
|
457
|
+
|
|
436
458
|
private async getRawProviderIssue(
|
|
437
459
|
session: ProviderAuthenticationSession,
|
|
438
460
|
resource: ResourceDescriptor,
|
|
439
461
|
id: string,
|
|
440
462
|
): Promise<ProviderIssue | undefined> {
|
|
441
463
|
const api = await this.getProvidersApi();
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
Logger.error(undefined, 'getProviderIssue: resource is not an IssueResourceDescriptor');
|
|
445
|
-
return undefined;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
const result = await api.getIssue(toTokenWithInfo(this.id, session), {
|
|
449
|
-
resourceId: resource.id,
|
|
450
|
-
number: id,
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
if (result == null) return undefined;
|
|
454
|
-
|
|
455
|
-
return result;
|
|
456
|
-
} catch (ex) {
|
|
457
|
-
Logger.error(ex, 'getProviderIssue');
|
|
464
|
+
if (!isIssueResourceDescriptor(resource)) {
|
|
465
|
+
Logger.error(undefined, 'getProviderIssue: resource is not an IssueResourceDescriptor');
|
|
458
466
|
return undefined;
|
|
459
467
|
}
|
|
468
|
+
|
|
469
|
+
return api.getIssue(toTokenWithInfo(this.id, session), {
|
|
470
|
+
resourceId: resource.id,
|
|
471
|
+
number: id,
|
|
472
|
+
});
|
|
460
473
|
}
|
|
461
474
|
private getIssueAutolinkLikeUrl(issue: ProviderIssue): string | null {
|
|
462
475
|
const url = issue.url;
|
|
@@ -1229,13 +1229,24 @@ function toIssueIdentifier(value: string | number): string {
|
|
|
1229
1229
|
return String(value);
|
|
1230
1230
|
}
|
|
1231
1231
|
|
|
1232
|
-
export function toIssueShape(
|
|
1232
|
+
export function toIssueShape(
|
|
1233
|
+
issue: ProviderIssue,
|
|
1234
|
+
provider: ProviderReference,
|
|
1235
|
+
options?: { reliableStateCategory?: boolean },
|
|
1236
|
+
): IssueShape | undefined {
|
|
1233
1237
|
// TODO: Add some protections/baselines rather than killing the transformation here
|
|
1234
1238
|
// `author` is intentionally not required: some providers have no per-item creator (e.g. Trello cards,
|
|
1235
1239
|
// which the SDK maps with `author: null`), and dropping every such item would discard the whole board.
|
|
1236
1240
|
// Fall back to an empty author instead so these issues still surface.
|
|
1237
1241
|
if (issue.updatedDate == null || issue.url == null) return undefined;
|
|
1238
1242
|
|
|
1243
|
+
// Jira SDK results derive this category from a localized display name and default unknown names to DONE.
|
|
1244
|
+
// Only the direct point read opts in because it maps Jira's stable status-category key itself.
|
|
1245
|
+
const closed =
|
|
1246
|
+
issue.closedDate != null ||
|
|
1247
|
+
(issue.state?.category === 'DONE' &&
|
|
1248
|
+
(provider.id !== IssuesCloudHostIntegrationId.Jira || options?.reliableStateCategory === true));
|
|
1249
|
+
|
|
1239
1250
|
return {
|
|
1240
1251
|
type: 'issue',
|
|
1241
1252
|
provider: provider,
|
|
@@ -1254,8 +1265,8 @@ export function toIssueShape(issue: ProviderIssue, provider: ProviderReference):
|
|
|
1254
1265
|
createdDate: issue.createdDate,
|
|
1255
1266
|
updatedDate: issue.updatedDate,
|
|
1256
1267
|
closedDate: issue.closedDate ?? undefined,
|
|
1257
|
-
closed:
|
|
1258
|
-
state:
|
|
1268
|
+
closed: closed,
|
|
1269
|
+
state: closed ? 'closed' : 'opened',
|
|
1259
1270
|
author: {
|
|
1260
1271
|
id: issue.author?.id ?? '',
|
|
1261
1272
|
// An absent name stays absent, matching {@link fromProviderAccount}; see `IssueMember.name`.
|
|
@@ -1688,6 +1699,7 @@ export function fromProviderIssue(
|
|
|
1688
1699
|
options?: { project?: IssueProject },
|
|
1689
1700
|
): Issue {
|
|
1690
1701
|
const identifier = toIssueIdentifier(issue.number);
|
|
1702
|
+
const closed = issue.closedDate != null || issue.state?.category === 'DONE';
|
|
1691
1703
|
return new Issue(
|
|
1692
1704
|
integration,
|
|
1693
1705
|
identifier,
|
|
@@ -1696,8 +1708,8 @@ export function fromProviderIssue(
|
|
|
1696
1708
|
issue.url ?? '',
|
|
1697
1709
|
issue.createdDate,
|
|
1698
1710
|
issue.updatedDate ?? issue.closedDate ?? issue.createdDate,
|
|
1699
|
-
|
|
1700
|
-
|
|
1711
|
+
closed,
|
|
1712
|
+
closed ? 'closed' : 'opened',
|
|
1701
1713
|
fromProviderAccount(issue.author),
|
|
1702
1714
|
issue.assignees?.map(fromProviderAccount) ?? undefined,
|
|
1703
1715
|
issue.repository != null
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { TokenWithInfo } from '../authentication/models.js';
|
|
2
|
+
import type { IntegrationIds } from '../constants.js';
|
|
3
|
+
import { IssuesCloudHostIntegrationId } from '../constants.js';
|
|
4
|
+
import {
|
|
5
|
+
AuthenticationError,
|
|
6
|
+
AuthenticationErrorReason,
|
|
7
|
+
isRateLimitResponse,
|
|
8
|
+
RequestClientError,
|
|
9
|
+
RequestNotFoundError,
|
|
10
|
+
toError,
|
|
11
|
+
toRateLimitError,
|
|
12
|
+
} from '../errors.js';
|
|
13
|
+
|
|
14
|
+
const linearIssueNotFoundMessage = /^Linear issue not found: .+$/i;
|
|
15
|
+
const maxProviderErrorBodyLength = 500;
|
|
16
|
+
|
|
17
|
+
type ProviderGraphQLError = {
|
|
18
|
+
message?: unknown;
|
|
19
|
+
extensions?: { code?: unknown };
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function getProviderGraphQLErrors(body: unknown): ProviderGraphQLError[] {
|
|
23
|
+
if (body == null || typeof body !== 'object') return [];
|
|
24
|
+
|
|
25
|
+
const errors = (body as { errors?: unknown }).errors;
|
|
26
|
+
return Array.isArray(errors)
|
|
27
|
+
? errors.filter((error): error is ProviderGraphQLError => error != null && typeof error === 'object')
|
|
28
|
+
: [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isLinearRateLimitError(providerId: IntegrationIds, ex: unknown): boolean {
|
|
32
|
+
if (providerId !== IssuesCloudHostIntegrationId.Linear) return false;
|
|
33
|
+
|
|
34
|
+
const body = (ex as { response?: { body?: unknown } }).response?.body;
|
|
35
|
+
return getProviderGraphQLErrors(body).some(error => error.extensions?.code === 'RATELIMITED');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isProviderIssueNotFoundError(providerId: IntegrationIds, ex: unknown): boolean {
|
|
39
|
+
if (providerId === IssuesCloudHostIntegrationId.Linear) {
|
|
40
|
+
return ex instanceof Error && linearIssueNotFoundMessage.test(ex.message);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const status = (ex as { response?: { status?: unknown } }).response?.status;
|
|
44
|
+
return status === 404 || status === 410 || status === 422;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function throwProviderError(tokenWithInfo: TokenWithInfo, error: unknown): never {
|
|
48
|
+
const { accessToken: token, ...tokenInfo } = tokenWithInfo;
|
|
49
|
+
const providerId = tokenWithInfo.providerId;
|
|
50
|
+
const normalizedError = toError(error);
|
|
51
|
+
// Linear reports GraphQL throttling as HTTP 400, so its structured code must win over generic 4xx handling.
|
|
52
|
+
if (isLinearRateLimitError(providerId, error)) {
|
|
53
|
+
throw toRateLimitError(normalizedError, token);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const status = (error as { response?: { status?: unknown } }).response?.status;
|
|
57
|
+
if (typeof status === 'number') {
|
|
58
|
+
switch (status) {
|
|
59
|
+
case 404:
|
|
60
|
+
case 410:
|
|
61
|
+
case 422:
|
|
62
|
+
throw new RequestNotFoundError(normalizedError);
|
|
63
|
+
case 429:
|
|
64
|
+
throw toRateLimitError(normalizedError, token);
|
|
65
|
+
case 401:
|
|
66
|
+
case 403:
|
|
67
|
+
// Some hosts overload 403 for throttling; classify that before asking the user to reconnect.
|
|
68
|
+
if (isRateLimitResponse({ status: status, message: normalizedError.message })) {
|
|
69
|
+
throw toRateLimitError(normalizedError, token);
|
|
70
|
+
}
|
|
71
|
+
throw new AuthenticationError(
|
|
72
|
+
tokenInfo,
|
|
73
|
+
status === 401 ? AuthenticationErrorReason.Unauthorized : AuthenticationErrorReason.Forbidden,
|
|
74
|
+
normalizedError,
|
|
75
|
+
);
|
|
76
|
+
default:
|
|
77
|
+
if (status >= 400 && status < 500) {
|
|
78
|
+
throw new RequestClientError(normalizedError);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Extracts the useful provider prose from an already-parsed SDK response body. */
|
|
87
|
+
export function getProviderResponseBodyMessage(body: unknown): string | undefined {
|
|
88
|
+
let message: string | undefined;
|
|
89
|
+
if (typeof body === 'string') {
|
|
90
|
+
message = body;
|
|
91
|
+
} else if (body != null && typeof body === 'object') {
|
|
92
|
+
const { message: direct, error } = body as { message?: unknown; error?: unknown };
|
|
93
|
+
if (typeof direct === 'string') {
|
|
94
|
+
message = direct;
|
|
95
|
+
} else if (typeof error === 'string') {
|
|
96
|
+
message = error;
|
|
97
|
+
} else if (error != null && typeof error === 'object') {
|
|
98
|
+
const nested = (error as { message?: unknown }).message;
|
|
99
|
+
if (typeof nested === 'string') {
|
|
100
|
+
message = nested;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (message == null) {
|
|
104
|
+
const graphQLError = getProviderGraphQLErrors(body)[0];
|
|
105
|
+
if (typeof graphQLError?.message === 'string') {
|
|
106
|
+
message = graphQLError.message;
|
|
107
|
+
} else if (typeof graphQLError?.extensions?.code === 'string') {
|
|
108
|
+
message = graphQLError.extensions.code;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
message = message?.trim();
|
|
114
|
+
return message ? message.slice(0, maxProviderErrorBodyLength) : undefined;
|
|
115
|
+
}
|