@gitkraken/core-gitlens 0.5.107 → 0.5.109

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.
Files changed (145) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/dist/git/models/issue.d.ts +70 -0
  3. package/dist/git/models/issue.d.ts.map +1 -1
  4. package/dist/git/models/issue.js +17 -0
  5. package/dist/git/models/issue.js.map +1 -1
  6. package/dist/git/utils/issue.utils.d.ts +41 -1
  7. package/dist/git/utils/issue.utils.d.ts.map +1 -1
  8. package/dist/git/utils/issue.utils.js +80 -0
  9. package/dist/git/utils/issue.utils.js.map +1 -1
  10. package/dist/plus/git-github/api/github.d.ts +33 -6
  11. package/dist/plus/git-github/api/github.d.ts.map +1 -1
  12. package/dist/plus/git-github/api/github.js +137 -15
  13. package/dist/plus/git-github/api/github.js.map +1 -1
  14. package/dist/plus/git-github/api/issueSearchQuery.d.ts +49 -5
  15. package/dist/plus/git-github/api/issueSearchQuery.d.ts.map +1 -1
  16. package/dist/plus/git-github/api/issueSearchQuery.js +64 -6
  17. package/dist/plus/git-github/api/issueSearchQuery.js.map +1 -1
  18. package/dist/plus/integrations/collectionMetadata.d.ts +18 -0
  19. package/dist/plus/integrations/collectionMetadata.d.ts.map +1 -1
  20. package/dist/plus/integrations/collectionMetadata.js +24 -0
  21. package/dist/plus/integrations/collectionMetadata.js.map +1 -1
  22. package/dist/plus/integrations/index.d.ts +1 -1
  23. package/dist/plus/integrations/index.d.ts.map +1 -1
  24. package/dist/plus/integrations/index.js.map +1 -1
  25. package/dist/plus/integrations/integrationService.d.ts +15 -9
  26. package/dist/plus/integrations/integrationService.d.ts.map +1 -1
  27. package/dist/plus/integrations/integrationService.js.map +1 -1
  28. package/dist/plus/integrations/manager.d.ts +115 -26
  29. package/dist/plus/integrations/manager.d.ts.map +1 -1
  30. package/dist/plus/integrations/models/gitHostIntegration.d.ts +6 -31
  31. package/dist/plus/integrations/models/gitHostIntegration.d.ts.map +1 -1
  32. package/dist/plus/integrations/models/gitHostIntegration.js +11 -1
  33. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  34. package/dist/plus/integrations/models/issueReads.d.ts +63 -1
  35. package/dist/plus/integrations/models/issueReads.d.ts.map +1 -1
  36. package/dist/plus/integrations/models/issuesIntegration.d.ts +7 -21
  37. package/dist/plus/integrations/models/issuesIntegration.d.ts.map +1 -1
  38. package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
  39. package/dist/plus/integrations/providerFilters.d.ts +1 -1
  40. package/dist/plus/integrations/providerFilters.d.ts.map +1 -1
  41. package/dist/plus/integrations/providers/azureDevOps.d.ts.map +1 -1
  42. package/dist/plus/integrations/providers/azureDevOps.js +1 -0
  43. package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
  44. package/dist/plus/integrations/providers/github.d.ts.map +1 -1
  45. package/dist/plus/integrations/providers/github.js +1 -0
  46. package/dist/plus/integrations/providers/github.js.map +1 -1
  47. package/dist/plus/integrations/providers/gitlab.d.ts.map +1 -1
  48. package/dist/plus/integrations/providers/gitlab.js +1 -0
  49. package/dist/plus/integrations/providers/gitlab.js.map +1 -1
  50. package/dist/plus/integrations/providers/issueSorts.d.ts +36 -0
  51. package/dist/plus/integrations/providers/issueSorts.d.ts.map +1 -0
  52. package/dist/plus/integrations/providers/issueSorts.js +76 -0
  53. package/dist/plus/integrations/providers/issueSorts.js.map +1 -0
  54. package/dist/plus/integrations/providers/jira.d.ts +3 -9
  55. package/dist/plus/integrations/providers/jira.d.ts.map +1 -1
  56. package/dist/plus/integrations/providers/jira.js +5 -1
  57. package/dist/plus/integrations/providers/jira.js.map +1 -1
  58. package/dist/plus/integrations/providers/linear.d.ts +24 -9
  59. package/dist/plus/integrations/providers/linear.d.ts.map +1 -1
  60. package/dist/plus/integrations/providers/linear.js +63 -5
  61. package/dist/plus/integrations/providers/linear.js.map +1 -1
  62. package/dist/plus/integrations/providers/models.d.ts +63 -7
  63. package/dist/plus/integrations/providers/models.d.ts.map +1 -1
  64. package/dist/plus/integrations/providers/models.js +55 -0
  65. package/dist/plus/integrations/providers/models.js.map +1 -1
  66. package/dist/plus/integrations/providers/providersApi.d.ts +18 -3
  67. package/dist/plus/integrations/providers/providersApi.d.ts.map +1 -1
  68. package/dist/plus/integrations/providers/providersApi.js +24 -7
  69. package/dist/plus/integrations/providers/providersApi.js.map +1 -1
  70. package/dist/plus/integrations/providers/trello.d.ts +3 -9
  71. package/dist/plus/integrations/providers/trello.d.ts.map +1 -1
  72. package/dist/plus/integrations/providers/trello.js +1 -0
  73. package/dist/plus/integrations/providers/trello.js.map +1 -1
  74. package/dist/plus/integrations/providers/utils/providerPaging.d.ts.map +1 -1
  75. package/dist/plus/integrations/providers/utils/providerPaging.js +5 -1
  76. package/dist/plus/integrations/providers/utils/providerPaging.js.map +1 -1
  77. package/dist/plus/integrations/reads/broaden.d.ts +6 -0
  78. package/dist/plus/integrations/reads/broaden.d.ts.map +1 -1
  79. package/dist/plus/integrations/reads/broaden.js.map +1 -1
  80. package/dist/plus/integrations/reads/counts.d.ts +8 -1
  81. package/dist/plus/integrations/reads/counts.d.ts.map +1 -1
  82. package/dist/plus/integrations/reads/counts.js.map +1 -1
  83. package/dist/plus/integrations/reads/filters.d.ts +59 -9
  84. package/dist/plus/integrations/reads/filters.d.ts.map +1 -1
  85. package/dist/plus/integrations/reads/filters.js +17 -0
  86. package/dist/plus/integrations/reads/filters.js.map +1 -1
  87. package/dist/plus/integrations/reads/issueTracker.d.ts +18 -1
  88. package/dist/plus/integrations/reads/issueTracker.d.ts.map +1 -1
  89. package/dist/plus/integrations/reads/issueTracker.js +40 -3
  90. package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
  91. package/dist/plus/integrations/reads/issues.d.ts +27 -3
  92. package/dist/plus/integrations/reads/issues.d.ts.map +1 -1
  93. package/dist/plus/integrations/reads/issues.js +223 -123
  94. package/dist/plus/integrations/reads/issues.js.map +1 -1
  95. package/dist/plus/integrations/reads/ordering.d.ts +101 -0
  96. package/dist/plus/integrations/reads/ordering.d.ts.map +1 -0
  97. package/dist/plus/integrations/reads/ordering.js +48 -0
  98. package/dist/plus/integrations/reads/ordering.js.map +1 -0
  99. package/dist/plus/integrations/reads/searchIssues.d.ts +7 -2
  100. package/dist/plus/integrations/reads/searchIssues.d.ts.map +1 -1
  101. package/dist/plus/integrations/reads/searchIssues.js +9 -3
  102. package/dist/plus/integrations/reads/searchIssues.js.map +1 -1
  103. package/dist/plus/integrations/reads/sweeps.d.ts.map +1 -1
  104. package/dist/plus/integrations/reads/sweeps.js +120 -56
  105. package/dist/plus/integrations/reads/sweeps.js.map +1 -1
  106. package/dist/plus/integrations/reads/warnings.d.ts +34 -2
  107. package/dist/plus/integrations/reads/warnings.d.ts.map +1 -1
  108. package/dist/plus/integrations/reads/warnings.js +47 -2
  109. package/dist/plus/integrations/reads/warnings.js.map +1 -1
  110. package/dist/plus/integrations/results.d.ts +10 -0
  111. package/dist/plus/integrations/results.d.ts.map +1 -1
  112. package/dist/plus/integrations/results.js.map +1 -1
  113. package/package.json +2 -2
  114. package/src/git/models/issue.ts +73 -0
  115. package/src/git/utils/issue.utils.ts +91 -1
  116. package/src/plus/git-github/api/github.ts +180 -20
  117. package/src/plus/git-github/api/issueSearchQuery.ts +67 -7
  118. package/src/plus/integrations/collectionMetadata.ts +24 -0
  119. package/src/plus/integrations/index.ts +1 -0
  120. package/src/plus/integrations/integrationService.ts +15 -10
  121. package/src/plus/integrations/manager.ts +116 -31
  122. package/src/plus/integrations/models/gitHostIntegration.ts +18 -31
  123. package/src/plus/integrations/models/issueReads.ts +65 -1
  124. package/src/plus/integrations/models/issuesIntegration.ts +7 -6
  125. package/src/plus/integrations/providerFilters.ts +2 -0
  126. package/src/plus/integrations/providers/azureDevOps.ts +1 -0
  127. package/src/plus/integrations/providers/github.ts +1 -0
  128. package/src/plus/integrations/providers/gitlab.ts +1 -0
  129. package/src/plus/integrations/providers/issueSorts.ts +89 -0
  130. package/src/plus/integrations/providers/jira.ts +9 -3
  131. package/src/plus/integrations/providers/linear.ts +77 -8
  132. package/src/plus/integrations/providers/models.ts +122 -5
  133. package/src/plus/integrations/providers/providersApi.ts +51 -13
  134. package/src/plus/integrations/providers/trello.ts +4 -3
  135. package/src/plus/integrations/providers/utils/providerPaging.ts +6 -1
  136. package/src/plus/integrations/reads/broaden.ts +6 -0
  137. package/src/plus/integrations/reads/counts.ts +8 -1
  138. package/src/plus/integrations/reads/filters.ts +70 -8
  139. package/src/plus/integrations/reads/issueTracker.ts +78 -4
  140. package/src/plus/integrations/reads/issues.ts +386 -194
  141. package/src/plus/integrations/reads/ordering.ts +124 -0
  142. package/src/plus/integrations/reads/searchIssues.ts +15 -3
  143. package/src/plus/integrations/reads/sweeps.ts +150 -76
  144. package/src/plus/integrations/reads/warnings.ts +72 -1
  145. package/src/plus/integrations/results.ts +10 -0
