@gitkraken/core-gitlens 0.5.114 → 0.5.115
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 +26 -1
- 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 +22 -0
- 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/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/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 +2 -15
- package/dist/plus/integrations/reads/warnings.d.ts.map +1 -1
- package/dist/plus/integrations/reads/warnings.js +19 -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 +37 -26
- package/docs/kepler-read-api-parity.md +13 -0
- package/package.json +2 -2
- package/src/plus/git-github/api/github.ts +51 -58
- 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 +22 -0
- 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/issueTracker.ts +26 -24
- package/src/plus/integrations/reads/trackerIssue.ts +105 -0
- package/src/plus/integrations/reads/warnings.ts +23 -17
- package/src/plus/integrations/results.ts +4 -20
- package/src/utils/promise.ts +15 -0
|
@@ -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
|
+
}
|
|
@@ -20,15 +20,8 @@ import {
|
|
|
20
20
|
GitSelfManagedHostIntegrationId,
|
|
21
21
|
IssuesCloudHostIntegrationId,
|
|
22
22
|
} from '../constants.js';
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
AuthenticationErrorReason,
|
|
26
|
-
isRateLimitResponse,
|
|
27
|
-
RequestClientError,
|
|
28
|
-
RequestNotFoundError,
|
|
29
|
-
toError,
|
|
30
|
-
toRateLimitError,
|
|
31
|
-
} from '../errors.js';
|
|
23
|
+
import { RequestNotFoundError, toError } from '../errors.js';
|
|
24
|
+
import { requestJiraIssueByKey } from './jiraIssueByKey.js';
|
|
32
25
|
import type {
|
|
33
26
|
GetIssueFn,
|
|
34
27
|
GetIssuesForReposFn,
|
|
@@ -69,6 +62,7 @@ import type {
|
|
|
69
62
|
PullRequestFilter,
|
|
70
63
|
} from './models.js';
|
|
71
64
|
import { isRepoIdsInput, providersMetadata } from './models.js';
|
|
65
|
+
import { getProviderResponseBodyMessage, isProviderIssueNotFoundError, throwProviderError } from './providerErrors.js';
|
|
72
66
|
import {
|
|
73
67
|
collectProviderPagedResult,
|
|
74
68
|
mergeCollectionMetadata,
|
|
@@ -88,7 +82,6 @@ const createProviderApis: ProviderApisFactory =
|
|
|
88
82
|
// `Repository <x> not found`. Anchoring to that shape (rather than a loose `not found` substring) keeps
|
|
89
83
|
// the message fallbacks from misclassifying unrelated GraphQL/transport failures as a confident negative.
|
|
90
84
|
const repoNotFoundMessage = /^Repository .+ not found$/i;
|
|
91
|
-
|
|
92
85
|
// Duck-typed rather than `ex instanceof GraphQLErrors`, because importing the class as a value makes this
|
|
93
86
|
// module unloadable from plain Node ESM consumers: `@gitkraken/provider-apis` is CommonJS and declares its
|
|
94
87
|
// exports through getters, which Node's `cjs-module-lexer` cannot see, so the only named exports it can
|
|
@@ -562,43 +555,13 @@ export class ProvidersApi {
|
|
|
562
555
|
}
|
|
563
556
|
|
|
564
557
|
private handleProviderError<T>(tokenWithInfo: TokenWithInfo, error: any): T {
|
|
565
|
-
const { accessToken: token, ...tokenInfo } = tokenWithInfo;
|
|
566
558
|
const providerId = tokenWithInfo.providerId;
|
|
567
559
|
const provider = this.providers[providerId];
|
|
568
560
|
if (provider == null) {
|
|
569
561
|
throw new Error(`Provider with id ${providerId} not registered`);
|
|
570
562
|
}
|
|
571
563
|
|
|
572
|
-
|
|
573
|
-
const status: number = error.response.status;
|
|
574
|
-
switch (status) {
|
|
575
|
-
case 404: // Not found
|
|
576
|
-
case 410: // Gone
|
|
577
|
-
case 422: // Unprocessable Entity
|
|
578
|
-
throw new RequestNotFoundError(error);
|
|
579
|
-
case 429: // Too Many Requests
|
|
580
|
-
throw toRateLimitError(error, token);
|
|
581
|
-
case 401: // Unauthorized
|
|
582
|
-
case 403: // Forbidden
|
|
583
|
-
// A throttled request arrives on both statuses depending on the host (see
|
|
584
|
-
// `isRateLimitResponse`); 403 previously skipped the check, so every provider going through the
|
|
585
|
-
// SDK reported a rate limit as `auth` and asked the user to reconnect a healthy account.
|
|
586
|
-
if (isRateLimitResponse({ status: status, message: error.message })) {
|
|
587
|
-
throw toRateLimitError(error, token);
|
|
588
|
-
}
|
|
589
|
-
throw new AuthenticationError(
|
|
590
|
-
tokenInfo,
|
|
591
|
-
status === 401 ? AuthenticationErrorReason.Unauthorized : AuthenticationErrorReason.Forbidden,
|
|
592
|
-
error,
|
|
593
|
-
);
|
|
594
|
-
default:
|
|
595
|
-
if (status >= 400 && status < 500) {
|
|
596
|
-
throw new RequestClientError(error);
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
throw error;
|
|
564
|
+
return throwProviderError(tokenWithInfo, error);
|
|
602
565
|
}
|
|
603
566
|
|
|
604
567
|
async getPagedResult<T>(
|
|
@@ -878,7 +841,7 @@ export class ProvidersApi {
|
|
|
878
841
|
/** See {@link GetIssuesOptions.sort}. Linear expresses `created`/`updated`, descending only. */
|
|
879
842
|
sort?: IssueSorting;
|
|
880
843
|
},
|
|
881
|
-
): Promise<
|
|
844
|
+
): Promise<ProviderApiPagedResult<ProviderIssue>> {
|
|
882
845
|
const { provider, tokenWithInfo } = await this.ensureProviderTokenAndFunction(
|
|
883
846
|
tokenOptInfo,
|
|
884
847
|
'getLinearIssuesFn',
|
|
@@ -1522,7 +1485,7 @@ export class ProvidersApi {
|
|
|
1522
1485
|
tokenOptInfo: TokenOptInfo,
|
|
1523
1486
|
reposOrIds: ProviderReposInput,
|
|
1524
1487
|
options?: GetIssuesOptions & { isPAT?: boolean; baseUrl?: string },
|
|
1525
|
-
): Promise<
|
|
1488
|
+
): Promise<ProviderApiPagedResult<ProviderIssue>> {
|
|
1526
1489
|
const { provider, tokenWithInfo } = await this.ensureProviderTokenAndFunction(
|
|
1527
1490
|
tokenOptInfo,
|
|
1528
1491
|
'getIssuesForReposFn',
|
|
@@ -1837,6 +1800,24 @@ export class ProvidersApi {
|
|
|
1837
1800
|
);
|
|
1838
1801
|
}
|
|
1839
1802
|
|
|
1803
|
+
async getJiraIssueByKey(
|
|
1804
|
+
tokenOptInfo: TokenWithInfo<IssuesCloudHostIntegrationId.Jira>,
|
|
1805
|
+
resourceId: string,
|
|
1806
|
+
resourceUrl: string,
|
|
1807
|
+
key: string,
|
|
1808
|
+
): Promise<ProviderIssue | undefined> {
|
|
1809
|
+
const { tokenWithInfo } = await this.ensureProviderToken(tokenOptInfo);
|
|
1810
|
+
|
|
1811
|
+
try {
|
|
1812
|
+
return await requestJiraIssueByKey(this.request, tokenWithInfo.accessToken, resourceId, resourceUrl, key);
|
|
1813
|
+
} catch (e) {
|
|
1814
|
+
const status = (e as { response?: { status?: unknown } }).response?.status;
|
|
1815
|
+
if (status === 404) return undefined;
|
|
1816
|
+
|
|
1817
|
+
return this.handleProviderError<ProviderIssue | undefined>(tokenWithInfo, e);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1840
1821
|
async getIssue(
|
|
1841
1822
|
tokenOptInfo: TokenWithInfo,
|
|
1842
1823
|
input: { resourceId: string; number: string } | { namespace: string; name: string; number: string },
|
|
@@ -1854,6 +1835,8 @@ export class ProvidersApi {
|
|
|
1854
1835
|
|
|
1855
1836
|
return result?.data;
|
|
1856
1837
|
} catch (e) {
|
|
1838
|
+
if (isProviderIssueNotFoundError(tokenWithInfo.providerId, e)) return undefined;
|
|
1839
|
+
|
|
1857
1840
|
return this.handleProviderError<ProviderIssue | undefined>(tokenWithInfo, e);
|
|
1858
1841
|
}
|
|
1859
1842
|
}
|
|
@@ -1896,28 +1879,3 @@ async function parseFetchResponseForApi<T>(response: Response): Promise<Provider
|
|
|
1896
1879
|
|
|
1897
1880
|
return result;
|
|
1898
1881
|
}
|
|
1899
|
-
|
|
1900
|
-
const maxProviderErrorBodyLength = 500;
|
|
1901
|
-
|
|
1902
|
-
/** Extracts the useful provider prose from an already-parsed SDK response body. */
|
|
1903
|
-
function getProviderResponseBodyMessage(body: unknown): string | undefined {
|
|
1904
|
-
let message: string | undefined;
|
|
1905
|
-
if (typeof body === 'string') {
|
|
1906
|
-
message = body;
|
|
1907
|
-
} else if (body != null && typeof body === 'object') {
|
|
1908
|
-
const { message: direct, error } = body as { message?: unknown; error?: unknown };
|
|
1909
|
-
if (typeof direct === 'string') {
|
|
1910
|
-
message = direct;
|
|
1911
|
-
} else if (typeof error === 'string') {
|
|
1912
|
-
message = error;
|
|
1913
|
-
} else if (error != null && typeof error === 'object') {
|
|
1914
|
-
const nested = (error as { message?: unknown }).message;
|
|
1915
|
-
if (typeof nested === 'string') {
|
|
1916
|
-
message = nested;
|
|
1917
|
-
}
|
|
1918
|
-
}
|
|
1919
|
-
}
|
|
1920
|
-
|
|
1921
|
-
message = message?.trim();
|
|
1922
|
-
return message ? message.slice(0, maxProviderErrorBodyLength) : undefined;
|
|
1923
|
-
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { CollectionMetadata } from '@gitkraken/provider-apis';
|
|
2
1
|
import type { Account } from '../../../git/models/author.js';
|
|
3
2
|
import type { Issue, IssueShape } from '../../../git/models/issue.js';
|
|
4
3
|
import type { IssueOrPullRequest, IssueOrPullRequestType } from '../../../git/models/issueOrPullRequest.js';
|
|
@@ -9,7 +8,7 @@ import type { ProviderAuthenticationSession } from '../authentication/models.js'
|
|
|
9
8
|
import { toTokenWithInfo } from '../authentication/models.js';
|
|
10
9
|
import { IssuesCloudHostIntegrationId } from '../constants.js';
|
|
11
10
|
import { IntegrationReadUnavailableError } from '../errors.js';
|
|
12
|
-
import type { IssuesForProjectOptions } from '../models/issueReads.js';
|
|
11
|
+
import type { IssuesForProjectOptions, ProjectIssuesDrain } from '../models/issueReads.js';
|
|
13
12
|
import { IssuesIntegration } from '../models/issuesIntegration.js';
|
|
14
13
|
import { fromProviderIssue, providersMetadata, toIssueShape } from './models.js';
|
|
15
14
|
|
|
@@ -104,7 +103,7 @@ export class TrelloIntegration extends IssuesIntegration<IssuesCloudHostIntegrat
|
|
|
104
103
|
session: ProviderAuthenticationSession,
|
|
105
104
|
project: ResourceDescriptor,
|
|
106
105
|
options?: IssuesForProjectOptions,
|
|
107
|
-
): Promise<
|
|
106
|
+
): Promise<ProjectIssuesDrain | undefined> {
|
|
108
107
|
// A non-issue descriptor genuinely has nothing to read (empty), but a missing app key is a broken read.
|
|
109
108
|
if (!isIssueResourceDescriptor(project)) return undefined;
|
|
110
109
|
|
|
@@ -141,7 +140,9 @@ export class TrelloIntegration extends IssuesIntegration<IssuesCloudHostIntegrat
|
|
|
141
140
|
// never a cursor. Surface that as terminal truncation; there is no next page to fetch, so retrying the
|
|
142
141
|
// same read cannot recover the omitted cards (D11).
|
|
143
142
|
const truncated = result.metadata != null && result.metadata.completeness !== 'complete';
|
|
144
|
-
return
|
|
143
|
+
return truncated
|
|
144
|
+
? { values: values, truncated: true, recovery: 'none', metadata: result.metadata }
|
|
145
|
+
: { values: values, truncated: false, metadata: result.metadata };
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
protected override searchProviderMyIssues(
|
|
@@ -32,17 +32,24 @@ export function parsePageCursor(cursor: string | undefined): number | undefined
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/** Preserves successful sibling scopes, but doesn't turn an all-scope provider failure into an empty success. */
|
|
35
|
-
export
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
35
|
+
export function throwIfAllSettledFailed<T>(results: PromiseSettledResult<T>[]): void {
|
|
36
|
+
if (results.some(result => result.status === 'fulfilled')) return;
|
|
37
|
+
|
|
38
|
+
const rejected = results.find((result): result is PromiseRejectedResult => result.status === 'rejected');
|
|
39
|
+
if (rejected != null) throw rejected.reason;
|
|
40
|
+
}
|
|
42
41
|
|
|
42
|
+
export function flatSettledResultsOrThrow<T>(results: PromiseSettledResult<T[]>[]): T[] {
|
|
43
|
+
throwIfAllSettledFailed(results);
|
|
44
|
+
|
|
45
|
+
const fulfilled = results.filter((result): result is PromiseFulfilledResult<T[]> => result.status === 'fulfilled');
|
|
43
46
|
return fulfilled.flatMap(result => result.value);
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
export async function flatSettledOrThrow<T>(promises: Promise<T[]>[]): Promise<T[]> {
|
|
50
|
+
return flatSettledResultsOrThrow(await Promise.allSettled(promises));
|
|
51
|
+
}
|
|
52
|
+
|
|
46
53
|
/** Precedence for merged completeness: any known omission (`partial`) wins, then inability to confirm
|
|
47
54
|
* (`unknown`), and only an all-`complete` set of pages stays `complete`. */
|
|
48
55
|
const completenessRank: Record<CollectionCompleteness, number> = { partial: 2, unknown: 1, complete: 0 };
|
|
@@ -2,7 +2,7 @@ import type { CollectionMetadata } from '@gitkraken/provider-apis';
|
|
|
2
2
|
import type { IssueShape, IssueSorting } from '../../../git/models/issue.js';
|
|
3
3
|
import type { ResourceDescriptor } from '../../../git/models/resourceDescriptor.js';
|
|
4
4
|
import { mapBounded } from '../../../utils/promise.js';
|
|
5
|
-
import { mergeAssessmentInto } from '../collectionMetadata.js';
|
|
5
|
+
import { assessCollectionMetadata, mergeAssessmentInto } from '../collectionMetadata.js';
|
|
6
6
|
import type { IntegrationIds } from '../constants.js';
|
|
7
7
|
import { providerFanOutConcurrency } from '../constants.js';
|
|
8
8
|
import { isIssuesIntegration } from '../models/issuesIntegration.js';
|
|
@@ -452,10 +452,7 @@ export async function listIssueTrackerIssuesPage(
|
|
|
452
452
|
// Partial project discovery means some projects' issues are missing from this page; propagate it so the
|
|
453
453
|
// page reports fetchFailed even when every discovered project's own read succeeded.
|
|
454
454
|
let fetchFailed = projectDiscoveryFailed || accountLookupFailed;
|
|
455
|
-
|
|
456
|
-
// `truncated`; surface it as `page.truncated` so a windowed read isn't published as having drained each
|
|
457
|
-
// project completely.
|
|
458
|
-
let projectTruncated = projectDiscoveryTruncated;
|
|
455
|
+
let truncationRecovery: 'narrow-scope' | 'none' | undefined = projectDiscoveryTruncated ? 'none' : undefined;
|
|
459
456
|
let drainMetadata: CollectionMetadata | undefined;
|
|
460
457
|
for (const { project, value: result, warning } of perProject) {
|
|
461
458
|
const key = projectKey(project);
|
|
@@ -465,7 +462,7 @@ export async function listIssueTrackerIssuesPage(
|
|
|
465
462
|
// single failing token repeats verbatim once per project in the window.
|
|
466
463
|
appendDedupedWarning(warnings, warning);
|
|
467
464
|
fetchFailed = true;
|
|
468
|
-
|
|
465
|
+
truncationRecovery = 'none';
|
|
469
466
|
}
|
|
470
467
|
// A thrown/unsupported read (e.g. Linear not-implemented) surfaces as a warning with no value;
|
|
471
468
|
// mark the aggregate as fetchFailed so an empty result isn't mistaken for "no issues".
|
|
@@ -475,7 +472,8 @@ export async function listIssueTrackerIssuesPage(
|
|
|
475
472
|
if (result != null) {
|
|
476
473
|
items.push(...result.values);
|
|
477
474
|
if (result.truncated) {
|
|
478
|
-
|
|
475
|
+
const recovery = result.recovery;
|
|
476
|
+
truncationRecovery = truncationRecovery == null || truncationRecovery === recovery ? recovery : 'none';
|
|
479
477
|
}
|
|
480
478
|
if (result.metadata != null) {
|
|
481
479
|
drainMetadata = mergeCollectionMetadata(drainMetadata, result.metadata);
|
|
@@ -497,29 +495,33 @@ export async function listIssueTrackerIssuesPage(
|
|
|
497
495
|
// publishes. A no-op when nothing merged, since the tracker already ordered that single run.
|
|
498
496
|
const orderedItems = ordering.order(items);
|
|
499
497
|
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
498
|
+
const projectReadReportedTruncation = truncationRecovery != null;
|
|
499
|
+
const drainAssessment = assessCollectionMetadata(options.providerId, domain, options.connectionId, drainMetadata);
|
|
500
|
+
for (const warning of drainAssessment.warnings) {
|
|
501
|
+
// A project result already supplies the recovery semantics. Replace metadata's unstructured fallback with
|
|
502
|
+
// the structured warning below, while retaining specific failures and omissions.
|
|
503
|
+
if (projectReadReportedTruncation && !drainAssessment.fetchFailed && warning.omission == null) continue;
|
|
504
|
+
|
|
505
|
+
appendDedupedWarning(warnings, warning);
|
|
506
|
+
}
|
|
507
507
|
fetchFailed = fetchFailed || drainAssessment.fetchFailed;
|
|
508
|
-
|
|
508
|
+
if (drainAssessment.truncated) {
|
|
509
|
+
truncationRecovery = 'none';
|
|
510
|
+
}
|
|
509
511
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
512
|
+
const projectTruncated = truncationRecovery != null;
|
|
513
|
+
const scopeTooLarge = !fetchFailed && truncationRecovery === 'narrow-scope';
|
|
514
|
+
if (scopeTooLarge || (!fetchFailed && projectTruncated && !warnings.some(warning => warning.omission != null))) {
|
|
515
|
+
appendDedupedWarning(
|
|
516
|
+
warnings,
|
|
515
517
|
incompleteReadWarning(
|
|
516
518
|
options.providerId,
|
|
517
519
|
domain,
|
|
518
520
|
options.connectionId,
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
521
|
+
scopeTooLarge
|
|
522
|
+
? 'Some projects hold more issues than one read can return; narrow the scope to read the rest.'
|
|
523
|
+
: 'Some issues were omitted; the provider returned an incomplete result.',
|
|
524
|
+
scopeTooLarge ? 'scope-too-large' : 'exhausted',
|
|
523
525
|
),
|
|
524
526
|
);
|
|
525
527
|
}
|