@gitkraken/core-gitlens 0.9.3 → 0.9.5
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 +34 -1
- package/dist/git/models/issue.d.ts +25 -2
- package/dist/git/models/issue.js +3 -1
- package/dist/git/models/issue.js.map +1 -1
- package/dist/git/models/pullRequest.d.ts +55 -5
- package/dist/git/models/pullRequest.js +5 -1
- package/dist/git/models/pullRequest.js.map +1 -1
- package/dist/git/utils/issue.utils.d.ts +4 -0
- package/dist/git/utils/issue.utils.js +4 -0
- package/dist/git/utils/issue.utils.js.map +1 -1
- package/dist/git/utils/pullRequest.utils.d.ts +4 -0
- package/dist/git/utils/pullRequest.utils.js +8 -0
- package/dist/git/utils/pullRequest.utils.js.map +1 -1
- package/dist/plus/git-github/api/github.d.ts +64 -4
- package/dist/plus/git-github/api/github.js +232 -15
- package/dist/plus/git-github/api/github.js.map +1 -1
- package/dist/plus/git-github/models.d.ts +48 -6
- package/dist/plus/git-github/models.js +11 -10
- package/dist/plus/git-github/models.js.map +1 -1
- package/dist/plus/integrations/fieldPresence.d.ts +75 -0
- package/dist/plus/integrations/fieldPresence.js +381 -0
- package/dist/plus/integrations/fieldPresence.js.map +1 -0
- package/dist/plus/integrations/index.d.ts +6 -4
- package/dist/plus/integrations/index.js +2 -1
- package/dist/plus/integrations/index.js.map +1 -1
- package/dist/plus/integrations/integrationService.d.ts +13 -4
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/manager.d.ts +62 -7
- package/dist/plus/integrations/models/gitHostIntegration.d.ts +103 -9
- package/dist/plus/integrations/models/gitHostIntegration.js +116 -5
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
- package/dist/plus/integrations/models/integration.d.ts +91 -4
- package/dist/plus/integrations/models/integration.js +56 -7
- package/dist/plus/integrations/models/integration.js.map +1 -1
- package/dist/plus/integrations/models/issueReads.d.ts +14 -0
- package/dist/plus/integrations/models/issuesIntegration.d.ts +44 -3
- package/dist/plus/integrations/models/issuesIntegration.js +51 -1
- package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
- package/dist/plus/integrations/providers/azure/azure.js +3 -3
- package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
- package/dist/plus/integrations/providers/azure/models.d.ts +36 -4
- package/dist/plus/integrations/providers/azure/models.js +118 -8
- package/dist/plus/integrations/providers/azure/models.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOps.d.ts +102 -5
- package/dist/plus/integrations/providers/azureDevOps.js +264 -45
- package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOpsServer.d.ts +30 -6
- package/dist/plus/integrations/providers/azureDevOpsServer.js +82 -21
- package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket/bitbucket.d.ts +21 -1
- package/dist/plus/integrations/providers/bitbucket/bitbucket.js +74 -10
- package/dist/plus/integrations/providers/bitbucket/bitbucket.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket/models.d.ts +45 -8
- package/dist/plus/integrations/providers/bitbucket/models.js +32 -9
- package/dist/plus/integrations/providers/bitbucket/models.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket-server/models.d.ts +2 -6
- package/dist/plus/integrations/providers/bitbucket-server/models.js +1 -3
- package/dist/plus/integrations/providers/bitbucket-server/models.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.d.ts +3 -1
- package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.js +4 -1
- package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket-server.d.ts +2 -2
- package/dist/plus/integrations/providers/bitbucket-server.js +7 -5
- package/dist/plus/integrations/providers/bitbucket-server.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket.d.ts +22 -2
- package/dist/plus/integrations/providers/bitbucket.js +29 -6
- package/dist/plus/integrations/providers/bitbucket.js.map +1 -1
- package/dist/plus/integrations/providers/github.d.ts +40 -3
- package/dist/plus/integrations/providers/github.js +133 -11
- package/dist/plus/integrations/providers/github.js.map +1 -1
- package/dist/plus/integrations/providers/gitlab/gitlab.d.ts +34 -1
- package/dist/plus/integrations/providers/gitlab/gitlab.js +118 -4
- package/dist/plus/integrations/providers/gitlab/gitlab.js.map +1 -1
- package/dist/plus/integrations/providers/gitlab/models.d.ts +70 -5
- package/dist/plus/integrations/providers/gitlab/models.js +106 -13
- package/dist/plus/integrations/providers/gitlab/models.js.map +1 -1
- package/dist/plus/integrations/providers/gitlab.d.ts +31 -2
- package/dist/plus/integrations/providers/gitlab.js +48 -8
- package/dist/plus/integrations/providers/gitlab.js.map +1 -1
- package/dist/plus/integrations/providers/jira-server.d.ts +3 -2
- package/dist/plus/integrations/providers/jira-server.js +18 -9
- package/dist/plus/integrations/providers/jira-server.js.map +1 -1
- package/dist/plus/integrations/providers/jira.d.ts +26 -2
- package/dist/plus/integrations/providers/jira.js +120 -9
- package/dist/plus/integrations/providers/jira.js.map +1 -1
- package/dist/plus/integrations/providers/jiraIssueByKey.d.ts +79 -0
- package/dist/plus/integrations/providers/jiraIssueByKey.js +58 -7
- package/dist/plus/integrations/providers/jiraIssueByKey.js.map +1 -1
- package/dist/plus/integrations/providers/linear.d.ts +19 -1
- package/dist/plus/integrations/providers/linear.js +105 -7
- package/dist/plus/integrations/providers/linear.js.map +1 -1
- package/dist/plus/integrations/providers/linearIssuesEtag.d.ts +42 -0
- package/dist/plus/integrations/providers/linearIssuesEtag.js +94 -0
- package/dist/plus/integrations/providers/linearIssuesEtag.js.map +1 -0
- package/dist/plus/integrations/providers/models.d.ts +70 -8
- package/dist/plus/integrations/providers/models.js +59 -21
- package/dist/plus/integrations/providers/models.js.map +1 -1
- package/dist/plus/integrations/providers/providerErrors.js +8 -1
- package/dist/plus/integrations/providers/providerErrors.js.map +1 -1
- package/dist/plus/integrations/providers/providersApi.d.ts +70 -4
- package/dist/plus/integrations/providers/providersApi.js +148 -4
- package/dist/plus/integrations/providers/providersApi.js.map +1 -1
- package/dist/plus/integrations/providers/pullRequestReviews.d.ts +13 -0
- package/dist/plus/integrations/providers/pullRequestReviews.js +30 -6
- package/dist/plus/integrations/providers/pullRequestReviews.js.map +1 -1
- package/dist/plus/integrations/providers/trello.js +11 -7
- package/dist/plus/integrations/providers/trello.js.map +1 -1
- package/dist/plus/integrations/providers/utils/providerPaging.d.ts +11 -0
- package/dist/plus/integrations/providers/utils/providerPaging.js +47 -1
- package/dist/plus/integrations/providers/utils/providerPaging.js.map +1 -1
- package/dist/plus/integrations/providers/utils.d.ts +1 -1
- package/dist/plus/integrations/reads/batchEtags.d.ts +69 -0
- package/dist/plus/integrations/reads/batchEtags.js +114 -0
- package/dist/plus/integrations/reads/batchEtags.js.map +1 -0
- package/dist/plus/integrations/reads/counts.js +2 -2
- package/dist/plus/integrations/reads/counts.js.map +1 -1
- package/dist/plus/integrations/reads/currentAccount.js +2 -2
- package/dist/plus/integrations/reads/currentAccount.js.map +1 -1
- package/dist/plus/integrations/reads/cursors.d.ts +8 -0
- package/dist/plus/integrations/reads/cursors.js +5 -3
- package/dist/plus/integrations/reads/cursors.js.map +1 -1
- package/dist/plus/integrations/reads/drains.d.ts +13 -4
- package/dist/plus/integrations/reads/drains.js +29 -5
- package/dist/plus/integrations/reads/drains.js.map +1 -1
- package/dist/plus/integrations/reads/etag.d.ts +26 -0
- package/dist/plus/integrations/reads/etag.js +108 -0
- package/dist/plus/integrations/reads/etag.js.map +1 -0
- package/dist/plus/integrations/reads/filters.d.ts +8 -0
- package/dist/plus/integrations/reads/filters.js +2 -1
- package/dist/plus/integrations/reads/filters.js.map +1 -1
- package/dist/plus/integrations/reads/hierarchy.js +3 -3
- package/dist/plus/integrations/reads/hierarchy.js.map +1 -1
- package/dist/plus/integrations/reads/issueBatch.d.ts +47 -16
- package/dist/plus/integrations/reads/issueBatch.js +55 -87
- package/dist/plus/integrations/reads/issueBatch.js.map +1 -1
- package/dist/plus/integrations/reads/issueTracker.d.ts +10 -2
- package/dist/plus/integrations/reads/issueTracker.js +23 -6
- package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
- package/dist/plus/integrations/reads/issues.d.ts +1 -1
- package/dist/plus/integrations/reads/issues.js +4 -4
- package/dist/plus/integrations/reads/issues.js.map +1 -1
- package/dist/plus/integrations/reads/pullRequestBatch.d.ts +44 -6
- package/dist/plus/integrations/reads/pullRequestBatch.js +31 -34
- package/dist/plus/integrations/reads/pullRequestBatch.js.map +1 -1
- package/dist/plus/integrations/reads/pullRequestBranches.d.ts +5 -2
- package/dist/plus/integrations/reads/pullRequestBranches.js +4 -3
- package/dist/plus/integrations/reads/pullRequestBranches.js.map +1 -1
- package/dist/plus/integrations/reads/pullRequests.js +8 -4
- package/dist/plus/integrations/reads/pullRequests.js.map +1 -1
- package/dist/plus/integrations/reads/sweeps.js +8 -4
- package/dist/plus/integrations/reads/sweeps.js.map +1 -1
- package/dist/plus/integrations/reads/warnings.d.ts +23 -8
- package/dist/plus/integrations/reads/warnings.js +43 -19
- package/dist/plus/integrations/reads/warnings.js.map +1 -1
- package/dist/plus/integrations/results.d.ts +12 -4
- package/dist/plus/integrations/results.js +2 -0
- package/dist/plus/integrations/results.js.map +1 -1
- package/docs/integrations.md +389 -23
- package/docs/kepler-read-api-parity.md +16 -2
- package/package.json +2 -2
|
@@ -33,6 +33,14 @@ function toStatusCategory(key) {
|
|
|
33
33
|
default: return "TO_DO";
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
+
function toJiraIssueState(status) {
|
|
37
|
+
return {
|
|
38
|
+
id: status.id,
|
|
39
|
+
name: status.name,
|
|
40
|
+
color: status.statusCategory.colorName,
|
|
41
|
+
category: toStatusCategory(status.statusCategory.key)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
36
44
|
function toIssueWebUrl(resourceUrl, key) {
|
|
37
45
|
const url = new URL(resourceUrl);
|
|
38
46
|
url.pathname = `${url.pathname.replace(/\/+$/, "")}/browse/${encodeURIComponent(key)}`;
|
|
@@ -64,12 +72,7 @@ function fromJiraIssueByKey(issue, resourceId, resourceUrl) {
|
|
|
64
72
|
namespace: null,
|
|
65
73
|
id: project.id ?? null
|
|
66
74
|
} : void 0,
|
|
67
|
-
state:
|
|
68
|
-
id: status.id,
|
|
69
|
-
name: status.name,
|
|
70
|
-
color: status.statusCategory.colorName,
|
|
71
|
-
category: toStatusCategory(status.statusCategory.key)
|
|
72
|
-
},
|
|
75
|
+
state: toJiraIssueState(status),
|
|
73
76
|
type: issue.fields.issuetype?.name ?? null,
|
|
74
77
|
upvoteCount: issue.fields.votes?.votes ?? null,
|
|
75
78
|
labels: (issue.fields.labels ?? []).map((label) => ({
|
|
@@ -92,7 +95,55 @@ async function requestJiraIssueByKey(request, accessToken, resourceId, resourceU
|
|
|
92
95
|
headers: { Authorization: `Bearer ${accessToken}` }
|
|
93
96
|
})).body, resourceId, resourceUrl);
|
|
94
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* The most keys one bulk fetch takes. Atlassian allows up to 1000 for a request that names its fields explicitly, but
|
|
100
|
+
* documents 100 as the default limit.
|
|
101
|
+
*/
|
|
102
|
+
const jiraBulkFetchMaxKeys = 100;
|
|
103
|
+
/**
|
|
104
|
+
* The cheap check behind the batch issue read's etags: `status` and `updated` of up to {@link jiraBulkFetchMaxKeys}
|
|
105
|
+
* issues of one site in ONE request, where {@link requestJiraIssueByKey} reads one issue per request. Jira returns the
|
|
106
|
+
* issues in its own order, each under its CURRENT key (a lowercase or moved key resolves to the issue it names now), and
|
|
107
|
+
* omits the ones it can't find, so a caller matches them to its keys by key, never by position.
|
|
108
|
+
*/
|
|
109
|
+
async function requestJiraIssuesEtagFields(request, accessToken, resourceId, keys) {
|
|
110
|
+
const response = await request({
|
|
111
|
+
url: `${jiraCloudApiUrl}/${encodeURIComponent(resourceId)}/rest/api/3/issue/bulkfetch`,
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: {
|
|
114
|
+
Authorization: `Bearer ${accessToken}`,
|
|
115
|
+
"Content-Type": "application/json"
|
|
116
|
+
},
|
|
117
|
+
body: JSON.stringify({
|
|
118
|
+
issueIdsOrKeys: keys,
|
|
119
|
+
fields: ["status", "updated"]
|
|
120
|
+
})
|
|
121
|
+
});
|
|
122
|
+
const issues = response.body?.issues;
|
|
123
|
+
if (!Array.isArray(issues)) throw new Error("Jira bulk fetch returned no issues");
|
|
124
|
+
return {
|
|
125
|
+
issues,
|
|
126
|
+
errorCount: response.body?.issueErrors?.length ?? 0
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* A bulk-fetched issue's change state, as the fields the full row's etag reads. {@link fromJiraIssueByKey} maps the
|
|
131
|
+
* status with the same {@link toJiraIssueState} and never sets `closedDate`, so `toIssueShape` (with
|
|
132
|
+
* `reliableStateCategory`) closes exactly the issues whose status category is done. Throws on a missing status or an
|
|
133
|
+
* unparseable `updated`, so the target falls through to the full read rather than matching an etag that would ignore
|
|
134
|
+
* its update time.
|
|
135
|
+
*/
|
|
136
|
+
function toJiraIssueEtagFields(issue) {
|
|
137
|
+
const { status, updated } = issue.fields;
|
|
138
|
+
if (status?.statusCategory == null) throw new Error(`Jira bulk fetch returned no status for ${issue.key}`);
|
|
139
|
+
const updatedDate = new Date(updated ?? NaN);
|
|
140
|
+
if (Number.isNaN(updatedDate.getTime())) throw new Error(`Jira bulk fetch returned no update time for ${issue.key}`);
|
|
141
|
+
return {
|
|
142
|
+
state: toJiraIssueState(status).category === "DONE" ? "closed" : "opened",
|
|
143
|
+
updatedDate
|
|
144
|
+
};
|
|
145
|
+
}
|
|
95
146
|
//#endregion
|
|
96
|
-
export { requestJiraIssueByKey };
|
|
147
|
+
export { jiraBulkFetchMaxKeys, requestJiraIssueByKey, requestJiraIssuesEtagFields, toJiraIssueEtagFields };
|
|
97
148
|
|
|
98
149
|
//# sourceMappingURL=jiraIssueByKey.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jiraIssueByKey.js","names":[],"sources":["../../../../../plus/integrations/src/providers/jiraIssueByKey.ts"],"sourcesContent":["import type { ProviderAccount, ProviderIssue, ProviderRequestFunction } from './models.js';\n\nconst jiraCloudApiUrl = 'https://api.atlassian.com/ex/jira';\nconst jiraIssueByKeyFields = [\n\t'assignee',\n\t'comment',\n\t'created',\n\t'creator',\n\t'description',\n\t'issuetype',\n\t'labels',\n\t'project',\n\t'status',\n\t'summary',\n\t'updated',\n\t'votes',\n];\n\ntype JiraIssueMemberResponse = {\n\taccountId?: string;\n\tdisplayName?: string | null;\n\temailAddress?: string | null;\n\tavatarUrls?: Record<string, string | undefined>;\n};\n\ntype JiraIssueStatusResponse = {\n\tid: string;\n\tname: string;\n\tstatusCategory: {\n\t\tcolorName: string;\n\t\tkey: string;\n\t\tname: string;\n\t};\n};\n\ntype JiraIssueByKeyResponse = {\n\tid: string;\n\tkey: string;\n\tfields: {\n\t\tassignee?: JiraIssueMemberResponse | null;\n\t\tcomment?: { comments?: unknown[]; total?: number };\n\t\tcreated: string;\n\t\tcreator?: JiraIssueMemberResponse | null;\n\t\tdescription?: string | null;\n\t\tissuetype?: { name?: string | null };\n\t\tlabels?: string[];\n\t\tproject?: { id?: string | null; key?: string | null; name?: string | null };\n\t\tstatus: JiraIssueStatusResponse;\n\t\tsummary: string;\n\t\tupdated: string;\n\t\tvotes?: { votes?: number | null };\n\t};\n};\n\nfunction toAccount(member: JiraIssueMemberResponse | null | undefined): ProviderAccount | null {\n\tif (member == null) return null;\n\n\treturn {\n\t\tid: member.accountId ?? '',\n\t\tname: member.displayName ?? null,\n\t\tusername: member.displayName ?? null,\n\t\temail: member.emailAddress ?? null,\n\t\tavatarUrl: member.avatarUrls?.['48x48'] ?? null,\n\t\turl: null,\n\t};\n}\n\nfunction toStatusCategory(key: string): 'TO_DO' | 'IN_PROGRESS' | 'DONE' {\n\tswitch (key.toLowerCase()) {\n\t\tcase 'new':\n\t\t\treturn 'TO_DO';\n\t\tcase 'indeterminate':\n\t\t\treturn 'IN_PROGRESS';\n\t\tcase 'done':\n\t\t\treturn 'DONE';\n\t\tdefault:\n\t\t\treturn 'TO_DO';\n\t}\n}\n\nfunction toIssueWebUrl(resourceUrl: string, key: string): string {\n\tconst url = new URL(resourceUrl);\n\turl.pathname = `${url.pathname.replace(/\\/+$/, '')}/browse/${encodeURIComponent(key)}`;\n\turl.search = '';\n\turl.hash = '';\n\treturn url.toString();\n}\n\nfunction fromJiraIssueByKey(issue: JiraIssueByKeyResponse, resourceId: string, resourceUrl: string): ProviderIssue {\n\tconst assignee = toAccount(issue.fields.assignee);\n\tconst project = issue.fields.project;\n\tconst status = issue.fields.status;\n\n\treturn {\n\t\tid: issue.id,\n\t\tcommentCount: issue.fields.comment?.total ?? issue.fields.comment?.comments?.length ?? null,\n\t\tnumber: issue.key,\n\t\ttitle: issue.fields.summary,\n\t\turl: toIssueWebUrl(resourceUrl, issue.key),\n\t\tclosedDate: null,\n\t\tcreatedDate: new Date(issue.fields.created),\n\t\tauthor: toAccount(issue.fields.creator),\n\t\tupdatedDate: new Date(issue.fields.updated),\n\t\tassignees: assignee != null ? [assignee] : [],\n\t\tdescription: issue.fields.description ?? null,\n\t\trepository: null,\n\t\tproject:\n\t\t\tproject != null\n\t\t\t\t? {\n\t\t\t\t\t\tname: project.name ?? '',\n\t\t\t\t\t\tresourceId: resourceId,\n\t\t\t\t\t\tkey: project.key ?? null,\n\t\t\t\t\t\tnamespace: null,\n\t\t\t\t\t\tid: project.id ?? null,\n\t\t\t\t\t}\n\t\t\t\t: undefined,\n\t\tstate: {\n\t\t\tid: status.id,\n\t\t\tname: status.name,\n\t\t\tcolor: status.statusCategory.colorName,\n\t\t\tcategory: toStatusCategory(status.statusCategory.key),\n\t\t},\n\t\ttype: issue.fields.issuetype?.name ?? null,\n\t\tupvoteCount: issue.fields.votes?.votes ?? null,\n\t\tlabels: (issue.fields.labels ?? []).map(label => ({\n\t\t\tcolor: null,\n\t\t\tdescription: null,\n\t\t\tid: null,\n\t\t\tname: label,\n\t\t})),\n\t};\n}\n\n/**\n * The SDK's Jira point read performs resource and field discovery before the issue GET. This focused path uses\n * caller-supplied resource identity so a cold lookup remains one upstream request.\n */\nexport async function requestJiraIssueByKey(\n\trequest: ProviderRequestFunction,\n\taccessToken: string,\n\tresourceId: string,\n\tresourceUrl: string,\n\tkey: string,\n): Promise<ProviderIssue> {\n\tconst url = new URL(\n\t\t`${jiraCloudApiUrl}/${encodeURIComponent(resourceId)}/rest/api/2/issue/${encodeURIComponent(key)}`,\n\t);\n\turl.searchParams.set('fields', jiraIssueByKeyFields.join(','));\n\n\tconst response = await request<JiraIssueByKeyResponse>({\n\t\turl: url.toString(),\n\t\theaders: { Authorization: `Bearer ${accessToken}` },\n\t});\n\treturn fromJiraIssueByKey(response.body, resourceId, resourceUrl);\n}\n"],"mappings":";AAEA,MAAM,kBAAkB;AACxB,MAAM,uBAAuB;CAC5B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACD;AAsCA,SAAS,UAAU,QAA4E;CAC9F,IAAI,UAAU,MAAM,OAAO;CAE3B,OAAO;EACN,IAAI,OAAO,aAAa;EACxB,MAAM,OAAO,eAAe;EAC5B,UAAU,OAAO,eAAe;EAChC,OAAO,OAAO,gBAAgB;EAC9B,WAAW,OAAO,aAAa,YAAY;EAC3C,KAAK;CACN;AACD;AAEA,SAAS,iBAAiB,KAA+C;CACxE,QAAQ,IAAI,YAAY,GAAxB;EACC,KAAK,OACJ,OAAO;EACR,KAAK,iBACJ,OAAO;EACR,KAAK,QACJ,OAAO;EACR,SACC,OAAO;CACT;AACD;AAEA,SAAS,cAAc,aAAqB,KAAqB;CAChE,MAAM,MAAM,IAAI,IAAI,WAAW;CAC/B,IAAI,WAAW,GAAG,IAAI,SAAS,QAAQ,QAAQ,EAAE,EAAE,UAAU,mBAAmB,GAAG;CACnF,IAAI,SAAS;CACb,IAAI,OAAO;CACX,OAAO,IAAI,SAAS;AACrB;AAEA,SAAS,mBAAmB,OAA+B,YAAoB,aAAoC;CAClH,MAAM,WAAW,UAAU,MAAM,OAAO,QAAQ;CAChD,MAAM,UAAU,MAAM,OAAO;CAC7B,MAAM,SAAS,MAAM,OAAO;CAE5B,OAAO;EACN,IAAI,MAAM;EACV,cAAc,MAAM,OAAO,SAAS,SAAS,MAAM,OAAO,SAAS,UAAU,UAAU;EACvF,QAAQ,MAAM;EACd,OAAO,MAAM,OAAO;EACpB,KAAK,cAAc,aAAa,MAAM,GAAG;EACzC,YAAY;EACZ,aAAa,IAAI,KAAK,MAAM,OAAO,OAAO;EAC1C,QAAQ,UAAU,MAAM,OAAO,OAAO;EACtC,aAAa,IAAI,KAAK,MAAM,OAAO,OAAO;EAC1C,WAAW,YAAY,OAAO,CAAC,QAAQ,IAAI,CAAC;EAC5C,aAAa,MAAM,OAAO,eAAe;EACzC,YAAY;EACZ,SACC,WAAW,OACR;GACA,MAAM,QAAQ,QAAQ;GACV;GACZ,KAAK,QAAQ,OAAO;GACpB,WAAW;GACX,IAAI,QAAQ,MAAM;EACnB,IACC,KAAA;EACJ,OAAO;GACN,IAAI,OAAO;GACX,MAAM,OAAO;GACb,OAAO,OAAO,eAAe;GAC7B,UAAU,iBAAiB,OAAO,eAAe,GAAG;EACrD;EACA,MAAM,MAAM,OAAO,WAAW,QAAQ;EACtC,aAAa,MAAM,OAAO,OAAO,SAAS;EAC1C,SAAS,MAAM,OAAO,UAAU,CAAC,EAAA,CAAG,KAAI,WAAU;GACjD,OAAO;GACP,aAAa;GACb,IAAI;GACJ,MAAM;EACP,EAAE;CACH;AACD;;;;;AAMA,eAAsB,sBACrB,SACA,aACA,YACA,aACA,KACyB;CACzB,MAAM,MAAM,IAAI,IACf,GAAG,gBAAgB,GAAG,mBAAmB,UAAU,EAAE,oBAAoB,mBAAmB,GAAG,GAChG;CACA,IAAI,aAAa,IAAI,UAAU,qBAAqB,KAAK,GAAG,CAAC;CAM7D,OAAO,oBAAmB,MAJH,QAAgC;EACtD,KAAK,IAAI,SAAS;EAClB,SAAS,EAAE,eAAe,UAAU,cAAc;CACnD,CAAC,EAAA,CACkC,MAAM,YAAY,WAAW;AACjE"}
|
|
1
|
+
{"version":3,"file":"jiraIssueByKey.js","names":[],"sources":["../../../../../plus/integrations/src/providers/jiraIssueByKey.ts"],"sourcesContent":["import type { IssueEtagFields } from '../models/integration.js';\nimport type { ProviderAccount, ProviderIssue, ProviderRequestFunction } from './models.js';\n\nconst jiraCloudApiUrl = 'https://api.atlassian.com/ex/jira';\nconst jiraIssueByKeyFields = [\n\t'assignee',\n\t'comment',\n\t'created',\n\t'creator',\n\t'description',\n\t'issuetype',\n\t'labels',\n\t'project',\n\t'status',\n\t'summary',\n\t'updated',\n\t'votes',\n];\n\ntype JiraIssueMemberResponse = {\n\taccountId?: string;\n\tdisplayName?: string | null;\n\temailAddress?: string | null;\n\tavatarUrls?: Record<string, string | undefined>;\n};\n\ntype JiraIssueStatusResponse = {\n\tid: string;\n\tname: string;\n\tstatusCategory: {\n\t\tcolorName: string;\n\t\tkey: string;\n\t\tname: string;\n\t};\n};\n\ntype JiraIssueByKeyResponse = {\n\tid: string;\n\tkey: string;\n\tfields: {\n\t\tassignee?: JiraIssueMemberResponse | null;\n\t\tcomment?: { comments?: unknown[]; total?: number };\n\t\tcreated: string;\n\t\tcreator?: JiraIssueMemberResponse | null;\n\t\tdescription?: string | null;\n\t\tissuetype?: { name?: string | null };\n\t\tlabels?: string[];\n\t\tproject?: { id?: string | null; key?: string | null; name?: string | null };\n\t\tstatus: JiraIssueStatusResponse;\n\t\tsummary: string;\n\t\tupdated: string;\n\t\tvotes?: { votes?: number | null };\n\t};\n};\n\nfunction toAccount(member: JiraIssueMemberResponse | null | undefined): ProviderAccount | null {\n\tif (member == null) return null;\n\n\treturn {\n\t\tid: member.accountId ?? '',\n\t\tname: member.displayName ?? null,\n\t\tusername: member.displayName ?? null,\n\t\temail: member.emailAddress ?? null,\n\t\tavatarUrl: member.avatarUrls?.['48x48'] ?? null,\n\t\turl: null,\n\t};\n}\n\nfunction toStatusCategory(key: string): 'TO_DO' | 'IN_PROGRESS' | 'DONE' {\n\tswitch (key.toLowerCase()) {\n\t\tcase 'new':\n\t\t\treturn 'TO_DO';\n\t\tcase 'indeterminate':\n\t\t\treturn 'IN_PROGRESS';\n\t\tcase 'done':\n\t\t\treturn 'DONE';\n\t\tdefault:\n\t\t\treturn 'TO_DO';\n\t}\n}\n\nfunction toJiraIssueState(status: JiraIssueStatusResponse): NonNullable<ProviderIssue['state']> {\n\treturn {\n\t\tid: status.id,\n\t\tname: status.name,\n\t\tcolor: status.statusCategory.colorName,\n\t\tcategory: toStatusCategory(status.statusCategory.key),\n\t};\n}\n\nfunction toIssueWebUrl(resourceUrl: string, key: string): string {\n\tconst url = new URL(resourceUrl);\n\turl.pathname = `${url.pathname.replace(/\\/+$/, '')}/browse/${encodeURIComponent(key)}`;\n\turl.search = '';\n\turl.hash = '';\n\treturn url.toString();\n}\n\nfunction fromJiraIssueByKey(issue: JiraIssueByKeyResponse, resourceId: string, resourceUrl: string): ProviderIssue {\n\tconst assignee = toAccount(issue.fields.assignee);\n\tconst project = issue.fields.project;\n\tconst status = issue.fields.status;\n\n\treturn {\n\t\tid: issue.id,\n\t\tcommentCount: issue.fields.comment?.total ?? issue.fields.comment?.comments?.length ?? null,\n\t\tnumber: issue.key,\n\t\ttitle: issue.fields.summary,\n\t\turl: toIssueWebUrl(resourceUrl, issue.key),\n\t\tclosedDate: null,\n\t\tcreatedDate: new Date(issue.fields.created),\n\t\tauthor: toAccount(issue.fields.creator),\n\t\tupdatedDate: new Date(issue.fields.updated),\n\t\tassignees: assignee != null ? [assignee] : [],\n\t\tdescription: issue.fields.description ?? null,\n\t\trepository: null,\n\t\tproject:\n\t\t\tproject != null\n\t\t\t\t? {\n\t\t\t\t\t\tname: project.name ?? '',\n\t\t\t\t\t\tresourceId: resourceId,\n\t\t\t\t\t\tkey: project.key ?? null,\n\t\t\t\t\t\tnamespace: null,\n\t\t\t\t\t\tid: project.id ?? null,\n\t\t\t\t\t}\n\t\t\t\t: undefined,\n\t\tstate: toJiraIssueState(status),\n\t\ttype: issue.fields.issuetype?.name ?? null,\n\t\tupvoteCount: issue.fields.votes?.votes ?? null,\n\t\tlabels: (issue.fields.labels ?? []).map(label => ({\n\t\t\tcolor: null,\n\t\t\tdescription: null,\n\t\t\tid: null,\n\t\t\tname: label,\n\t\t})),\n\t};\n}\n\n/**\n * The SDK's Jira point read performs resource and field discovery before the issue GET. This focused path uses\n * caller-supplied resource identity so a cold lookup remains one upstream request.\n */\nexport async function requestJiraIssueByKey(\n\trequest: ProviderRequestFunction,\n\taccessToken: string,\n\tresourceId: string,\n\tresourceUrl: string,\n\tkey: string,\n): Promise<ProviderIssue> {\n\tconst url = new URL(\n\t\t`${jiraCloudApiUrl}/${encodeURIComponent(resourceId)}/rest/api/2/issue/${encodeURIComponent(key)}`,\n\t);\n\turl.searchParams.set('fields', jiraIssueByKeyFields.join(','));\n\n\tconst response = await request<JiraIssueByKeyResponse>({\n\t\turl: url.toString(),\n\t\theaders: { Authorization: `Bearer ${accessToken}` },\n\t});\n\treturn fromJiraIssueByKey(response.body, resourceId, resourceUrl);\n}\n\n/**\n * The most keys one bulk fetch takes. Atlassian allows up to 1000 for a request that names its fields explicitly, but\n * documents 100 as the default limit.\n */\nexport const jiraBulkFetchMaxKeys = 100;\n\n/**\n * One issue of a bulk fetch that asks for `status` and `updated` only. The bulk fetch is API v3 while the full read is\n * v2, but these two fields have the same shape in both: the v2 and v3 OpenAPI specs define the same `StatusDetails`\n * and `StatusCategory`, and a live comparison found both fields byte-identical. v3 differs in rich-text fields, which\n * it returns as ADF, so this request must never widen to `description`. `self` differs too, and is never read.\n */\nexport type JiraIssueEtagResponse = {\n\tid: string;\n\tkey: string;\n\tfields: Partial<Pick<JiraIssueByKeyResponse['fields'], 'status' | 'updated'>>;\n};\n\ntype JiraBulkFetchResponse = {\n\tissues?: JiraIssueEtagResponse[];\n\t/**\n\t * Only issues Jira could not return for a retriable error or a payload constraint, by numeric issue id rather than\n\t * the key asked for. Issues that don't exist or aren't visible are NOT listed here, nor in `issues`: they are\n\t * silently omitted, so a bulk fetch never proves an absence.\n\t */\n\tissueErrors?: { id?: string; errorMessage?: string }[];\n};\n\n/**\n * The cheap check behind the batch issue read's etags: `status` and `updated` of up to {@link jiraBulkFetchMaxKeys}\n * issues of one site in ONE request, where {@link requestJiraIssueByKey} reads one issue per request. Jira returns the\n * issues in its own order, each under its CURRENT key (a lowercase or moved key resolves to the issue it names now), and\n * omits the ones it can't find, so a caller matches them to its keys by key, never by position.\n */\nexport async function requestJiraIssuesEtagFields(\n\trequest: ProviderRequestFunction,\n\taccessToken: string,\n\tresourceId: string,\n\tkeys: readonly string[],\n): Promise<{ issues: JiraIssueEtagResponse[]; errorCount: number }> {\n\tconst response = await request<JiraBulkFetchResponse | null>({\n\t\turl: `${jiraCloudApiUrl}/${encodeURIComponent(resourceId)}/rest/api/3/issue/bulkfetch`,\n\t\tmethod: 'POST',\n\t\theaders: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },\n\t\tbody: JSON.stringify({ issueIdsOrKeys: keys, fields: ['status', 'updated'] }),\n\t});\n\n\tconst issues = response.body?.issues;\n\tif (!Array.isArray(issues)) throw new Error('Jira bulk fetch returned no issues');\n\n\treturn { issues: issues, errorCount: response.body?.issueErrors?.length ?? 0 };\n}\n\n/**\n * A bulk-fetched issue's change state, as the fields the full row's etag reads. {@link fromJiraIssueByKey} maps the\n * status with the same {@link toJiraIssueState} and never sets `closedDate`, so `toIssueShape` (with\n * `reliableStateCategory`) closes exactly the issues whose status category is done. Throws on a missing status or an\n * unparseable `updated`, so the target falls through to the full read rather than matching an etag that would ignore\n * its update time.\n */\nexport function toJiraIssueEtagFields(issue: JiraIssueEtagResponse): IssueEtagFields {\n\tconst { status, updated } = issue.fields;\n\tif (status?.statusCategory == null) throw new Error(`Jira bulk fetch returned no status for ${issue.key}`);\n\n\tconst updatedDate = new Date(updated ?? Number.NaN);\n\tif (Number.isNaN(updatedDate.getTime())) {\n\t\tthrow new Error(`Jira bulk fetch returned no update time for ${issue.key}`);\n\t}\n\n\treturn { state: toJiraIssueState(status).category === 'DONE' ? 'closed' : 'opened', updatedDate: updatedDate };\n}\n"],"mappings":";AAGA,MAAM,kBAAkB;AACxB,MAAM,uBAAuB;CAC5B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACD;AAsCA,SAAS,UAAU,QAA4E;CAC9F,IAAI,UAAU,MAAM,OAAO;CAE3B,OAAO;EACN,IAAI,OAAO,aAAa;EACxB,MAAM,OAAO,eAAe;EAC5B,UAAU,OAAO,eAAe;EAChC,OAAO,OAAO,gBAAgB;EAC9B,WAAW,OAAO,aAAa,YAAY;EAC3C,KAAK;CACN;AACD;AAEA,SAAS,iBAAiB,KAA+C;CACxE,QAAQ,IAAI,YAAY,GAAxB;EACC,KAAK,OACJ,OAAO;EACR,KAAK,iBACJ,OAAO;EACR,KAAK,QACJ,OAAO;EACR,SACC,OAAO;CACT;AACD;AAEA,SAAS,iBAAiB,QAAsE;CAC/F,OAAO;EACN,IAAI,OAAO;EACX,MAAM,OAAO;EACb,OAAO,OAAO,eAAe;EAC7B,UAAU,iBAAiB,OAAO,eAAe,GAAG;CACrD;AACD;AAEA,SAAS,cAAc,aAAqB,KAAqB;CAChE,MAAM,MAAM,IAAI,IAAI,WAAW;CAC/B,IAAI,WAAW,GAAG,IAAI,SAAS,QAAQ,QAAQ,EAAE,EAAE,UAAU,mBAAmB,GAAG;CACnF,IAAI,SAAS;CACb,IAAI,OAAO;CACX,OAAO,IAAI,SAAS;AACrB;AAEA,SAAS,mBAAmB,OAA+B,YAAoB,aAAoC;CAClH,MAAM,WAAW,UAAU,MAAM,OAAO,QAAQ;CAChD,MAAM,UAAU,MAAM,OAAO;CAC7B,MAAM,SAAS,MAAM,OAAO;CAE5B,OAAO;EACN,IAAI,MAAM;EACV,cAAc,MAAM,OAAO,SAAS,SAAS,MAAM,OAAO,SAAS,UAAU,UAAU;EACvF,QAAQ,MAAM;EACd,OAAO,MAAM,OAAO;EACpB,KAAK,cAAc,aAAa,MAAM,GAAG;EACzC,YAAY;EACZ,aAAa,IAAI,KAAK,MAAM,OAAO,OAAO;EAC1C,QAAQ,UAAU,MAAM,OAAO,OAAO;EACtC,aAAa,IAAI,KAAK,MAAM,OAAO,OAAO;EAC1C,WAAW,YAAY,OAAO,CAAC,QAAQ,IAAI,CAAC;EAC5C,aAAa,MAAM,OAAO,eAAe;EACzC,YAAY;EACZ,SACC,WAAW,OACR;GACA,MAAM,QAAQ,QAAQ;GACV;GACZ,KAAK,QAAQ,OAAO;GACpB,WAAW;GACX,IAAI,QAAQ,MAAM;EACnB,IACC,KAAA;EACJ,OAAO,iBAAiB,MAAM;EAC9B,MAAM,MAAM,OAAO,WAAW,QAAQ;EACtC,aAAa,MAAM,OAAO,OAAO,SAAS;EAC1C,SAAS,MAAM,OAAO,UAAU,CAAC,EAAA,CAAG,KAAI,WAAU;GACjD,OAAO;GACP,aAAa;GACb,IAAI;GACJ,MAAM;EACP,EAAE;CACH;AACD;;;;;AAMA,eAAsB,sBACrB,SACA,aACA,YACA,aACA,KACyB;CACzB,MAAM,MAAM,IAAI,IACf,GAAG,gBAAgB,GAAG,mBAAmB,UAAU,EAAE,oBAAoB,mBAAmB,GAAG,GAChG;CACA,IAAI,aAAa,IAAI,UAAU,qBAAqB,KAAK,GAAG,CAAC;CAM7D,OAAO,oBAAmB,MAJH,QAAgC;EACtD,KAAK,IAAI,SAAS;EAClB,SAAS,EAAE,eAAe,UAAU,cAAc;CACnD,CAAC,EAAA,CACkC,MAAM,YAAY,WAAW;AACjE;;;;;AAMA,MAAa,uBAAuB;;;;;;;AA8BpC,eAAsB,4BACrB,SACA,aACA,YACA,MACmE;CACnE,MAAM,WAAW,MAAM,QAAsC;EAC5D,KAAK,GAAG,gBAAgB,GAAG,mBAAmB,UAAU,EAAE;EAC1D,QAAQ;EACR,SAAS;GAAE,eAAe,UAAU;GAAe,gBAAgB;EAAmB;EACtF,MAAM,KAAK,UAAU;GAAE,gBAAgB;GAAM,QAAQ,CAAC,UAAU,SAAS;EAAE,CAAC;CAC7E,CAAC;CAED,MAAM,SAAS,SAAS,MAAM;CAC9B,IAAI,CAAC,MAAM,QAAQ,MAAM,GAAG,MAAM,IAAI,MAAM,oCAAoC;CAEhF,OAAO;EAAU;EAAQ,YAAY,SAAS,MAAM,aAAa,UAAU;CAAE;AAC9E;;;;;;;;AASA,SAAgB,sBAAsB,OAA+C;CACpF,MAAM,EAAE,QAAQ,YAAY,MAAM;CAClC,IAAI,QAAQ,kBAAkB,MAAM,MAAM,IAAI,MAAM,0CAA0C,MAAM,KAAK;CAEzG,MAAM,cAAc,IAAI,KAAK,WAAW,GAAU;CAClD,IAAI,OAAO,MAAM,YAAY,QAAQ,CAAC,GACrC,MAAM,IAAI,MAAM,+CAA+C,MAAM,KAAK;CAG3E,OAAO;EAAE,OAAO,iBAAiB,MAAM,CAAC,CAAC,aAAa,SAAS,WAAW;EAAuB;CAAY;AAC9G"}
|
|
@@ -5,9 +5,10 @@ import { Issue, IssueShape } from "../../../git/models/issue.js";
|
|
|
5
5
|
import { Account } from "../../../git/models/author.js";
|
|
6
6
|
import { ProviderAuthenticationSession } from "../authentication/models.js";
|
|
7
7
|
import { IssuesCloudHostIntegrationId } from "../constants.js";
|
|
8
|
+
import { ProviderApiCollectionResult } from "./models.js";
|
|
8
9
|
import { AccountWideIssuesResult, IssuesForProjectOptions, ProjectIssuesDrain, SearchMyIssuesOptions } from "../models/issueReads.js";
|
|
9
10
|
import { IssuesIntegration } from "../models/issuesIntegration.js";
|
|
10
|
-
import {
|
|
11
|
+
import { IssueEtagFields } from "../models/integration.js";
|
|
11
12
|
import { IntegrationAuthenticationProviderDescriptor } from "../authentication/integrationAuthenticationProvider.js";
|
|
12
13
|
//#region ../plus/integrations/src/providers/linear.d.ts
|
|
13
14
|
export interface LinearTeamDescriptor extends IssueResourceDescriptor {
|
|
@@ -74,6 +75,23 @@ export declare class LinearIntegration extends IssuesIntegration<IssuesCloudHost
|
|
|
74
75
|
}, _type: undefined | IssueOrPullRequestType): Promise<IssueOrPullRequest | undefined>;
|
|
75
76
|
protected getProviderIssue(session: ProviderAuthenticationSession, resource: ResourceDescriptor, id: string): Promise<Issue | undefined>;
|
|
76
77
|
protected getProviderIssueByResourceId(session: ProviderAuthenticationSession, resourceId: string, id: string, _resourceUrl: string | undefined): Promise<Issue | undefined>;
|
|
78
|
+
/**
|
|
79
|
+
* The cheap check behind the batch issue read's etags: one `issues` query per team and
|
|
80
|
+
* {@link linearIssuesEtagMaxNumbers} distinct numbers, where {@link getProviderIssueByResourceId} sends one request
|
|
81
|
+
* per issue. A request that throws rejects only its own targets' slots, with its error classified as the full
|
|
82
|
+
* read's would be.
|
|
83
|
+
*
|
|
84
|
+
* Never proves an absence. The query filters by the team's CURRENT key and asks for archived issues too, but the
|
|
85
|
+
* full read's `issue(id:)` may also resolve an identifier the issue no longer carries (its team was renamed, or it
|
|
86
|
+
* moved to another team), so an issue the query leaves out may still exist. Its target's slot is rejected, as is a
|
|
87
|
+
* target whose identifier isn't a team key and a number, so the full read decides. When no target was answered and
|
|
88
|
+
* nothing failed, the check declines instead, so a batch of only such targets costs a full read, not a failure.
|
|
89
|
+
*/
|
|
90
|
+
protected getProviderIssuesEtagFieldsByResourceId(session: ProviderAuthenticationSession, targets: readonly {
|
|
91
|
+
resourceId: string;
|
|
92
|
+
identifier: string;
|
|
93
|
+
resourceUrl?: string;
|
|
94
|
+
}[]): Promise<PromiseSettledResult<IssueEtagFields | undefined>[] | undefined>;
|
|
77
95
|
private getRawProviderIssue;
|
|
78
96
|
private getIssueAutolinkLikeUrl;
|
|
79
97
|
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import { chunk } from "../../../utils/array.js";
|
|
1
2
|
import { Logger as _logger } from "../../../utils/logger.js";
|
|
3
|
+
import { mapSettledBounded } from "../../../utils/promise.js";
|
|
2
4
|
import { isIssueResourceDescriptor } from "../../../git/utils/resourceDescriptor.utils.js";
|
|
3
5
|
import { IntegrationReadUnavailableError } from "../errors.js";
|
|
4
6
|
import "../constants.js";
|
|
5
7
|
import { toCollectionScopeFailure } from "../collectionMetadata.js";
|
|
6
8
|
import { toTokenWithInfo } from "../authentication/models.js";
|
|
7
9
|
import { mergeCollectionMetadata } from "./utils/providerPaging.js";
|
|
8
|
-
import { fromProviderIssue, providersMetadata, toIssueShape } from "./models.js";
|
|
10
|
+
import { fromProviderIssue, providersMetadata, toIssueShape, toProviderIssueStates } from "./models.js";
|
|
9
11
|
import { IssuesIntegration } from "../models/issuesIntegration.js";
|
|
10
12
|
import { DiscoveryCache } from "./utils/discoveryCache.js";
|
|
13
|
+
import { toLinearIssueEtagFields } from "./linearIssuesEtag.js";
|
|
11
14
|
import * as l10n from "@vscode/l10n";
|
|
12
15
|
//#region ../plus/integrations/src/providers/linear.ts
|
|
13
16
|
const metadata = providersMetadata["linear"];
|
|
@@ -32,6 +35,8 @@ const maxPagesPerRequest = 10;
|
|
|
32
35
|
*/
|
|
33
36
|
const maxAccountWidePagesPerRequest = 50;
|
|
34
37
|
const linearImplicitTeamsPageSize = 50;
|
|
38
|
+
/** A team key and issue number, as Linear writes an issue's identifier (`ENG-123`). */
|
|
39
|
+
const linearIdentifier = /^([A-Za-z0-9]+)-(\d+)$/;
|
|
35
40
|
var LinearIntegration = class extends IssuesIntegration {
|
|
36
41
|
/**
|
|
37
42
|
* Built from the teams, so it follows them: rebuilt when the team list changes or the caches are dropped, and once
|
|
@@ -169,6 +174,7 @@ var LinearIntegration = class extends IssuesIntegration {
|
|
|
169
174
|
async getProviderIssuesForProjectWithTruncation(session, project, options) {
|
|
170
175
|
if (!isIssueResourceDescriptor(project)) return void 0;
|
|
171
176
|
const api = await this.getProvidersApi();
|
|
177
|
+
const states = toProviderIssueStates(options?.state);
|
|
172
178
|
let cursor;
|
|
173
179
|
let hasMore;
|
|
174
180
|
let requestCount = 0;
|
|
@@ -178,7 +184,10 @@ var LinearIntegration = class extends IssuesIntegration {
|
|
|
178
184
|
do {
|
|
179
185
|
let result;
|
|
180
186
|
try {
|
|
181
|
-
result = await api.getLinearIssues(toTokenWithInfo(this.id, session), {
|
|
187
|
+
result = await api.getLinearIssues(toTokenWithInfo(this.id, session), {
|
|
188
|
+
teams: [project.id],
|
|
189
|
+
states
|
|
190
|
+
}, {
|
|
182
191
|
cursor,
|
|
183
192
|
sort: options?.sort
|
|
184
193
|
});
|
|
@@ -200,7 +209,7 @@ var LinearIntegration = class extends IssuesIntegration {
|
|
|
200
209
|
truncated ||= result.paging?.truncated === true;
|
|
201
210
|
collectionMetadata = mergeCollectionMetadata(collectionMetadata, result.metadata);
|
|
202
211
|
for (const issue of result.values) {
|
|
203
|
-
const shape = toIssueShape(issue, this);
|
|
212
|
+
const shape = toIssueShape(issue, this, { projection: "project" });
|
|
204
213
|
if (shape != null) issues.push(shape);
|
|
205
214
|
}
|
|
206
215
|
if (hasMore && (nextCursor == null || nextCursor === cursor)) {
|
|
@@ -273,6 +282,7 @@ var LinearIntegration = class extends IssuesIntegration {
|
|
|
273
282
|
async searchProviderMyIssuesWithTruncation(session, resources, cancellation, options) {
|
|
274
283
|
if (resources != null) return;
|
|
275
284
|
const api = await this.getProvidersApi();
|
|
285
|
+
const states = toProviderIssueStates(options?.state);
|
|
276
286
|
let cursor = void 0;
|
|
277
287
|
let hasMore = false;
|
|
278
288
|
let requestCount = 0;
|
|
@@ -283,12 +293,13 @@ var LinearIntegration = class extends IssuesIntegration {
|
|
|
283
293
|
if (cancellation?.aborted) break;
|
|
284
294
|
const result = await api.getIssuesForCurrentUser(toTokenWithInfo(this.id, session), {
|
|
285
295
|
cursor,
|
|
296
|
+
states,
|
|
286
297
|
sort: options?.sort
|
|
287
298
|
});
|
|
288
299
|
requestCount += 1;
|
|
289
300
|
hasMore = result.paging?.more ?? false;
|
|
290
301
|
const nextCursor = result.paging?.cursor;
|
|
291
|
-
const formattedIssues = result.values.map((issue) => toIssueShape(issue, this)).filter((result) => result != null);
|
|
302
|
+
const formattedIssues = result.values.map((issue) => toIssueShape(issue, this, { projection: "account" })).filter((result) => result != null);
|
|
292
303
|
if (formattedIssues.length > 0) issues.push(...formattedIssues);
|
|
293
304
|
if (hasMore && (nextCursor == null || nextCursor === cursor)) {
|
|
294
305
|
truncated = true;
|
|
@@ -313,18 +324,105 @@ var LinearIntegration = class extends IssuesIntegration {
|
|
|
313
324
|
...issue,
|
|
314
325
|
url: this.getIssueAutolinkLikeUrl(issue)
|
|
315
326
|
};
|
|
316
|
-
return autolinkableIssue && toIssueShape(autolinkableIssue, this);
|
|
327
|
+
return autolinkableIssue && toIssueShape(autolinkableIssue, this, { projection: "point" });
|
|
317
328
|
}
|
|
318
329
|
async getProviderIssue(session, resource, id) {
|
|
319
330
|
const result = await this.getRawProviderIssue(session, resource, id);
|
|
320
|
-
return result && fromProviderIssue(result, this);
|
|
331
|
+
return result && fromProviderIssue(result, this, { projection: "point" });
|
|
321
332
|
}
|
|
322
333
|
async getProviderIssueByResourceId(session, resourceId, id, _resourceUrl) {
|
|
323
334
|
const result = await (await this.getProvidersApi()).getIssue(toTokenWithInfo(this.id, session), {
|
|
324
335
|
resourceId,
|
|
325
336
|
number: id
|
|
326
337
|
});
|
|
327
|
-
return result && fromProviderIssue(result, this);
|
|
338
|
+
return result && fromProviderIssue(result, this, { projection: "batch" });
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* The cheap check behind the batch issue read's etags: one `issues` query per team and
|
|
342
|
+
* {@link linearIssuesEtagMaxNumbers} distinct numbers, where {@link getProviderIssueByResourceId} sends one request
|
|
343
|
+
* per issue. A request that throws rejects only its own targets' slots, with its error classified as the full
|
|
344
|
+
* read's would be.
|
|
345
|
+
*
|
|
346
|
+
* Never proves an absence. The query filters by the team's CURRENT key and asks for archived issues too, but the
|
|
347
|
+
* full read's `issue(id:)` may also resolve an identifier the issue no longer carries (its team was renamed, or it
|
|
348
|
+
* moved to another team), so an issue the query leaves out may still exist. Its target's slot is rejected, as is a
|
|
349
|
+
* target whose identifier isn't a team key and a number, so the full read decides. When no target was answered and
|
|
350
|
+
* nothing failed, the check declines instead, so a batch of only such targets costs a full read, not a failure.
|
|
351
|
+
*/
|
|
352
|
+
async getProviderIssuesEtagFieldsByResourceId(session, targets) {
|
|
353
|
+
const slots = new Array(targets.length);
|
|
354
|
+
let answered = false;
|
|
355
|
+
let failed = false;
|
|
356
|
+
const byTeam = /* @__PURE__ */ new Map();
|
|
357
|
+
for (const [index, target] of targets.entries()) {
|
|
358
|
+
const match = linearIdentifier.exec(target.identifier);
|
|
359
|
+
const number = match != null ? Number(match[2]) : NaN;
|
|
360
|
+
if (match == null || !Number.isSafeInteger(number) || String(number) !== match[2]) {
|
|
361
|
+
slots[index] = {
|
|
362
|
+
status: "rejected",
|
|
363
|
+
reason: /* @__PURE__ */ new Error(`Not a Linear issue identifier: ${target.identifier}`)
|
|
364
|
+
};
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
const teamKey = match[1].toUpperCase();
|
|
368
|
+
const groupKey = `${target.resourceId}\n${teamKey}`;
|
|
369
|
+
let group = byTeam.get(groupKey);
|
|
370
|
+
if (group == null) {
|
|
371
|
+
group = {
|
|
372
|
+
teamKey,
|
|
373
|
+
numbers: /* @__PURE__ */ new Map()
|
|
374
|
+
};
|
|
375
|
+
byTeam.set(groupKey, group);
|
|
376
|
+
}
|
|
377
|
+
let indices = group.numbers.get(number);
|
|
378
|
+
if (indices == null) {
|
|
379
|
+
indices = [];
|
|
380
|
+
group.numbers.set(number, indices);
|
|
381
|
+
}
|
|
382
|
+
indices.push(index);
|
|
383
|
+
}
|
|
384
|
+
const requests = [...byTeam.values()].flatMap(({ teamKey, numbers }) => chunk([...numbers], 50).map((entries) => ({
|
|
385
|
+
teamKey,
|
|
386
|
+
entries
|
|
387
|
+
})));
|
|
388
|
+
if (requests.length) {
|
|
389
|
+
const api = await this.getProvidersApi();
|
|
390
|
+
const tokenWithInfo = toTokenWithInfo(this.id, session);
|
|
391
|
+
const answers = await mapSettledBounded(requests, 6, (r) => api.getLinearIssuesEtagFields(tokenWithInfo, r.teamKey, r.entries.map(([number]) => number)));
|
|
392
|
+
for (const [i, answer] of answers.entries()) {
|
|
393
|
+
const { teamKey, entries } = requests[i];
|
|
394
|
+
if (answer.status === "rejected") {
|
|
395
|
+
failed = true;
|
|
396
|
+
for (const [, indices] of entries) for (const index of indices) slots[index] = answer;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
const byNumber = /* @__PURE__ */ new Map();
|
|
400
|
+
for (const issue of answer.value) byNumber.set(issue.number, issue);
|
|
401
|
+
for (const [number, indices] of entries) {
|
|
402
|
+
const issue = byNumber.get(number);
|
|
403
|
+
let slot;
|
|
404
|
+
if (issue == null) slot = {
|
|
405
|
+
status: "rejected",
|
|
406
|
+
reason: /* @__PURE__ */ new Error(`Linear did not return ${teamKey}-${number}`)
|
|
407
|
+
};
|
|
408
|
+
else try {
|
|
409
|
+
slot = {
|
|
410
|
+
status: "fulfilled",
|
|
411
|
+
value: toLinearIssueEtagFields(issue)
|
|
412
|
+
};
|
|
413
|
+
answered = true;
|
|
414
|
+
} catch (ex) {
|
|
415
|
+
slot = {
|
|
416
|
+
status: "rejected",
|
|
417
|
+
reason: ex
|
|
418
|
+
};
|
|
419
|
+
failed = true;
|
|
420
|
+
}
|
|
421
|
+
for (const index of indices) slots[index] = slot;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return answered || failed ? slots : void 0;
|
|
328
426
|
}
|
|
329
427
|
async getRawProviderIssue(session, resource, id) {
|
|
330
428
|
const api = await this.getProvidersApi();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"linear.js","names":[],"sources":["../../../../../plus/integrations/src/providers/linear.ts"],"sourcesContent":["import type { CollectionMetadata } from '@gitkraken/provider-apis';\nimport * as l10n from '@vscode/l10n';\nimport type { Account } from '@gitlens/git/models/author.js';\nimport type { AutolinkReference, DynamicAutolinkReference } from '@gitlens/git/models/autolink.js';\nimport type { Issue, IssueShape } from '@gitlens/git/models/issue.js';\nimport type { IssueOrPullRequest, IssueOrPullRequestType } from '@gitlens/git/models/issueOrPullRequest.js';\nimport type { IssueResourceDescriptor, ResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport { isIssueResourceDescriptor } from '@gitlens/git/utils/resourceDescriptor.utils.js';\nimport { Logger } from '@gitlens/utils/logger.js';\nimport type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider.js';\nimport type { ProviderAuthenticationSession } from '../authentication/models.js';\nimport { toTokenWithInfo } from '../authentication/models.js';\nimport { toCollectionScopeFailure } from '../collectionMetadata.js';\nimport { IssuesCloudHostIntegrationId } from '../constants.js';\nimport { IntegrationReadUnavailableError } from '../errors.js';\nimport type {\n\tAccountWideIssuesResult,\n\tIssuesForProjectOptions,\n\tProjectIssuesDrain,\n\tSearchMyIssuesOptions,\n} from '../models/issueReads.js';\nimport { IssuesIntegration } from '../models/issuesIntegration.js';\nimport type { ProviderApiCollectionResult, ProviderIssue } from './models.js';\nimport { fromProviderIssue, providersMetadata, toIssueShape } from './models.js';\nimport { DiscoveryCache, discoveryCacheTtl } from './utils/discoveryCache.js';\nimport { mergeCollectionMetadata } from './utils/providerPaging.js';\n\nconst metadata = providersMetadata[IssuesCloudHostIntegrationId.Linear];\nconst authProvider = Object.freeze({ id: metadata.id, scopes: metadata.scopes });\nconst maxPagesPerRequest = 10;\n/**\n * The account-wide drain's own backstop, separate from {@link maxPagesPerRequest}.\n *\n * Raised to 50 (5,000 issues at the SDK's 100-per-page) because 10 no longer means what it used to. That read\n * once fanned out over four overlapping relationship queries and merged them, so most of what a page cost was\n * rows already returned — a low budget capped the waste. It is now a single server-ordered query over an `or`\n * filter, so every page is 100 issues the caller has not seen, and the same budget just truncates real results\n * at 1,000.\n *\n * Reaching it is reported rather than silent: {@link LinearIntegration.searchProviderMyIssuesWithTruncation}\n * returns `truncated`, so the facade can say the read was incomplete instead of publishing a capped list as a\n * whole account. The bound stays — a runaway cursor must not spend requests forever — but it is now set where a\n * real Linear account is unlikely to reach it rather than where the duplication used to hurt.\n */\nconst maxAccountWidePagesPerRequest = 50;\nconst linearImplicitTeamsPageSize = 50;\n\nexport interface LinearTeamDescriptor extends IssueResourceDescriptor {\n\tavatarUrl: string | undefined;\n}\n\nexport interface LinearOrganizationDescriptor extends IssueResourceDescriptor {\n\turl: string;\n}\n\nexport interface LinearProjectDescriptor extends IssueResourceDescriptor {}\n\nexport class LinearIntegration extends IssuesIntegration<IssuesCloudHostIntegrationId.Linear> {\n\t/**\n\t * Built from the teams, so it follows them: rebuilt when the team list changes or the caches are dropped, and once\n\t * older than {@link discoveryCacheTtl}. An expired set is still served while it is rebuilt in the background, because\n\t * autolinks are read on every render of a commit message and must not wait on the network (#5907).\n\t */\n\tprivate readonly _autolinks = new Map<\n\t\tstring,\n\t\t{ autolinks: (AutolinkReference | DynamicAutolinkReference)[]; builtAt: number }\n\t>();\n\tprivate readonly _autolinksBuilds = new Map<string, Promise<(AutolinkReference | DynamicAutolinkReference)[]>>();\n\n\toverride async autolinks(): Promise<(AutolinkReference | DynamicAutolinkReference)[]> {\n\t\tconst connected = this.maybeConnected ?? (await this.isConnected());\n\t\tif (!connected || this._session == null) {\n\t\t\treturn [];\n\t\t}\n\n\t\tconst session = this._session;\n\t\tconst cached = this._autolinks.get(session.accessToken);\n\t\tif (cached == null) return this.buildAutolinks(session);\n\n\t\tif (Date.now() - cached.builtAt >= discoveryCacheTtl) {\n\t\t\tvoid this.buildAutolinks(session).catch(() => {});\n\t\t}\n\t\treturn cached.autolinks;\n\t}\n\n\t/** One build per token at a time: concurrent renders share it rather than each re-reading the teams. */\n\tprivate buildAutolinks(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<(AutolinkReference | DynamicAutolinkReference)[]> {\n\t\tconst { accessToken } = session;\n\t\tlet build = this._autolinksBuilds.get(accessToken);\n\t\tif (build == null) {\n\t\t\tbuild = this.buildAutolinksCore(session).finally(() => this._autolinksBuilds.delete(accessToken));\n\t\t\tthis._autolinksBuilds.set(accessToken, build);\n\t\t}\n\t\treturn build;\n\t}\n\n\tprivate async buildAutolinksCore(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<(AutolinkReference | DynamicAutolinkReference)[]> {\n\t\tconst generation = this._teams.generation;\n\t\tconst organization = await this.getOrganization(session);\n\t\tif (organization == null) return [];\n\n\t\tconst autolinks: (AutolinkReference | DynamicAutolinkReference)[] = [];\n\n\t\tconst teams = await this.getTeams(session);\n\t\tfor (const team of teams ?? []) {\n\t\t\tconst dashedPrefix = `${team.key}-`;\n\t\t\tconst underscoredPrefix = `${team.key}_`;\n\n\t\t\tautolinks.push({\n\t\t\t\tprefix: dashedPrefix,\n\t\t\t\turl: `${organization.url}/issue/${dashedPrefix}<num>`,\n\t\t\t\talphanumeric: false,\n\t\t\t\tignoreCase: false,\n\t\t\t\ttitle: l10n.t('Open Issue {0} on {1}', `${dashedPrefix}<num>`, organization.name),\n\n\t\t\t\ttype: 'issue',\n\t\t\t\tdescription: l10n.t('{0} Issue {1}', organization.name, `${dashedPrefix}<num>`),\n\t\t\t\tdescriptor: { ...organization },\n\t\t\t});\n\t\t\tautolinks.push({\n\t\t\t\tprefix: underscoredPrefix,\n\t\t\t\turl: `${organization.url}/issue/${dashedPrefix}<num>`,\n\t\t\t\talphanumeric: false,\n\t\t\t\tignoreCase: false,\n\t\t\t\treferenceType: 'branch',\n\t\t\t\ttitle: l10n.t('Open Issue {0} on {1}', `${dashedPrefix}<num>`, organization.name),\n\n\t\t\t\ttype: 'issue',\n\t\t\t\tdescription: l10n.t('{0} Issue {1}', organization.name, `${dashedPrefix}<num>`),\n\t\t\t\tdescriptor: { ...organization },\n\t\t\t});\n\t\t}\n\n\t\t// Not kept when built from teams read before the caches were dropped.\n\t\tif (this._teams.generation === generation) {\n\t\t\tthis._autolinks.set(session.accessToken, { autolinks: autolinks, builtAt: Date.now() });\n\t\t}\n\t\treturn autolinks;\n\t}\n\n\tprivate _organizations: Map<string, LinearOrganizationDescriptor | undefined> | undefined;\n\tprivate async getOrganization(\n\t\tsession: ProviderAuthenticationSession,\n\t\tforce: boolean = false,\n\t): Promise<LinearOrganizationDescriptor | undefined> {\n\t\tconst { accessToken } = session;\n\t\tthis._organizations ??= new Map<string, LinearOrganizationDescriptor | undefined>();\n\n\t\tconst cachedResources = this._organizations.get(accessToken);\n\n\t\tif (cachedResources == null || force) {\n\t\t\tconst api = await this.getProvidersApi();\n\t\t\tconst organization = await api.getLinearOrganization(toTokenWithInfo(this.id, session));\n\t\t\tconst descriptor: LinearOrganizationDescriptor | undefined = organization && {\n\t\t\t\tid: organization.id,\n\t\t\t\tkey: organization.key,\n\t\t\t\tname: organization.name,\n\t\t\t\turl: organization.url,\n\t\t\t};\n\t\t\tif (descriptor) {\n\t\t\t\tthis._organizations.set(accessToken, descriptor);\n\t\t\t}\n\t\t}\n\n\t\treturn this._organizations.get(accessToken);\n\t}\n\n\t/** Expires, and is dropped on a re-sync, so a team created or left mid-session is picked up (#5907). */\n\tprivate readonly _teams = new DiscoveryCache<LinearTeamDescriptor[]>();\n\tprivate async getTeams(\n\t\tsession: ProviderAuthenticationSession,\n\t\tforce: boolean = false,\n\t): Promise<LinearTeamDescriptor[] | undefined> {\n\t\treturn (await this.getTeamsWithMetadata(session, force))?.values;\n\t}\n\n\tprivate async getTeamsWithMetadata(\n\t\tsession: ProviderAuthenticationSession,\n\t\tforce: boolean = false,\n\t): Promise<ProviderApiCollectionResult<LinearTeamDescriptor> | undefined> {\n\t\tconst { accessToken } = session;\n\n\t\tconst cachedResources = this._teams.get(accessToken);\n\t\tif (cachedResources != null && !force) return { values: cachedResources };\n\n\t\tconst generation = this._teams.generation;\n\t\tconst api = await this.getProvidersApi();\n\t\tconst teams = await api.getLinearTeamsForCurrentUser(toTokenWithInfo(this.id, session));\n\t\tconst descriptors: LinearTeamDescriptor[] | undefined = teams?.map(t => ({\n\t\t\tid: t.id,\n\t\t\tkey: t.key,\n\t\t\tname: t.name,\n\t\t\tavatarUrl: t.iconUrl,\n\t\t}));\n\t\tif (descriptors == null) return undefined;\n\n\t\t// provider-apis currently requests Linear's teams connection without pageInfo or a cursor. Linear's\n\t\t// implicit page size is 50, so exactly a full page cannot prove there is no team 51. Preserve the useful\n\t\t// prefix, but mark it unknown and leave it uncached so the next read retries instead of treating it as\n\t\t// authoritative. Once provider-apis exposes a paged primitive, replace this fail-closed guard with a drain.\n\t\tif (descriptors.length >= linearImplicitTeamsPageSize) {\n\t\t\treturn { values: descriptors, metadata: { completeness: 'unknown' } };\n\t\t}\n\n\t\tif (this._teams.set(accessToken, descriptors, { generation: generation })) {\n\t\t\t// Built from the teams; a new set means new prefixes.\n\t\t\tthis._autolinks.delete(accessToken);\n\t\t}\n\t\treturn { values: descriptors };\n\t}\n\n\tprotected override async getProviderResourcesForUser(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<ResourceDescriptor[] | undefined> {\n\t\tconst organization = await this.getOrganization(session);\n\t\treturn organization != null ? [organization] : undefined;\n\t}\n\tprotected override getProviderProjectsForResources(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resources: ResourceDescriptor[],\n\t): Promise<ResourceDescriptor[] | undefined> {\n\t\treturn this.getTeams(session);\n\t}\n\tprotected override async getProviderProjectsForResourcesWithMetadata(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resources: ResourceDescriptor[],\n\t): Promise<ProviderApiCollectionResult<ResourceDescriptor>> {\n\t\treturn (await this.getTeamsWithMetadata(session)) ?? { values: [] };\n\t}\n\n\toverride invalidateDiscoveryCaches(): void {\n\t\tthis._organizations = undefined;\n\t\tthis._teams.clear();\n\t\tthis._autolinks.clear();\n\t}\n\treadonly authProvider: IntegrationAuthenticationProviderDescriptor = authProvider;\n\n\tprotected override async getProviderAccountForResource(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resource: ResourceDescriptor,\n\t): Promise<Account | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\t// Linear's viewer isn't a ProviderAccount (no username/avatar), so build the Account manually\n\t\t// (Trello-style) from the fields the viewer query returns.\n\t\tconst user = await api.getLinearCurrentUser(toTokenWithInfo(this.id, session));\n\t\tif (user == null) return undefined;\n\n\t\treturn {\n\t\t\tprovider: this,\n\t\t\tid: user.id,\n\t\t\tname: user.name ?? user.displayName ?? undefined,\n\t\t\tusername: user.displayName ?? undefined,\n\t\t\temail: user.email ?? undefined,\n\t\t\tavatarUrl: undefined,\n\t\t};\n\t}\n\n\tprotected override async getProviderIssuesForProject(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: ResourceDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<IssueShape[] | undefined> {\n\t\treturn (await this.getProviderIssuesForProjectWithTruncation(session, project, options))?.values;\n\t}\n\n\tprotected override async getProviderIssuesForProjectWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: ResourceDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<ProjectIssuesDrain | undefined> {\n\t\tif (!isIssueResourceDescriptor(project)) return undefined;\n\n\t\tconst api = await this.getProvidersApi();\n\t\t// `getProviderProjectsForResources` returns Linear teams, so `project.id` is a team id here. Drain the\n\t\t// team's issues (Linear pages by cursor); bounded by maxPagesPerRequest as a backstop. `truncated` is\n\t\t// set when that backstop stopped the drain with more pages still available.\n\t\tlet cursor: string | undefined;\n\t\tlet hasMore: boolean;\n\t\tlet requestCount = 0;\n\t\tlet truncated = false;\n\t\tlet collectionMetadata: CollectionMetadata | undefined;\n\t\tconst issues: IssueShape[] = [];\n\t\tdo {\n\t\t\tlet result: Awaited<ReturnType<typeof api.getLinearIssues>>;\n\t\t\ttry {\n\t\t\t\tresult = await api.getLinearIssues(\n\t\t\t\t\ttoTokenWithInfo(this.id, session),\n\t\t\t\t\t{ teams: [project.id] },\n\t\t\t\t\t{ cursor: cursor, sort: options?.sort },\n\t\t\t\t);\n\t\t\t} catch (ex) {\n\t\t\t\t// A page failure after the first page leaves the already-drained prefix intact; record the\n\t\t\t\t// failure at the project scope instead of re-throwing and discarding the prefix. If nothing was\n\t\t\t\t// fetched yet, preserve the original throw behavior so the caller sees a hard error.\n\t\t\t\tif (issues.length === 0) throw ex;\n\n\t\t\t\ttruncated = true;\n\t\t\t\tcollectionMetadata = mergeCollectionMetadata(collectionMetadata, {\n\t\t\t\t\tcompleteness: 'partial',\n\t\t\t\t\tfailures: [toCollectionScopeFailure({ providerId: this.id, projectId: project.id }, ex)],\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\trequestCount += 1;\n\t\t\thasMore = result.paging?.more ?? false;\n\t\t\tconst nextCursor = result.paging?.cursor;\n\t\t\ttruncated ||= result.paging?.truncated === true;\n\t\t\tcollectionMetadata = mergeCollectionMetadata(collectionMetadata, result.metadata);\n\t\t\tfor (const issue of result.values) {\n\t\t\t\tconst shape = toIssueShape(issue, this);\n\t\t\t\tif (shape != null) {\n\t\t\t\t\tissues.push(shape);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// The provider claims more but returns no advancing cursor: we can't continue without re-reading the\n\t\t\t// same page, so the drain is incomplete — flag it rather than silently stopping.\n\t\t\tif (hasMore && (nextCursor == null || nextCursor === cursor)) {\n\t\t\t\ttruncated = true;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcursor = nextCursor;\n\t\t\tif (hasMore && requestCount >= maxPagesPerRequest) {\n\t\t\t\ttruncated = true;\n\t\t\t}\n\t\t} while (requestCount < maxPagesPerRequest && hasMore);\n\n\t\t// Linear's issue list has no server-side author/assignee filter, so scope to the current user\n\t\t// client-side when a user was requested (the assignee filter is what \"my issues\" means here).\n\t\t// Match on the viewer's stable id, not the passed display name: Linear's `name` (full name) and\n\t\t// `displayName` (nickname) are distinct fields, and assignees are normalized with `.name` = the full\n\t\t// name while the caller's `user` is the displayName — so a name string can miss. The assignee `.id`\n\t\t// is the Linear user id, which is unambiguous.\n\t\tif (options?.user != null) {\n\t\t\tconst viewerId = (await api.getLinearCurrentUser(toTokenWithInfo(this.id, session)))?.id;\n\t\t\t// If the viewer can't be resolved we can't scope to \"my issues\" — returning the unfiltered team\n\t\t\t// issues would leak everyone else's, and returning [] is indistinguishable from \"no issues assigned\n\t\t\t// to me\". Throw so the facade (getIssuesForProjectResult → runCaptured) surfaces a warning +\n\t\t\t// fetchFailed the caller can act on, instead of a silent empty.\n\t\t\tif (viewerId == null) {\n\t\t\t\tthrow new IntegrationReadUnavailableError(\n\t\t\t\t\tmetadata.name,\n\t\t\t\t\t'could not resolve the current user to scope issues to',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst values = issues.filter(issue => issue.assignees?.some(a => a.id === viewerId));\n\t\t\treturn truncated\n\t\t\t\t? { values: values, truncated: true, recovery: 'none', metadata: collectionMetadata }\n\t\t\t\t: { values: values, truncated: false, metadata: collectionMetadata };\n\t\t}\n\n\t\treturn truncated\n\t\t\t? { values: issues, truncated: true, recovery: 'none', metadata: collectionMetadata }\n\t\t\t: { values: issues, truncated: false, metadata: collectionMetadata };\n\t}\n\n\toverride get id(): IssuesCloudHostIntegrationId.Linear {\n\t\treturn IssuesCloudHostIntegrationId.Linear;\n\t}\n\tprotected override get key(): 'linear' {\n\t\treturn 'linear';\n\t}\n\toverride get name(): string {\n\t\treturn metadata.name;\n\t}\n\toverride get domain(): string {\n\t\treturn metadata.domain;\n\t}\n\tprotected override async searchProviderMyIssues(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources?: ResourceDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t): Promise<IssueShape[] | undefined> {\n\t\treturn (await this.searchProviderMyIssuesWithTruncation(session, resources, cancellation))?.values;\n\t}\n\n\t/**\n\t * Account-wide \"my issues\" for Linear, drained to exhaustion and reporting whether it got there.\n\t *\n\t * The SDK read is one server-ordered query over an `or` filter across the four relationships (assigned,\n\t * created, named in an issue's content, named in a comment), so each page is 100 issues the caller has not\n\t * seen and the pages are ordered as one sequence. Draining it is therefore just following the cursor.\n\t *\n\t * Overridden rather than left to the base default because that default hardcodes `truncated: false`, and a\n\t * bounded drain that stops early is exactly the case a caller must be able to see. Three things end the\n\t * drain, and only one of them is completion:\n\t *\n\t * - the provider says there is no next page — complete;\n\t * - the backstop is reached — `truncated`, because issues beyond it exist and were not read;\n\t * - the provider claims a next page but hands back a cursor that does not advance — also `truncated`, since\n\t * the read cannot continue and the remainder is unreachable rather than absent.\n\t *\n\t * Cancellation is deliberately NOT truncation: the caller asked for the read to stop, so the partial list is\n\t * what it asked for, and flagging it would surface an incompleteness warning for a user action.\n\t */\n\tprotected override async searchProviderMyIssuesWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources?: ResourceDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t\toptions?: SearchMyIssuesOptions,\n\t): Promise<AccountWideIssuesResult | undefined> {\n\t\tif (resources != null) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst api = await this.getProvidersApi();\n\t\tlet cursor = undefined;\n\t\t// Starts false so an immediate cancellation, which leaves the loop before the first response, reads as\n\t\t// \"no more pages known\" rather than as an unfinished drain.\n\t\tlet hasMore = false;\n\t\tlet requestCount = 0;\n\t\tlet truncated = false;\n\t\tconst issues = [];\n\t\ttry {\n\t\t\tdo {\n\t\t\t\tif (cancellation?.aborted) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tconst result = await api.getIssuesForCurrentUser(toTokenWithInfo(this.id, session), {\n\t\t\t\t\tcursor: cursor,\n\t\t\t\t\tsort: options?.sort,\n\t\t\t\t});\n\t\t\t\trequestCount += 1;\n\t\t\t\thasMore = result.paging?.more ?? false;\n\t\t\t\tconst nextCursor = result.paging?.cursor;\n\n\t\t\t\t// Keep this page before deciding whether to continue: the request is already paid for, so\n\t\t\t\t// dropping its rows would lose real results to save nothing.\n\t\t\t\tconst formattedIssues = result.values\n\t\t\t\t\t.map(issue => toIssueShape(issue, this))\n\t\t\t\t\t.filter((result): result is IssueShape => result != null);\n\t\t\t\tif (formattedIssues.length > 0) {\n\t\t\t\t\tissues.push(...formattedIssues);\n\t\t\t\t}\n\n\t\t\t\t// The provider claims more but hands back no advancing cursor: continuing would re-read the page\n\t\t\t\t// just returned. `ProvidersApi.getPagedResult` already normalizes this into `more: false`, so\n\t\t\t\t// this is a backstop for if that ever stops holding rather than a case seen today -- but the rest\n\t\t\t\t// of the account is unreachable either way, which is what `truncated` says.\n\t\t\t\tif (hasMore && (nextCursor == null || nextCursor === cursor)) {\n\t\t\t\t\ttruncated = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tcursor = nextCursor;\n\t\t\t} while (requestCount < maxAccountWidePagesPerRequest && hasMore);\n\n\t\t\t// Ran out of budget with pages still to come.\n\t\t\tif (hasMore && requestCount >= maxAccountWidePagesPerRequest) {\n\t\t\t\ttruncated = true;\n\t\t\t}\n\t\t} catch (ex) {\n\t\t\tif (issues.length === 0) {\n\t\t\t\tthrow ex;\n\t\t\t}\n\n\t\t\t// Kept what was already fetched, so the list is real but short of the account.\n\t\t\ttruncated = true;\n\t\t\tLogger.error(ex, 'searchProviderMyIssues');\n\t\t}\n\t\treturn { values: issues, truncated: truncated };\n\t}\n\tprotected override async getProviderLinkedIssueOrPullRequest(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: ResourceDescriptor,\n\t\t{ key }: { id: string; key: string },\n\t\t_type: undefined | IssueOrPullRequestType,\n\t): Promise<IssueOrPullRequest | undefined> {\n\t\tconst issue = await this.getRawProviderIssue(session, resource, key);\n\t\tconst autolinkableIssue: ProviderIssue | undefined = issue && {\n\t\t\t...issue,\n\t\t\turl: this.getIssueAutolinkLikeUrl(issue),\n\t\t};\n\t\treturn autolinkableIssue && toIssueShape(autolinkableIssue, this);\n\t}\n\tprotected override async getProviderIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: ResourceDescriptor,\n\t\tid: string,\n\t): Promise<Issue | undefined> {\n\t\tconst result = await this.getRawProviderIssue(session, resource, id);\n\t\treturn result && fromProviderIssue(result, this);\n\t}\n\n\tprotected override async getProviderIssueByResourceId(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresourceId: string,\n\t\tid: string,\n\t\t_resourceUrl: string | undefined,\n\t): Promise<Issue | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst result = await api.getIssue(toTokenWithInfo(this.id, session), {\n\t\t\tresourceId: resourceId,\n\t\t\tnumber: id,\n\t\t});\n\t\treturn result && fromProviderIssue(result, this);\n\t}\n\n\tprivate async getRawProviderIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: ResourceDescriptor,\n\t\tid: string,\n\t): Promise<ProviderIssue | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tif (!isIssueResourceDescriptor(resource)) {\n\t\t\tLogger.error(undefined, 'getProviderIssue: resource is not an IssueResourceDescriptor');\n\t\t\treturn undefined;\n\t\t}\n\n\t\treturn api.getIssue(toTokenWithInfo(this.id, session), {\n\t\t\tresourceId: resource.id,\n\t\t\tnumber: id,\n\t\t});\n\t}\n\tprivate getIssueAutolinkLikeUrl(issue: ProviderIssue): string | null {\n\t\tconst url = issue.url;\n\t\tif (url == null) return null;\n\n\t\tconst lastSegment = url.split('/').pop();\n\t\tif (!lastSegment || issue.number === lastSegment) {\n\t\t\treturn url;\n\t\t}\n\t\treturn url.substring(0, url.length - lastSegment.length - 1);\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"linear.js","names":[],"sources":["../../../../../plus/integrations/src/providers/linear.ts"],"sourcesContent":["import type { CollectionMetadata } from '@gitkraken/provider-apis';\nimport * as l10n from '@vscode/l10n';\nimport type { Account } from '@gitlens/git/models/author.js';\nimport type { AutolinkReference, DynamicAutolinkReference } from '@gitlens/git/models/autolink.js';\nimport type { Issue, IssueShape } from '@gitlens/git/models/issue.js';\nimport type { IssueOrPullRequest, IssueOrPullRequestType } from '@gitlens/git/models/issueOrPullRequest.js';\nimport type { IssueResourceDescriptor, ResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport { isIssueResourceDescriptor } from '@gitlens/git/utils/resourceDescriptor.utils.js';\nimport { chunk } from '@gitlens/utils/array.js';\nimport { Logger } from '@gitlens/utils/logger.js';\nimport { mapSettledBounded } from '@gitlens/utils/promise.js';\nimport type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider.js';\nimport type { ProviderAuthenticationSession } from '../authentication/models.js';\nimport { toTokenWithInfo } from '../authentication/models.js';\nimport { toCollectionScopeFailure } from '../collectionMetadata.js';\nimport { IssuesCloudHostIntegrationId, providerFanOutConcurrency } from '../constants.js';\nimport { IntegrationReadUnavailableError } from '../errors.js';\nimport type { IssueEtagFields } from '../models/integration.js';\nimport type {\n\tAccountWideIssuesResult,\n\tIssuesForProjectOptions,\n\tProjectIssuesDrain,\n\tSearchMyIssuesOptions,\n} from '../models/issueReads.js';\nimport { IssuesIntegration } from '../models/issuesIntegration.js';\nimport type { LinearIssueEtagNode } from './linearIssuesEtag.js';\nimport { linearIssuesEtagMaxNumbers, toLinearIssueEtagFields } from './linearIssuesEtag.js';\nimport type { ProviderApiCollectionResult, ProviderIssue } from './models.js';\nimport { fromProviderIssue, providersMetadata, toIssueShape, toProviderIssueStates } from './models.js';\nimport { DiscoveryCache, discoveryCacheTtl } from './utils/discoveryCache.js';\nimport { mergeCollectionMetadata } from './utils/providerPaging.js';\n\nconst metadata = providersMetadata[IssuesCloudHostIntegrationId.Linear];\nconst authProvider = Object.freeze({ id: metadata.id, scopes: metadata.scopes });\nconst maxPagesPerRequest = 10;\n/**\n * The account-wide drain's own backstop, separate from {@link maxPagesPerRequest}.\n *\n * Raised to 50 (5,000 issues at the SDK's 100-per-page) because 10 no longer means what it used to. That read\n * once fanned out over four overlapping relationship queries and merged them, so most of what a page cost was\n * rows already returned — a low budget capped the waste. It is now a single server-ordered query over an `or`\n * filter, so every page is 100 issues the caller has not seen, and the same budget just truncates real results\n * at 1,000.\n *\n * Reaching it is reported rather than silent: {@link LinearIntegration.searchProviderMyIssuesWithTruncation}\n * returns `truncated`, so the facade can say the read was incomplete instead of publishing a capped list as a\n * whole account. The bound stays — a runaway cursor must not spend requests forever — but it is now set where a\n * real Linear account is unlikely to reach it rather than where the duplication used to hurt.\n */\nconst maxAccountWidePagesPerRequest = 50;\nconst linearImplicitTeamsPageSize = 50;\n/** A team key and issue number, as Linear writes an issue's identifier (`ENG-123`). */\nconst linearIdentifier = /^([A-Za-z0-9]+)-(\\d+)$/;\n\nexport interface LinearTeamDescriptor extends IssueResourceDescriptor {\n\tavatarUrl: string | undefined;\n}\n\nexport interface LinearOrganizationDescriptor extends IssueResourceDescriptor {\n\turl: string;\n}\n\nexport interface LinearProjectDescriptor extends IssueResourceDescriptor {}\n\nexport class LinearIntegration extends IssuesIntegration<IssuesCloudHostIntegrationId.Linear> {\n\t/**\n\t * Built from the teams, so it follows them: rebuilt when the team list changes or the caches are dropped, and once\n\t * older than {@link discoveryCacheTtl}. An expired set is still served while it is rebuilt in the background, because\n\t * autolinks are read on every render of a commit message and must not wait on the network (#5907).\n\t */\n\tprivate readonly _autolinks = new Map<\n\t\tstring,\n\t\t{ autolinks: (AutolinkReference | DynamicAutolinkReference)[]; builtAt: number }\n\t>();\n\tprivate readonly _autolinksBuilds = new Map<string, Promise<(AutolinkReference | DynamicAutolinkReference)[]>>();\n\n\toverride async autolinks(): Promise<(AutolinkReference | DynamicAutolinkReference)[]> {\n\t\tconst connected = this.maybeConnected ?? (await this.isConnected());\n\t\tif (!connected || this._session == null) {\n\t\t\treturn [];\n\t\t}\n\n\t\tconst session = this._session;\n\t\tconst cached = this._autolinks.get(session.accessToken);\n\t\tif (cached == null) return this.buildAutolinks(session);\n\n\t\tif (Date.now() - cached.builtAt >= discoveryCacheTtl) {\n\t\t\tvoid this.buildAutolinks(session).catch(() => {});\n\t\t}\n\t\treturn cached.autolinks;\n\t}\n\n\t/** One build per token at a time: concurrent renders share it rather than each re-reading the teams. */\n\tprivate buildAutolinks(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<(AutolinkReference | DynamicAutolinkReference)[]> {\n\t\tconst { accessToken } = session;\n\t\tlet build = this._autolinksBuilds.get(accessToken);\n\t\tif (build == null) {\n\t\t\tbuild = this.buildAutolinksCore(session).finally(() => this._autolinksBuilds.delete(accessToken));\n\t\t\tthis._autolinksBuilds.set(accessToken, build);\n\t\t}\n\t\treturn build;\n\t}\n\n\tprivate async buildAutolinksCore(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<(AutolinkReference | DynamicAutolinkReference)[]> {\n\t\tconst generation = this._teams.generation;\n\t\tconst organization = await this.getOrganization(session);\n\t\tif (organization == null) return [];\n\n\t\tconst autolinks: (AutolinkReference | DynamicAutolinkReference)[] = [];\n\n\t\tconst teams = await this.getTeams(session);\n\t\tfor (const team of teams ?? []) {\n\t\t\tconst dashedPrefix = `${team.key}-`;\n\t\t\tconst underscoredPrefix = `${team.key}_`;\n\n\t\t\tautolinks.push({\n\t\t\t\tprefix: dashedPrefix,\n\t\t\t\turl: `${organization.url}/issue/${dashedPrefix}<num>`,\n\t\t\t\talphanumeric: false,\n\t\t\t\tignoreCase: false,\n\t\t\t\ttitle: l10n.t('Open Issue {0} on {1}', `${dashedPrefix}<num>`, organization.name),\n\n\t\t\t\ttype: 'issue',\n\t\t\t\tdescription: l10n.t('{0} Issue {1}', organization.name, `${dashedPrefix}<num>`),\n\t\t\t\tdescriptor: { ...organization },\n\t\t\t});\n\t\t\tautolinks.push({\n\t\t\t\tprefix: underscoredPrefix,\n\t\t\t\turl: `${organization.url}/issue/${dashedPrefix}<num>`,\n\t\t\t\talphanumeric: false,\n\t\t\t\tignoreCase: false,\n\t\t\t\treferenceType: 'branch',\n\t\t\t\ttitle: l10n.t('Open Issue {0} on {1}', `${dashedPrefix}<num>`, organization.name),\n\n\t\t\t\ttype: 'issue',\n\t\t\t\tdescription: l10n.t('{0} Issue {1}', organization.name, `${dashedPrefix}<num>`),\n\t\t\t\tdescriptor: { ...organization },\n\t\t\t});\n\t\t}\n\n\t\t// Not kept when built from teams read before the caches were dropped.\n\t\tif (this._teams.generation === generation) {\n\t\t\tthis._autolinks.set(session.accessToken, { autolinks: autolinks, builtAt: Date.now() });\n\t\t}\n\t\treturn autolinks;\n\t}\n\n\tprivate _organizations: Map<string, LinearOrganizationDescriptor | undefined> | undefined;\n\tprivate async getOrganization(\n\t\tsession: ProviderAuthenticationSession,\n\t\tforce: boolean = false,\n\t): Promise<LinearOrganizationDescriptor | undefined> {\n\t\tconst { accessToken } = session;\n\t\tthis._organizations ??= new Map<string, LinearOrganizationDescriptor | undefined>();\n\n\t\tconst cachedResources = this._organizations.get(accessToken);\n\n\t\tif (cachedResources == null || force) {\n\t\t\tconst api = await this.getProvidersApi();\n\t\t\tconst organization = await api.getLinearOrganization(toTokenWithInfo(this.id, session));\n\t\t\tconst descriptor: LinearOrganizationDescriptor | undefined = organization && {\n\t\t\t\tid: organization.id,\n\t\t\t\tkey: organization.key,\n\t\t\t\tname: organization.name,\n\t\t\t\turl: organization.url,\n\t\t\t};\n\t\t\tif (descriptor) {\n\t\t\t\tthis._organizations.set(accessToken, descriptor);\n\t\t\t}\n\t\t}\n\n\t\treturn this._organizations.get(accessToken);\n\t}\n\n\t/** Expires, and is dropped on a re-sync, so a team created or left mid-session is picked up (#5907). */\n\tprivate readonly _teams = new DiscoveryCache<LinearTeamDescriptor[]>();\n\tprivate async getTeams(\n\t\tsession: ProviderAuthenticationSession,\n\t\tforce: boolean = false,\n\t): Promise<LinearTeamDescriptor[] | undefined> {\n\t\treturn (await this.getTeamsWithMetadata(session, force))?.values;\n\t}\n\n\tprivate async getTeamsWithMetadata(\n\t\tsession: ProviderAuthenticationSession,\n\t\tforce: boolean = false,\n\t): Promise<ProviderApiCollectionResult<LinearTeamDescriptor> | undefined> {\n\t\tconst { accessToken } = session;\n\n\t\tconst cachedResources = this._teams.get(accessToken);\n\t\tif (cachedResources != null && !force) return { values: cachedResources };\n\n\t\tconst generation = this._teams.generation;\n\t\tconst api = await this.getProvidersApi();\n\t\tconst teams = await api.getLinearTeamsForCurrentUser(toTokenWithInfo(this.id, session));\n\t\tconst descriptors: LinearTeamDescriptor[] | undefined = teams?.map(t => ({\n\t\t\tid: t.id,\n\t\t\tkey: t.key,\n\t\t\tname: t.name,\n\t\t\tavatarUrl: t.iconUrl,\n\t\t}));\n\t\tif (descriptors == null) return undefined;\n\n\t\t// provider-apis currently requests Linear's teams connection without pageInfo or a cursor. Linear's\n\t\t// implicit page size is 50, so exactly a full page cannot prove there is no team 51. Preserve the useful\n\t\t// prefix, but mark it unknown and leave it uncached so the next read retries instead of treating it as\n\t\t// authoritative. Once provider-apis exposes a paged primitive, replace this fail-closed guard with a drain.\n\t\tif (descriptors.length >= linearImplicitTeamsPageSize) {\n\t\t\treturn { values: descriptors, metadata: { completeness: 'unknown' } };\n\t\t}\n\n\t\tif (this._teams.set(accessToken, descriptors, { generation: generation })) {\n\t\t\t// Built from the teams; a new set means new prefixes.\n\t\t\tthis._autolinks.delete(accessToken);\n\t\t}\n\t\treturn { values: descriptors };\n\t}\n\n\tprotected override async getProviderResourcesForUser(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<ResourceDescriptor[] | undefined> {\n\t\tconst organization = await this.getOrganization(session);\n\t\treturn organization != null ? [organization] : undefined;\n\t}\n\tprotected override getProviderProjectsForResources(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resources: ResourceDescriptor[],\n\t): Promise<ResourceDescriptor[] | undefined> {\n\t\treturn this.getTeams(session);\n\t}\n\tprotected override async getProviderProjectsForResourcesWithMetadata(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resources: ResourceDescriptor[],\n\t): Promise<ProviderApiCollectionResult<ResourceDescriptor>> {\n\t\treturn (await this.getTeamsWithMetadata(session)) ?? { values: [] };\n\t}\n\n\toverride invalidateDiscoveryCaches(): void {\n\t\tthis._organizations = undefined;\n\t\tthis._teams.clear();\n\t\tthis._autolinks.clear();\n\t}\n\treadonly authProvider: IntegrationAuthenticationProviderDescriptor = authProvider;\n\n\tprotected override async getProviderAccountForResource(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resource: ResourceDescriptor,\n\t): Promise<Account | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\t// Linear's viewer isn't a ProviderAccount (no username/avatar), so build the Account manually\n\t\t// (Trello-style) from the fields the viewer query returns.\n\t\tconst user = await api.getLinearCurrentUser(toTokenWithInfo(this.id, session));\n\t\tif (user == null) return undefined;\n\n\t\treturn {\n\t\t\tprovider: this,\n\t\t\tid: user.id,\n\t\t\tname: user.name ?? user.displayName ?? undefined,\n\t\t\tusername: user.displayName ?? undefined,\n\t\t\temail: user.email ?? undefined,\n\t\t\tavatarUrl: undefined,\n\t\t};\n\t}\n\n\tprotected override async getProviderIssuesForProject(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: ResourceDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<IssueShape[] | undefined> {\n\t\treturn (await this.getProviderIssuesForProjectWithTruncation(session, project, options))?.values;\n\t}\n\n\tprotected override async getProviderIssuesForProjectWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: ResourceDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<ProjectIssuesDrain | undefined> {\n\t\tif (!isIssueResourceDescriptor(project)) return undefined;\n\n\t\tconst api = await this.getProvidersApi();\n\t\t// `getProviderProjectsForResources` returns Linear teams, so `project.id` is a team id here. Drain the\n\t\t// team's issues (Linear pages by cursor); bounded by maxPagesPerRequest as a backstop. `truncated` is\n\t\t// set when that backstop stopped the drain with more pages still available.\n\t\t//\n\t\t// The state is narrowed server-side, not after the drain: a team's completed and canceled issues would\n\t\t// otherwise count against the backstop, so a team with enough done work runs out of pages before its open\n\t\t// issues, and the viewer filter below would then hand back a page that looks complete.\n\t\tconst states = toProviderIssueStates(options?.state);\n\t\tlet cursor: string | undefined;\n\t\tlet hasMore: boolean;\n\t\tlet requestCount = 0;\n\t\tlet truncated = false;\n\t\tlet collectionMetadata: CollectionMetadata | undefined;\n\t\tconst issues: IssueShape[] = [];\n\t\tdo {\n\t\t\tlet result: Awaited<ReturnType<typeof api.getLinearIssues>>;\n\t\t\ttry {\n\t\t\t\tresult = await api.getLinearIssues(\n\t\t\t\t\ttoTokenWithInfo(this.id, session),\n\t\t\t\t\t{ teams: [project.id], states: states },\n\t\t\t\t\t{ cursor: cursor, sort: options?.sort },\n\t\t\t\t);\n\t\t\t} catch (ex) {\n\t\t\t\t// A page failure after the first page leaves the already-drained prefix intact; record the\n\t\t\t\t// failure at the project scope instead of re-throwing and discarding the prefix. If nothing was\n\t\t\t\t// fetched yet, preserve the original throw behavior so the caller sees a hard error.\n\t\t\t\tif (issues.length === 0) throw ex;\n\n\t\t\t\ttruncated = true;\n\t\t\t\tcollectionMetadata = mergeCollectionMetadata(collectionMetadata, {\n\t\t\t\t\tcompleteness: 'partial',\n\t\t\t\t\tfailures: [toCollectionScopeFailure({ providerId: this.id, projectId: project.id }, ex)],\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\trequestCount += 1;\n\t\t\thasMore = result.paging?.more ?? false;\n\t\t\tconst nextCursor = result.paging?.cursor;\n\t\t\ttruncated ||= result.paging?.truncated === true;\n\t\t\tcollectionMetadata = mergeCollectionMetadata(collectionMetadata, result.metadata);\n\t\t\tfor (const issue of result.values) {\n\t\t\t\tconst shape = toIssueShape(issue, this, { projection: 'project' });\n\t\t\t\tif (shape != null) {\n\t\t\t\t\tissues.push(shape);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// The provider claims more but returns no advancing cursor: we can't continue without re-reading the\n\t\t\t// same page, so the drain is incomplete — flag it rather than silently stopping.\n\t\t\tif (hasMore && (nextCursor == null || nextCursor === cursor)) {\n\t\t\t\ttruncated = true;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcursor = nextCursor;\n\t\t\tif (hasMore && requestCount >= maxPagesPerRequest) {\n\t\t\t\ttruncated = true;\n\t\t\t}\n\t\t} while (requestCount < maxPagesPerRequest && hasMore);\n\n\t\t// Linear's issue list has no server-side author/assignee filter, so scope to the current user\n\t\t// client-side when a user was requested (the assignee filter is what \"my issues\" means here).\n\t\t// Match on the viewer's stable id, not the passed display name: Linear's `name` (full name) and\n\t\t// `displayName` (nickname) are distinct fields, and assignees are normalized with `.name` = the full\n\t\t// name while the caller's `user` is the displayName — so a name string can miss. The assignee `.id`\n\t\t// is the Linear user id, which is unambiguous.\n\t\tif (options?.user != null) {\n\t\t\tconst viewerId = (await api.getLinearCurrentUser(toTokenWithInfo(this.id, session)))?.id;\n\t\t\t// If the viewer can't be resolved we can't scope to \"my issues\" — returning the unfiltered team\n\t\t\t// issues would leak everyone else's, and returning [] is indistinguishable from \"no issues assigned\n\t\t\t// to me\". Throw so the facade (getIssuesForProjectResult → runCaptured) surfaces a warning +\n\t\t\t// fetchFailed the caller can act on, instead of a silent empty.\n\t\t\tif (viewerId == null) {\n\t\t\t\tthrow new IntegrationReadUnavailableError(\n\t\t\t\t\tmetadata.name,\n\t\t\t\t\t'could not resolve the current user to scope issues to',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst values = issues.filter(issue => issue.assignees?.some(a => a.id === viewerId));\n\t\t\treturn truncated\n\t\t\t\t? { values: values, truncated: true, recovery: 'none', metadata: collectionMetadata }\n\t\t\t\t: { values: values, truncated: false, metadata: collectionMetadata };\n\t\t}\n\n\t\treturn truncated\n\t\t\t? { values: issues, truncated: true, recovery: 'none', metadata: collectionMetadata }\n\t\t\t: { values: issues, truncated: false, metadata: collectionMetadata };\n\t}\n\n\toverride get id(): IssuesCloudHostIntegrationId.Linear {\n\t\treturn IssuesCloudHostIntegrationId.Linear;\n\t}\n\tprotected override get key(): 'linear' {\n\t\treturn 'linear';\n\t}\n\toverride get name(): string {\n\t\treturn metadata.name;\n\t}\n\toverride get domain(): string {\n\t\treturn metadata.domain;\n\t}\n\tprotected override async searchProviderMyIssues(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources?: ResourceDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t): Promise<IssueShape[] | undefined> {\n\t\treturn (await this.searchProviderMyIssuesWithTruncation(session, resources, cancellation))?.values;\n\t}\n\n\t/**\n\t * Account-wide \"my issues\" for Linear, drained to exhaustion and reporting whether it got there.\n\t *\n\t * The SDK read is one server-ordered query over an `or` filter across the four relationships (assigned,\n\t * created, named in an issue's content, named in a comment), so each page is 100 issues the caller has not\n\t * seen and the pages are ordered as one sequence. Draining it is therefore just following the cursor.\n\t *\n\t * Overridden rather than left to the base default because that default hardcodes `truncated: false`, and a\n\t * bounded drain that stops early is exactly the case a caller must be able to see. Three things end the\n\t * drain, and only one of them is completion:\n\t *\n\t * - the provider says there is no next page — complete;\n\t * - the backstop is reached — `truncated`, because issues beyond it exist and were not read;\n\t * - the provider claims a next page but hands back a cursor that does not advance — also `truncated`, since\n\t * the read cannot continue and the remainder is unreachable rather than absent.\n\t *\n\t * Cancellation is deliberately NOT truncation: the caller asked for the read to stop, so the partial list is\n\t * what it asked for, and flagging it would surface an incompleteness warning for a user action.\n\t */\n\tprotected override async searchProviderMyIssuesWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources?: ResourceDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t\toptions?: SearchMyIssuesOptions,\n\t): Promise<AccountWideIssuesResult | undefined> {\n\t\tif (resources != null) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst states = toProviderIssueStates(options?.state);\n\t\tlet cursor = undefined;\n\t\t// Starts false so an immediate cancellation, which leaves the loop before the first response, reads as\n\t\t// \"no more pages known\" rather than as an unfinished drain.\n\t\tlet hasMore = false;\n\t\tlet requestCount = 0;\n\t\tlet truncated = false;\n\t\tconst issues = [];\n\t\ttry {\n\t\t\tdo {\n\t\t\t\tif (cancellation?.aborted) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tconst result = await api.getIssuesForCurrentUser(toTokenWithInfo(this.id, session), {\n\t\t\t\t\tcursor: cursor,\n\t\t\t\t\tstates: states,\n\t\t\t\t\tsort: options?.sort,\n\t\t\t\t});\n\t\t\t\trequestCount += 1;\n\t\t\t\thasMore = result.paging?.more ?? false;\n\t\t\t\tconst nextCursor = result.paging?.cursor;\n\n\t\t\t\t// Keep this page before deciding whether to continue: the request is already paid for, so\n\t\t\t\t// dropping its rows would lose real results to save nothing.\n\t\t\t\tconst formattedIssues = result.values\n\t\t\t\t\t.map(issue => toIssueShape(issue, this, { projection: 'account' }))\n\t\t\t\t\t.filter((result): result is IssueShape => result != null);\n\t\t\t\tif (formattedIssues.length > 0) {\n\t\t\t\t\tissues.push(...formattedIssues);\n\t\t\t\t}\n\n\t\t\t\t// The provider claims more but hands back no advancing cursor: continuing would re-read the page\n\t\t\t\t// just returned. `ProvidersApi.getPagedResult` already normalizes this into `more: false`, so\n\t\t\t\t// this is a backstop for if that ever stops holding rather than a case seen today -- but the rest\n\t\t\t\t// of the account is unreachable either way, which is what `truncated` says.\n\t\t\t\tif (hasMore && (nextCursor == null || nextCursor === cursor)) {\n\t\t\t\t\ttruncated = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tcursor = nextCursor;\n\t\t\t} while (requestCount < maxAccountWidePagesPerRequest && hasMore);\n\n\t\t\t// Ran out of budget with pages still to come.\n\t\t\tif (hasMore && requestCount >= maxAccountWidePagesPerRequest) {\n\t\t\t\ttruncated = true;\n\t\t\t}\n\t\t} catch (ex) {\n\t\t\tif (issues.length === 0) {\n\t\t\t\tthrow ex;\n\t\t\t}\n\n\t\t\t// Kept what was already fetched, so the list is real but short of the account.\n\t\t\ttruncated = true;\n\t\t\tLogger.error(ex, 'searchProviderMyIssues');\n\t\t}\n\t\treturn { values: issues, truncated: truncated };\n\t}\n\tprotected override async getProviderLinkedIssueOrPullRequest(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: ResourceDescriptor,\n\t\t{ key }: { id: string; key: string },\n\t\t_type: undefined | IssueOrPullRequestType,\n\t): Promise<IssueOrPullRequest | undefined> {\n\t\tconst issue = await this.getRawProviderIssue(session, resource, key);\n\t\tconst autolinkableIssue: ProviderIssue | undefined = issue && {\n\t\t\t...issue,\n\t\t\turl: this.getIssueAutolinkLikeUrl(issue),\n\t\t};\n\t\treturn autolinkableIssue && toIssueShape(autolinkableIssue, this, { projection: 'point' });\n\t}\n\tprotected override async getProviderIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: ResourceDescriptor,\n\t\tid: string,\n\t): Promise<Issue | undefined> {\n\t\tconst result = await this.getRawProviderIssue(session, resource, id);\n\t\treturn result && fromProviderIssue(result, this, { projection: 'point' });\n\t}\n\n\tprotected override async getProviderIssueByResourceId(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresourceId: string,\n\t\tid: string,\n\t\t_resourceUrl: string | undefined,\n\t): Promise<Issue | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst result = await api.getIssue(toTokenWithInfo(this.id, session), {\n\t\t\tresourceId: resourceId,\n\t\t\tnumber: id,\n\t\t});\n\t\treturn result && fromProviderIssue(result, this, { projection: 'batch' });\n\t}\n\n\t/**\n\t * The cheap check behind the batch issue read's etags: one `issues` query per team and\n\t * {@link linearIssuesEtagMaxNumbers} distinct numbers, where {@link getProviderIssueByResourceId} sends one request\n\t * per issue. A request that throws rejects only its own targets' slots, with its error classified as the full\n\t * read's would be.\n\t *\n\t * Never proves an absence. The query filters by the team's CURRENT key and asks for archived issues too, but the\n\t * full read's `issue(id:)` may also resolve an identifier the issue no longer carries (its team was renamed, or it\n\t * moved to another team), so an issue the query leaves out may still exist. Its target's slot is rejected, as is a\n\t * target whose identifier isn't a team key and a number, so the full read decides. When no target was answered and\n\t * nothing failed, the check declines instead, so a batch of only such targets costs a full read, not a failure.\n\t */\n\tprotected override async getProviderIssuesEtagFieldsByResourceId(\n\t\tsession: ProviderAuthenticationSession,\n\t\ttargets: readonly { resourceId: string; identifier: string; resourceUrl?: string }[],\n\t): Promise<PromiseSettledResult<IssueEtagFields | undefined>[] | undefined> {\n\t\tconst slots = new Array<PromiseSettledResult<IssueEtagFields | undefined>>(targets.length);\n\t\tlet answered = false;\n\t\tlet failed = false;\n\n\t\t// Each workspace and team's distinct numbers, with the targets asking for each.\n\t\tconst byTeam = new Map<string, { teamKey: string; numbers: Map<number, number[]> }>();\n\t\tfor (const [index, target] of targets.entries()) {\n\t\t\tconst match = linearIdentifier.exec(target.identifier);\n\t\t\tconst number = match != null ? Number(match[2]) : Number.NaN;\n\t\t\t// Only a number written as Linear writes it, so `ENG-012` never answers for `ENG-12`.\n\t\t\tif (match == null || !Number.isSafeInteger(number) || String(number) !== match[2]) {\n\t\t\t\tslots[index] = {\n\t\t\t\t\tstatus: 'rejected',\n\t\t\t\t\treason: new Error(`Not a Linear issue identifier: ${target.identifier}`),\n\t\t\t\t};\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst teamKey = match[1].toUpperCase();\n\t\t\tconst groupKey = `${target.resourceId}\\n${teamKey}`;\n\t\t\tlet group = byTeam.get(groupKey);\n\t\t\tif (group == null) {\n\t\t\t\tgroup = { teamKey: teamKey, numbers: new Map() };\n\t\t\t\tbyTeam.set(groupKey, group);\n\t\t\t}\n\n\t\t\tlet indices = group.numbers.get(number);\n\t\t\tif (indices == null) {\n\t\t\t\tindices = [];\n\t\t\t\tgroup.numbers.set(number, indices);\n\t\t\t}\n\t\t\tindices.push(index);\n\t\t}\n\n\t\tconst requests = [...byTeam.values()].flatMap(({ teamKey, numbers }) =>\n\t\t\tchunk([...numbers], linearIssuesEtagMaxNumbers).map(entries => ({ teamKey: teamKey, entries: entries })),\n\t\t);\n\t\tif (requests.length) {\n\t\t\tconst api = await this.getProvidersApi();\n\t\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\t\tconst answers = await mapSettledBounded(requests, providerFanOutConcurrency, r =>\n\t\t\t\tapi.getLinearIssuesEtagFields(\n\t\t\t\t\ttokenWithInfo,\n\t\t\t\t\tr.teamKey,\n\t\t\t\t\tr.entries.map(([number]) => number),\n\t\t\t\t),\n\t\t\t);\n\n\t\t\tfor (const [i, answer] of answers.entries()) {\n\t\t\t\tconst { teamKey, entries } = requests[i];\n\t\t\t\tif (answer.status === 'rejected') {\n\t\t\t\t\tfailed = true;\n\t\t\t\t\tfor (const [, indices] of entries) {\n\t\t\t\t\t\tfor (const index of indices) {\n\t\t\t\t\t\t\tslots[index] = answer;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// By number, never by position: Linear returns the issues in its own order, and only the ones it found.\n\t\t\t\tconst byNumber = new Map<number, LinearIssueEtagNode>();\n\t\t\t\tfor (const issue of answer.value) {\n\t\t\t\t\tbyNumber.set(issue.number, issue);\n\t\t\t\t}\n\n\t\t\t\tfor (const [number, indices] of entries) {\n\t\t\t\t\tconst issue = byNumber.get(number);\n\t\t\t\t\tlet slot: PromiseSettledResult<IssueEtagFields | undefined>;\n\t\t\t\t\tif (issue == null) {\n\t\t\t\t\t\tslot = { status: 'rejected', reason: new Error(`Linear did not return ${teamKey}-${number}`) };\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tslot = { status: 'fulfilled', value: toLinearIssueEtagFields(issue) };\n\t\t\t\t\t\t\tanswered = true;\n\t\t\t\t\t\t} catch (ex) {\n\t\t\t\t\t\t\tslot = { status: 'rejected', reason: ex };\n\t\t\t\t\t\t\tfailed = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const index of indices) {\n\t\t\t\t\t\tslots[index] = slot;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn answered || failed ? slots : undefined;\n\t}\n\n\tprivate async getRawProviderIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: ResourceDescriptor,\n\t\tid: string,\n\t): Promise<ProviderIssue | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tif (!isIssueResourceDescriptor(resource)) {\n\t\t\tLogger.error(undefined, 'getProviderIssue: resource is not an IssueResourceDescriptor');\n\t\t\treturn undefined;\n\t\t}\n\n\t\treturn api.getIssue(toTokenWithInfo(this.id, session), {\n\t\t\tresourceId: resource.id,\n\t\t\tnumber: id,\n\t\t});\n\t}\n\tprivate getIssueAutolinkLikeUrl(issue: ProviderIssue): string | null {\n\t\tconst url = issue.url;\n\t\tif (url == null) return null;\n\n\t\tconst lastSegment = url.split('/').pop();\n\t\tif (!lastSegment || issue.number === lastSegment) {\n\t\t\treturn url;\n\t\t}\n\t\treturn url.substring(0, url.length - lastSegment.length - 1);\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ProviderRequestFunction } from "./models.js";
|
|
2
|
+
import { IssueEtagFields } from "../models/integration.js";
|
|
3
|
+
//#region ../plus/integrations/src/providers/linearIssuesEtag.d.ts
|
|
4
|
+
/** The most issue numbers one cheap check asks of one team. */
|
|
5
|
+
export declare const linearIssuesEtagMaxNumbers = 50;
|
|
6
|
+
export type LinearIssueEtagNode = {
|
|
7
|
+
identifier: string;
|
|
8
|
+
number: number;
|
|
9
|
+
updatedAt?: string | null;
|
|
10
|
+
archivedAt?: string | null;
|
|
11
|
+
state?: {
|
|
12
|
+
type?: string | null;
|
|
13
|
+
} | null;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* The cheap check behind the batch issue read's etags: the change state of up to {@link linearIssuesEtagMaxNumbers}
|
|
17
|
+
* issues of one team in ONE GraphQL request, where the full read sends provider-apis' `issue(id:)` once per issue.
|
|
18
|
+
* Linear returns only the issues it found, in its own order, so a caller matches them by number, never by position.
|
|
19
|
+
*
|
|
20
|
+
* Sent as provider-apis sends every Linear request: the token as the whole `Authorization` header, with no scheme.
|
|
21
|
+
* GraphQL `errors` reject the whole request, as provider-apis rejects them, but keep the response so a throttling
|
|
22
|
+
* code is still classified as one. A next page also rejects it: with every number unique within a team, more
|
|
23
|
+
* issues than were asked for means the filter was not applied as written.
|
|
24
|
+
*/
|
|
25
|
+
export declare function requestLinearIssuesEtagFields(request: ProviderRequestFunction, accessToken: string, teamKey: string, numbers: readonly number[]): Promise<LinearIssueEtagNode[]>;
|
|
26
|
+
/**
|
|
27
|
+
* A cheap-checked issue's change state, as the fields the full row's etag reads. The full read's row is provider-apis'
|
|
28
|
+
* Linear issue normalizer (0.61.0) then `fromProviderIssue`, so this mirrors both:
|
|
29
|
+
*
|
|
30
|
+
* - provider-apis maps `state.type` to a category — `completed` and `canceled` to `DONE`, `started` to `IN_PROGRESS`,
|
|
31
|
+
* anything else (`backlog`, `unstarted`, `triage`) to `TO_DO`, and no state to none — and sets `closedDate` from
|
|
32
|
+
* `archivedAt` (a falsy value reads as none);
|
|
33
|
+
* - `fromProviderIssue` closes an issue that has a `closedDate` or a `DONE` category, so an archived issue is closed
|
|
34
|
+
* whatever its state.
|
|
35
|
+
*
|
|
36
|
+
* Throws on a missing or unparseable `updatedAt`, so the target falls through to the full read rather than matching an
|
|
37
|
+
* etag the full row would compute from a fallback date (`fromProviderIssue` falls back to `closedDate`, then
|
|
38
|
+
* `createdDate`).
|
|
39
|
+
*/
|
|
40
|
+
export declare function toLinearIssueEtagFields(issue: LinearIssueEtagNode): IssueEtagFields;
|
|
41
|
+
//#endregion
|
|
42
|
+
//# sourceMappingURL=linearIssuesEtag.d.ts.map
|