@@ -12,14 +12,30 @@ import { toTokenWithInfo } from '../authentication/models.js';
12
12
  import { toCollectionScopeFailure } from '../collectionMetadata.js';
13
13
  import { IssuesCloudHostIntegrationId } from '../constants.js';
14
14
  import { IntegrationReadUnavailableError } from '../errors.js';
15
+ import type { AccountWideIssuesResult, IssuesForProjectOptions, SearchMyIssuesOptions } from '../models/issueReads.js';
15
16
  import { IssuesIntegration } from '../models/issuesIntegration.js';
16
- import type { IssueFilter, ProviderApiCollectionResult, ProviderIssue } from './models.js';
17
+ import type { ProviderApiCollectionResult, ProviderIssue } from './models.js';
17
18
  import { fromProviderIssue, providersMetadata, toIssueShape } from './models.js';
18
19
  import { mergeCollectionMetadata } from './utils/providerPaging.js';
19
20
 
20
21
  const metadata = providersMetadata[IssuesCloudHostIntegrationId.Linear];
21
22
  const authProvider = Object.freeze({ id: metadata.id, scopes: metadata.scopes });
22
23
  const maxPagesPerRequest = 10;
24
+ /**
25
+ * The account-wide drain's own backstop, separate from {@link maxPagesPerRequest}.
26
+ *
27
+ * Raised to 50 (5,000 issues at the SDK's 100-per-page) because 10 no longer means what it used to. That read
28
+ * once fanned out over four overlapping relationship queries and merged them, so most of what a page cost was
29
+ * rows already returned — a low budget capped the waste. It is now a single server-ordered query over an `or`
30
+ * filter, so every page is 100 issues the caller has not seen, and the same budget just truncates real results
31
+ * at 1,000.
32
+ *
33
+ * Reaching it is reported rather than silent: {@link LinearIntegration.searchProviderMyIssuesWithTruncation}
34
+ * returns `truncated`, so the facade can say the read was incomplete instead of publishing a capped list as a
35
+ * whole account. The bound stays — a runaway cursor must not spend requests forever — but it is now set where a
36
+ * real Linear account is unlikely to reach it rather than where the duplication used to hurt.
37
+ */
38
+ const maxAccountWidePagesPerRequest = 50;
23
39
  const linearImplicitTeamsPageSize = 50;
24
40
 
25
41
  export interface LinearTeamDescriptor extends IssueResourceDescriptor {
@@ -194,7 +210,7 @@ export class LinearIntegration extends IssuesIntegration<IssuesCloudHostIntegrat
194
210
  protected override async getProviderIssuesForProject(
195
211
  session: ProviderAuthenticationSession,
196
212
  project: ResourceDescriptor,
197
- options?: { user?: string; filters?: IssueFilter[] },
213
+ options?: IssuesForProjectOptions,
198
214
  ): Promise<IssueShape[] | undefined> {
199
215
  return (await this.getProviderIssuesForProjectWithTruncation(session, project, options))?.values;
200
216
  }
@@ -202,7 +218,7 @@ export class LinearIntegration extends IssuesIntegration<IssuesCloudHostIntegrat
202
218
  protected override async getProviderIssuesForProjectWithTruncation(
203
219
  session: ProviderAuthenticationSession,
204
220
  project: ResourceDescriptor,
205
- options?: { user?: string; filters?: IssueFilter[] },
221
+ options?: IssuesForProjectOptions,
206
222
  ): Promise<{ values: IssueShape[]; truncated: boolean; metadata?: CollectionMetadata } | undefined> {
207
223
  if (!isIssueResourceDescriptor(project)) return undefined;
208
224
 
@@ -222,7 +238,7 @@ export class LinearIntegration extends IssuesIntegration<IssuesCloudHostIntegrat
222
238
  result = await api.getLinearIssues(
223
239
  toTokenWithInfo(this.id, session),
224
240
  { teams: [project.id] },
225
- { cursor: cursor },
241
+ { cursor: cursor, sort: options?.sort },
226
242
  );
227
243
  } catch (ex) {
228
244
  // A page failure after the first page leaves the already-drained prefix intact; record the
@@ -305,14 +321,45 @@ export class LinearIntegration extends IssuesIntegration<IssuesCloudHostIntegrat
305
321
  resources?: ResourceDescriptor[],
306
322
  cancellation?: AbortSignal,
307
323
  ): Promise<IssueShape[] | undefined> {
324
+ return (await this.searchProviderMyIssuesWithTruncation(session, resources, cancellation))?.values;
325
+ }
326
+
327
+ /**
328
+ * Account-wide "my issues" for Linear, drained to exhaustion and reporting whether it got there.
329
+ *
330
+ * The SDK read is one server-ordered query over an `or` filter across the four relationships (assigned,
331
+ * created, named in an issue's content, named in a comment), so each page is 100 issues the caller has not
332
+ * seen and the pages are ordered as one sequence. Draining it is therefore just following the cursor.
333
+ *
334
+ * Overridden rather than left to the base default because that default hardcodes `truncated: false`, and a
335
+ * bounded drain that stops early is exactly the case a caller must be able to see. Three things end the
336
+ * drain, and only one of them is completion:
337
+ *
338
+ * - the provider says there is no next page — complete;
339
+ * - the backstop is reached — `truncated`, because issues beyond it exist and were not read;
340
+ * - the provider claims a next page but hands back a cursor that does not advance — also `truncated`, since
341
+ * the read cannot continue and the remainder is unreachable rather than absent.
342
+ *
343
+ * Cancellation is deliberately NOT truncation: the caller asked for the read to stop, so the partial list is
344
+ * what it asked for, and flagging it would surface an incompleteness warning for a user action.
345
+ */
346
+ protected override async searchProviderMyIssuesWithTruncation(
347
+ session: ProviderAuthenticationSession,
348
+ resources?: ResourceDescriptor[],
349
+ cancellation?: AbortSignal,
350
+ options?: SearchMyIssuesOptions,
351
+ ): Promise<AccountWideIssuesResult | undefined> {
308
352
  if (resources != null) {
309
353
  return undefined;
310
354
  }
311
355
 
312
356
  const api = await this.getProvidersApi();
313
357
  let cursor = undefined;
314
- let hasMore: boolean;
358
+ // Starts false so an immediate cancellation, which leaves the loop before the first response, reads as
359
+ // "no more pages known" rather than as an unfinished drain.
360
+ let hasMore = false;
315
361
  let requestCount = 0;
362
+ let truncated = false;
316
363
  const issues = [];
317
364
  try {
318
365
  do {
@@ -322,25 +369,47 @@ export class LinearIntegration extends IssuesIntegration<IssuesCloudHostIntegrat
322
369
 
323
370
  const result = await api.getIssuesForCurrentUser(toTokenWithInfo(this.id, session), {
324
371
  cursor: cursor,
372
+ sort: options?.sort,
325
373
  });
326
374
  requestCount += 1;
327
375
  hasMore = result.paging?.more ?? false;
328
- cursor = result.paging?.cursor;
376
+ const nextCursor = result.paging?.cursor;
377
+
378
+ // Keep this page before deciding whether to continue: the request is already paid for, so
379
+ // dropping its rows would lose real results to save nothing.
329
380
  const formattedIssues = result.values
330
381
  .map(issue => toIssueShape(issue, this))
331
382
  .filter((result): result is IssueShape => result != null);
332
383
  if (formattedIssues.length > 0) {
333
384
  issues.push(...formattedIssues);
334
385
  }
335
- } while (requestCount < maxPagesPerRequest && hasMore);
386
+
387
+ // The provider claims more but hands back no advancing cursor: continuing would re-read the page
388
+ // just returned. `ProvidersApi.getPagedResult` already normalizes this into `more: false`, so
389
+ // this is a backstop for if that ever stops holding rather than a case seen today -- but the rest
390
+ // of the account is unreachable either way, which is what `truncated` says.
391
+ if (hasMore && (nextCursor == null || nextCursor === cursor)) {
392
+ truncated = true;
393
+ break;
394
+ }
395
+
396
+ cursor = nextCursor;
397
+ } while (requestCount < maxAccountWidePagesPerRequest && hasMore);
398
+
399
+ // Ran out of budget with pages still to come.
400
+ if (hasMore && requestCount >= maxAccountWidePagesPerRequest) {
401
+ truncated = true;
402
+ }
336
403
  } catch (ex) {
337
404
  if (issues.length === 0) {
338
405
  throw ex;
339
406
  }
340
407
 
408
+ // Kept what was already fetched, so the list is real but short of the account.
409
+ truncated = true;
341
410
  Logger.error(ex, 'searchProviderMyIssues');
342
411
  }
343
- return issues;
412
+ return { values: issues, truncated: truncated };
344
413
  }
345
414
  protected override async getProviderLinkedIssueOrPullRequest(
346
415
  session: ProviderAuthenticationSession,
@@ -75,7 +75,7 @@ import {
75
75
  IssuesCloudHostIntegrationId,
76
76
  } from '../constants.js';
77
77
  import type { Integration, IntegrationType } from '../models/integration.js';
78
- import type { IssueSearchCapabilities, PullRequestSearchCapabilities } from '../providerFilters.js';
78
+ import type { IssueSearchCapabilities, IssueSorting, PullRequestSearchCapabilities } from '../providerFilters.js';
79
79
  import { IssueFilter, PullRequestFilter } from '../providerFilters.js';
80
80
 
81
81
  export { IssueFilter, PullRequestFilter } from '../providerFilters.js';
@@ -83,12 +83,25 @@ export type {
83
83
  IssueSearchCapabilities,
84
84
  IssueSearchCriteria,
85
85
  IssueSearchRelationship,
86
+ IssueSortField,
87
+ IssueSorting,
86
88
  PullRequestSearchCapabilities,
87
89
  PullRequestSearchCriteria,
88
90
  } from '../providerFilters.js';
89
91
  import type { ProviderRepositoryShape } from '../results.js';
90
92
 
91
93
  export type { ProviderOrganization, ProviderRepositoryShape } from '../results.js';
94
+ import {
95
+ azureAccountWideIssueSorts,
96
+ azureIssueSorts,
97
+ githubAccountWideIssueSorts,
98
+ githubIssueSorts,
99
+ gitlabAccountWideIssueSorts,
100
+ gitlabIssueSorts,
101
+ jiraIssueSorts,
102
+ linearIssueSorts,
103
+ trelloIssueSorts,
104
+ } from './issueSorts.js';
92
105
  import { getEntityIdentifierInput } from './utils.js';
93
106
 
94
107
  type GitBuildStatusState = GitBuildStatusStateType;
@@ -156,6 +169,28 @@ type EnrichableItem = any;
156
169
  export type ProviderAccount = Account;
157
170
  export type ProviderReposInput = (string | number)[] | GetRepoInput[];
158
171
  export type ProviderRepoInput = GetRepoInput;
172
+
173
+ /**
174
+ * Which half of {@link ProviderReposInput} a caller supplied: repository IDS rather than descriptors.
175
+ *
176
+ * The union has no discriminant, so the answer is the element shape — and several layers need it for different
177
+ * reasons (`ProvidersApi` to pick `repoIds` vs `repos` in the SDK input, `GitHostIntegration` to refuse an
178
+ * id-based read a provider can't serve, `listIssuesPage` to know GitLab's id form takes the SDK's own merging
179
+ * aggregate). One predicate rather than one per layer, because each copy is free to disagree about the shape,
180
+ * and the symptom of disagreement is a read routed to the wrong provider query.
181
+ *
182
+ * Takes `unknown` rather than {@link ProviderReposInput} because `ProvidersApi.isRepoIdsInput` forwards here and
183
+ * is public with an unvalidated parameter, so the array check has to stay part of the rule rather than be assumed
184
+ * from the parameter type.
185
+ */
186
+ export function isRepoIdsInput(input: unknown): input is (string | number)[] {
187
+ return (
188
+ input != null &&
189
+ Array.isArray(input) &&
190
+ input.every(repo => typeof repo === 'string' || typeof repo === 'number')
191
+ );
192
+ }
193
+
159
194
  export type ProviderPullRequest = GitPullRequest;
160
195
  export type ProviderRepository = GitRepository;
161
196
  export type ProviderIssue = ProviderApiIssue;
@@ -331,6 +366,20 @@ export interface GetIssuesOptions {
331
366
  page?: number;
332
367
  // Items to request per page (numbered-page providers, plus GitHub's maxPageSize).
333
368
  pageSize?: number;
369
+ /**
370
+ * How the provider should order the result, as `field:direction`.
371
+ *
372
+ * Passed through to the SDK verbatim — the translation to each provider's own vocabulary lives THERE, next to
373
+ * the capability map it validates against, so that the table this facade publishes and the table that does the
374
+ * translating can't drift apart across two repositories. A key the provider can't express raises the SDK's
375
+ * `UnsupportedSortError`, which is why the facade validates against `supportedIssueSorts` first: reaching that
376
+ * error means this table has outrun the SDK's.
377
+ *
378
+ * Always supplied by the facade's reads, never omitted. Omitting it would delegate the order to each provider's
379
+ * own default, which is the incoherence across providers the SDK deliberately preserves and this layer
380
+ * deliberately does not.
381
+ */
382
+ sort?: IssueSorting;
334
383
  }
335
384
 
336
385
  export interface GetIssuesForRepoInput extends GetIssuesOptions {
@@ -492,6 +541,8 @@ export type GetIssuesForCurrentUserInput = PagingInput & {
492
541
  assigneeUsername?: string;
493
542
  authorUsername?: string;
494
543
  pageSize?: number;
544
+ /** See {@link GetIssuesOptions.sort}. */
545
+ sort?: IssueSorting;
495
546
  };
496
547
 
497
548
  export type GetIssuesForCurrentUserFn = (
@@ -624,10 +675,10 @@ export type GetBitbucketServerPullRequestsForCurrentUserFn = (
624
675
  data: GitPullRequest[];
625
676
  }>;
626
677
  export type GetIssuesForProjectFn = Jira['getIssuesForProject'];
627
- export type GetIssuesForResourceForCurrentUserFn = (
628
- input: { resourceId: string },
629
- options?: EnterpriseOptions,
630
- ) => Promise<{ data: ProviderIssue[] }>;
678
+ // Derived from the client method rather than hand-declared, as its project-scoped sibling above already is: the
679
+ // hand-written shape named only `resourceId`, so every other field the SDK accepts (the cursor, the sort, the
680
+ // transitions switch) was invisible to the type system and had to be smuggled through `getPagedResult`'s `any`.
681
+ export type GetIssuesForResourceForCurrentUserFn = Jira['getIssuesForResourceForCurrentUser'];
631
682
 
632
683
  // Trello reads (issues-capable provider). The Trello client is keyed by an `appKey` (the Trello app key from
633
684
  // the cloud token exchange) alongside the OAuth token, so these mirror the client method shapes directly.
@@ -729,6 +780,36 @@ export interface ProviderMetadata {
729
780
  * axes; it omits `Mention` because that REST read has no first-class mention filter.
730
781
  */
731
782
  supportedAccountWideIssueFilters?: IssueFilter[];
783
+ /**
784
+ * Sort keys the REPO-scoped issue read can express server-side — and, for an issue tracker (Jira/Linear/Trello),
785
+ * its project-scoped read, which is the only issue surface a tracker has.
786
+ *
787
+ * Absent/empty means the read can't be ordered at all, so a request carrying `sort` is refused rather than
788
+ * served in whatever order the provider felt like. Present, it is a promise: every key here reaches the
789
+ * provider query.
790
+ *
791
+ * A key listed here is expressible on ONE provider query, which is what this read is when it is given ONE
792
+ * scope. Given several (GitLab across repositories, Azure across projects, a tracker across projects) it merges
793
+ * their results here and can only honor a key a normalized issue carries, so it additionally refuses
794
+ * `priority`/`dueDate`/`resolved` — see `getIssueComparator`. That depends on the caller's scope count rather
795
+ * than on the provider, which is why it is a runtime refusal and not a fourth table: encoding it here would
796
+ * under-report the single-scope read, which really does support those keys.
797
+ */
798
+ supportedIssueSorts?: IssueSorting[];
799
+ /**
800
+ * Sort keys the ACCOUNT-WIDE issue read can express server-side, which is NOT the same set as
801
+ * {@link ProviderMetadata.supportedIssueSorts} — for GitLab the two reads are different APIs (GraphQL vs REST)
802
+ * with genuinely different vocabularies, not one narrowed twice.
803
+ *
804
+ * Absent means the account-wide read can't be ordered (or doesn't exist). An issue tracker leaves this empty
805
+ * and reports under `supportedIssueSorts`, so reading a tracker's capability from here under-reports it.
806
+ *
807
+ * Unlike {@link ProviderMetadata.supportedIssueSorts} this needs no companion runtime refusal, because every
808
+ * provider's account-wide read is a union of several queries with no scope count for a caller to reduce: it
809
+ * ALWAYS merges. So the keys a merge can't order by are simply absent here — the table states what the read can
810
+ * honor, which is what makes intersecting against it sufficient.
811
+ */
812
+ supportedAccountWideIssueSorts?: IssueSorting[];
732
813
  /**
733
814
  * What the provider's FILTERED issue search (`searchIssuesPage`, and the `countIssues` probe over the same
734
815
  * criteria) can express server-side. A third, wider surface than either filter set above: it is not bound to
@@ -774,6 +855,8 @@ const githubIssueSearchCapabilities: IssueSearchCapabilities = {
774
855
  withoutLinkedPullRequest: true,
775
856
  // `is:open` / `is:closed`, or neither for all states.
776
857
  states: true,
858
+ // `sort:created-desc`, `sort:updated`, `sort:comments-asc`, … — see `githubIssueSorts`.
859
+ sorts: githubIssueSorts,
777
860
  };
778
861
 
779
862
  /** GitHub and GHE use the same filtered pull-request search syntax and result ceiling. */
@@ -820,6 +903,10 @@ export const providersMetadata: ProvidersMetadata = {
820
903
  // The account-wide read is three independent searches (`author:@me`, `assignee:@me`, `mentions:@me`) behind
821
904
  // one composite cursor, so any subset of them is expressible.
822
905
  supportedAccountWideIssueFilters: [IssueFilter.Author, IssueFilter.Assignee, IssueFilter.Mention],
906
+ // One `search` field serves all three issue reads (repo-scoped, account-wide, filtered search), so the same
907
+ // qualifiers are expressible on each — the account-wide one narrowed to what its three-alias merge can order.
908
+ supportedIssueSorts: githubIssueSorts,
909
+ supportedAccountWideIssueSorts: githubAccountWideIssueSorts,
823
910
  supportedIssueSearch: githubIssueSearchCapabilities,
824
911
  issueSearchResultLimit: githubSearchResultLimit,
825
912
  scopes: ['repo', 'read:user', 'user:email'],
@@ -852,6 +939,10 @@ export const providersMetadata: ProvidersMetadata = {
852
939
  // The account-wide read is three independent searches (`author:@me`, `assignee:@me`, `mentions:@me`) behind
853
940
  // one composite cursor, so any subset of them is expressible.
854
941
  supportedAccountWideIssueFilters: [IssueFilter.Author, IssueFilter.Assignee, IssueFilter.Mention],
942
+ // One `search` field serves all three issue reads (repo-scoped, account-wide, filtered search), so the same
943
+ // qualifiers are expressible on each — the account-wide one narrowed to what its three-alias merge can order.
944
+ supportedIssueSorts: githubIssueSorts,
945
+ supportedAccountWideIssueSorts: githubAccountWideIssueSorts,
855
946
  supportedIssueSearch: githubIssueSearchCapabilities,
856
947
  issueSearchResultLimit: githubSearchResultLimit,
857
948
  scopes: ['repo', 'read:user', 'user:email'],
@@ -881,6 +972,10 @@ export const providersMetadata: ProvidersMetadata = {
881
972
  // unioned, so either axis is expressible on its own. Mention is absent because GitLab's REST issue read has
882
973
  // no first-class mention filter to narrow with.
883
974
  supportedAccountWideIssueFilters: [IssueFilter.Assignee, IssueFilter.Author],
975
+ // Two different surfaces, not one narrowed twice: the repository-scoped read is GraphQL and the
976
+ // account-wide one is REST, and REST has neither `title` nor `closed_at`.
977
+ supportedIssueSorts: gitlabIssueSorts,
978
+ supportedAccountWideIssueSorts: gitlabAccountWideIssueSorts,
884
979
  scopes: ['api', 'read_user', 'read_repository'],
885
980
  },
886
981
  [GitSelfManagedHostIntegrationId.CloudGitLabSelfHosted]: {
@@ -908,6 +1003,10 @@ export const providersMetadata: ProvidersMetadata = {
908
1003
  // unioned, so either axis is expressible on its own. Mention is absent because GitLab's REST issue read has
909
1004
  // no first-class mention filter to narrow with.
910
1005
  supportedAccountWideIssueFilters: [IssueFilter.Assignee, IssueFilter.Author],
1006
+ // Two different surfaces, not one narrowed twice: the repository-scoped read is GraphQL and the
1007
+ // account-wide one is REST, and REST has neither `title` nor `closed_at`.
1008
+ supportedIssueSorts: gitlabIssueSorts,
1009
+ supportedAccountWideIssueSorts: gitlabAccountWideIssueSorts,
911
1010
  scopes: ['api', 'read_user', 'read_repository'],
912
1011
  },
913
1012
  [GitCloudHostIntegrationId.Bitbucket]: {
@@ -956,6 +1055,11 @@ export const providersMetadata: ProvidersMetadata = {
956
1055
  // The account-wide read drains one (project × assignee) and one (project × author) query per project, so
957
1056
  // either axis is expressible on its own. There is no mention query to narrow to.
958
1057
  supportedAccountWideIssueFilters: [IssueFilter.Author, IssueFilter.Assignee],
1058
+ // Both reads emit the same WIQL, but the account-wide one drains every (project x relationship) query and
1059
+ // merges the results, so it can only honor keys a normalized issue carries — `resolved` and `priority` are
1060
+ // dropped from that surface for the same reason they are dropped from GitLab's.
1061
+ supportedIssueSorts: azureIssueSorts,
1062
+ supportedAccountWideIssueSorts: azureAccountWideIssueSorts,
959
1063
  scopes: ['vso.code', 'vso.identity', 'vso.project', 'vso.profile', 'vso.work'],
960
1064
  },
961
1065
  [GitSelfManagedHostIntegrationId.AzureDevOpsServer]: {
@@ -982,6 +1086,11 @@ export const providersMetadata: ProvidersMetadata = {
982
1086
  // The account-wide read drains one (project × assignee) and one (project × author) query per project, so
983
1087
  // either axis is expressible on its own. There is no mention query to narrow to.
984
1088
  supportedAccountWideIssueFilters: [IssueFilter.Author, IssueFilter.Assignee],
1089
+ // Both reads emit the same WIQL, but the account-wide one drains every (project x relationship) query and
1090
+ // merges the results, so it can only honor keys a normalized issue carries — `resolved` and `priority` are
1091
+ // dropped from that surface for the same reason they are dropped from GitLab's.
1092
+ supportedIssueSorts: azureIssueSorts,
1093
+ supportedAccountWideIssueSorts: azureAccountWideIssueSorts,
985
1094
  scopes: ['vso.code', 'vso.identity', 'vso.project', 'vso.profile', 'vso.work'],
986
1095
  },
987
1096
  [IssuesCloudHostIntegrationId.Jira]: {
@@ -1027,6 +1136,9 @@ export const providersMetadata: ProvidersMetadata = {
1027
1136
  'read:project-version:jira',
1028
1137
  ],
1029
1138
  supportedIssueFilters: [IssueFilter.Author, IssueFilter.Assignee, IssueFilter.Mention],
1139
+ // A tracker's issues live under resource -> project, so it has no account-wide surface to declare: its
1140
+ // capability is reported under `issues`, which is what `listIssueTrackerIssuesPage` validates against.
1141
+ supportedIssueSorts: jiraIssueSorts,
1030
1142
  },
1031
1143
  [IssuesCloudHostIntegrationId.Linear]: {
1032
1144
  domain: 'linear.app',
@@ -1037,6 +1149,9 @@ export const providersMetadata: ProvidersMetadata = {
1037
1149
  scopes: [],
1038
1150
  // Linear scopes "my issues" client-side by the viewer's assignee id; author/mention aren't supported.
1039
1151
  supportedIssueFilters: [IssueFilter.Assignee],
1152
+ // Both directions, and one table for both reads: they are the same root `issues` query taking the same
1153
+ // `sort` argument, so there is no account-wide narrowing to declare.
1154
+ supportedIssueSorts: linearIssueSorts,
1040
1155
  },
1041
1156
  [IssuesCloudHostIntegrationId.Trello]: {
1042
1157
  domain: 'trello.com',
@@ -1047,6 +1162,8 @@ export const providersMetadata: ProvidersMetadata = {
1047
1162
  scopes: [],
1048
1163
  // Trello cards are filtered by the assignee (member) only; author/mention have no Trello equivalent.
1049
1164
  supportedIssueFilters: [IssueFilter.Assignee],
1165
+ // `sort:edited` / `sort:-edited`. Trello's other search sorts (`created`, `due`) have no card-order effect.
1166
+ supportedIssueSorts: trelloIssueSorts,
1050
1167
  },
1051
1168
  };
1052
1169
 
@@ -39,6 +39,7 @@ import type {
39
39
  GetPullRequestsOptions,
40
40
  GetReposOptions,
41
41
  IssueFilter,
42
+ IssueSorting,
42
43
  PageInfo,
43
44
  PagingInput,
44
45
  PagingMode,
@@ -67,7 +68,7 @@ import type {
67
68
  Providers,
68
69
  PullRequestFilter,
69
70
  } from './models.js';
70
- import { providersMetadata } from './models.js';
71
+ import { isRepoIdsInput, providersMetadata } from './models.js';
71
72
  import {
72
73
  collectProviderPagedResult,
73
74
  mergeCollectionMetadata,
@@ -132,6 +133,14 @@ function isGraphQLRepoNotFoundError(ex: unknown): boolean {
132
133
 
133
134
  const trelloBaseUrl = 'https://api.trello.com';
134
135
 
136
+ /**
137
+ * Jira computes an issue's available transitions per issue, from the workflow and the reader's permissions, so a
138
+ * list read at a page size of 100 pays for 100 of those in both server work and response payload. Nothing in this
139
+ * package reads `statusTransitions` off a list — only the singular `getIssue` path exposes them, and it always
140
+ * expands them — so every list read opts out. Revisit the day a list row offers a status change straight off it.
141
+ */
142
+ const jiraListIncludeTransitions = false;
143
+
135
144
  type TrelloMemberResponse = {
136
145
  id: string;
137
146
  username?: string | null;
@@ -479,12 +488,9 @@ export class ProvidersApi {
479
488
  );
480
489
  }
481
490
 
482
- isRepoIdsInput(input: any): input is (string | number)[] {
483
- return (
484
- input != null &&
485
- Array.isArray(input) &&
486
- input.every((id: any) => typeof id === 'string' || typeof id === 'number')
487
- );
491
+ /** See {@link isRepoIdsInput}, which owns the rule; kept as a method because callers reach it through the api. */
492
+ isRepoIdsInput(input: unknown): input is (string | number)[] {
493
+ return isRepoIdsInput(input);
488
494
  }
489
495
 
490
496
  private async getProviderToken<T extends IntegrationIds>(
@@ -868,7 +874,10 @@ export class ProvidersApi {
868
874
  async getLinearIssues(
869
875
  tokenOptInfo: TokenWithInfo<IssuesCloudHostIntegrationId.Linear>,
870
876
  input: { teams?: string[]; projects?: string[]; labels?: string[] },
871
- options?: PagingInput,
877
+ options?: PagingInput & {
878
+ /** See {@link GetIssuesOptions.sort}. Linear expresses `created`/`updated`, descending only. */
879
+ sort?: IssueSorting;
880
+ },
872
881
  ): Promise<PagedResult<ProviderIssue>> {
873
882
  const { provider, tokenWithInfo } = await this.ensureProviderTokenAndFunction(
874
883
  tokenOptInfo,
@@ -1565,6 +1574,8 @@ export class ProvidersApi {
1565
1574
  */
1566
1575
  authorUsername?: string;
1567
1576
  pageSize?: number;
1577
+ /** See {@link GetIssuesOptions.sort}. Forwarded to the provider fn; ordering is translated in the SDK. */
1578
+ sort?: IssueSorting;
1568
1579
  isPAT?: boolean;
1569
1580
  baseUrl?: string;
1570
1581
  },
@@ -1580,6 +1591,7 @@ export class ProvidersApi {
1580
1591
  authorUsername: options?.authorUsername,
1581
1592
  page: options?.page,
1582
1593
  pageSize: options?.pageSize,
1594
+ sort: options?.sort,
1583
1595
  },
1584
1596
  provider.getIssuesForCurrentUserFn,
1585
1597
  tokenWithInfo,
@@ -1626,7 +1638,12 @@ export class ProvidersApi {
1626
1638
 
1627
1639
  try {
1628
1640
  const result = await provider.getIssuesForProjectFn?.(
1629
- { projectKey: project, resourceId: resourceId, ...options },
1641
+ {
1642
+ projectKey: project,
1643
+ resourceId: resourceId,
1644
+ ...options,
1645
+ includeTransitions: jiraListIncludeTransitions,
1646
+ },
1630
1647
  { token: token },
1631
1648
  );
1632
1649
 
@@ -1655,7 +1672,12 @@ export class ProvidersApi {
1655
1672
 
1656
1673
  try {
1657
1674
  const result = await provider.getIssuesForProjectFn?.(
1658
- { projectKey: project, resourceId: resourceId, ...options },
1675
+ {
1676
+ projectKey: project,
1677
+ resourceId: resourceId,
1678
+ ...options,
1679
+ includeTransitions: jiraListIncludeTransitions,
1680
+ },
1659
1681
  { token: token },
1660
1682
  );
1661
1683
  if (result == null) return undefined;
@@ -1760,7 +1782,12 @@ export class ProvidersApi {
1760
1782
  tokenOptInfo: TokenWithInfo,
1761
1783
  appKey: string,
1762
1784
  boardId: string,
1763
- options?: { assigneeLogins?: string[]; trelloBoardListsById?: Record<string, { name: string }> },
1785
+ options?: {
1786
+ assigneeLogins?: string[];
1787
+ trelloBoardListsById?: Record<string, { name: string }>;
1788
+ /** See {@link GetIssuesOptions.sort}. Trello expresses only `updated`, in either direction. */
1789
+ sort?: IssueSorting;
1790
+ },
1764
1791
  ): Promise<ProviderApiCollectionResult<ProviderIssue>> {
1765
1792
  const { provider, tokenWithInfo } = await this.ensureProviderTokenAndFunction(
1766
1793
  tokenOptInfo,
@@ -1782,7 +1809,18 @@ export class ProvidersApi {
1782
1809
  async getIssuesForResourceForCurrentUser(
1783
1810
  tokenOptInfo: TokenWithInfo,
1784
1811
  resourceId: string,
1785
- options?: { cursor?: string; isPAT?: boolean; baseUrl?: string },
1812
+ options?: {
1813
+ cursor?: string;
1814
+ /**
1815
+ * See {@link GetIssuesOptions.sort}. Forwarded for the same reason as every sibling issue read, even
1816
+ * though nothing reaches this one with a sort today: `IssuesIntegration.searchProviderMyIssues` is the
1817
+ * only caller and takes no options. A wrapper that accepted the SDK's input and silently dropped one
1818
+ * field of it would give whoever adds that option no ordering and no error.
1819
+ */
1820
+ sort?: IssueSorting;
1821
+ isPAT?: boolean;
1822
+ baseUrl?: string;
1823
+ },
1786
1824
  ): Promise<PagedResult<ProviderIssue>> {
1787
1825
  const { provider, tokenWithInfo } = await this.ensureProviderTokenAndFunction(
1788
1826
  tokenOptInfo,
@@ -1790,7 +1828,7 @@ export class ProvidersApi {
1790
1828
  );
1791
1829
 
1792
1830
  return this.getPagedResult<ProviderIssue>(
1793
- { resourceId: resourceId },
1831
+ { resourceId: resourceId, sort: options?.sort, includeTransitions: jiraListIncludeTransitions },
1794
1832
  provider.getIssuesForResourceForCurrentUserFn,
1795
1833
  tokenWithInfo,
1796
1834
  options?.cursor,
@@ -9,8 +9,8 @@ import type { ProviderAuthenticationSession } from '../authentication/models.js'
9
9
  import { toTokenWithInfo } from '../authentication/models.js';
10
10
  import { IssuesCloudHostIntegrationId } from '../constants.js';
11
11
  import { IntegrationReadUnavailableError } from '../errors.js';
12
+ import type { IssuesForProjectOptions } from '../models/issueReads.js';
12
13
  import { IssuesIntegration } from '../models/issuesIntegration.js';
13
- import type { IssueFilter } from './models.js';
14
14
  import { fromProviderIssue, providersMetadata, toIssueShape } from './models.js';
15
15
 
16
16
  const metadata = providersMetadata[IssuesCloudHostIntegrationId.Trello];
@@ -95,7 +95,7 @@ export class TrelloIntegration extends IssuesIntegration<IssuesCloudHostIntegrat
95
95
  protected override async getProviderIssuesForProject(
96
96
  session: ProviderAuthenticationSession,
97
97
  project: ResourceDescriptor,
98
- options?: { user?: string; filters?: IssueFilter[] },
98
+ options?: IssuesForProjectOptions,
99
99
  ): Promise<IssueShape[] | undefined> {
100
100
  return (await this.getProviderIssuesForProjectWithTruncation(session, project, options))?.values;
101
101
  }
@@ -103,7 +103,7 @@ export class TrelloIntegration extends IssuesIntegration<IssuesCloudHostIntegrat
103
103
  protected override async getProviderIssuesForProjectWithTruncation(
104
104
  session: ProviderAuthenticationSession,
105
105
  project: ResourceDescriptor,
106
- options?: { user?: string; filters?: IssueFilter[] },
106
+ options?: IssuesForProjectOptions,
107
107
  ): Promise<{ values: IssueShape[]; truncated: boolean; metadata?: CollectionMetadata } | undefined> {
108
108
  // A non-issue descriptor genuinely has nothing to read (empty), but a missing app key is a broken read.
109
109
  if (!isIssueResourceDescriptor(project)) return undefined;
@@ -129,6 +129,7 @@ export class TrelloIntegration extends IssuesIntegration<IssuesCloudHostIntegrat
129
129
  const result = await api.getTrelloIssuesForBoard(tokenWithInfo, appKey, project.id, {
130
130
  assigneeLogins: options?.user != null ? [options.user] : undefined,
131
131
  trelloBoardListsById: trelloBoardListsById,
132
+ sort: options?.sort,
132
133
  });
133
134
 
134
135
  const values = result.values.flatMap(issue => {
@@ -6,7 +6,7 @@ import type {
6
6
  } from '@gitkraken/provider-apis';
7
7
  import { isCancellationError } from '../../../../utils/cancellation.js';
8
8
  import { uniqueBy } from '../../../../utils/iterable.js';
9
- import { toCollectionScopeFailure } from '../../collectionMetadata.js';
9
+ import { throwIfCallerContractError, toCollectionScopeFailure } from '../../collectionMetadata.js';
10
10
  import { collectionScopeKey } from '../../results.js';
11
11
  import type { ProviderApiPagedResult, ProviderHierarchyResult } from '../models.js';
12
12
 
@@ -144,6 +144,11 @@ export async function collectProviderPagedResult<T>(
144
144
  } catch (ex) {
145
145
  if (isCancellationError(ex)) throw ex;
146
146
 
147
+ // A caller-contract error is not a fact about this scope, so it is never recorded as one — see
148
+ // `throwIfCallerContractError`. Checked alongside cancellation because both are errors that a
149
+ // per-scope failure would misdescribe.
150
+ throwIfCallerContractError(ex);
151
+
147
152
  // When the caller supplied a scope, preserve the items already fetched from that scope and record the
148
153
  // failure in collection metadata rather than re-throwing and discarding the prefix. Callers without a
149
154
  // scope keep the legacy throw behavior.
@@ -33,6 +33,12 @@ import { gitHostOnlySurfaceWarning, issuesUnsupportedWarning, noConnectionWarnin
33
33
  * `includeAllAssignees: true`, which resolves to no assignee constraint at all, so unassigned issues ARE
34
34
  * included. The equivalent is an OMITTED `relationships`, not `['any-assignee']`: `assignee:*` means "has some
35
35
  * assignee" and would silently exclude every unassigned issue, which is the opposite of broadening.
36
+ *
37
+ * Takes no `sort`, unlike `listIssuesPage` and `searchIssuesPage`. One logical page here spans several orgs, each
38
+ * at its own provider position in a cursor bundle, so honoring an order across them would need a k-way merge with
39
+ * a buffer per org rather than a sort of what arrived — the page is a slice of several independent walks, not a
40
+ * union of one round's results. `searchIssuesPage({ repos, criteria: { sort } })` answers the ordered version of
41
+ * this question for a caller that knows its repositories, and is already the recommended migration above.
36
42
  */
37
43
 
38
44
  export interface BroadenIssuesOptions {
@@ -44,7 +44,14 @@ export interface IssueCountScope {
44
44
  /** Repositories to count over. Combines with `org`, exactly as in `searchIssuesPage`. */
45
45
  repos?: ProviderReposInput;
46
46
  org?: string;
47
- /** Same criteria model as `searchIssuesPage`, validated against the same capability table. */
47
+ /**
48
+ * Same criteria model as `searchIssuesPage`, validated against the same capability table.
49
+ *
50
+ * `criteria.sort` does not affect the count — a total is invariant under ordering — but it is still VALIDATED,
51
+ * and an unsupported key refuses this scope exactly as it would refuse the read. That is the point of the
52
+ * count: it previews the constraints the read will apply, so one that accepted what the read refuses would
53
+ * promise a fetch that can't happen, which is worse than no preview at all.
54
+ */
48
55
  criteria?: IssueSearchCriteria;
49
56
  }
50
57