@gitkraken/core-gitlens 0.5.108 → 0.5.110

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 (190) hide show
  1. package/CHANGELOG.md +49 -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/models/pullRequest.d.ts +81 -0
  7. package/dist/git/models/pullRequest.d.ts.map +1 -1
  8. package/dist/git/models/pullRequest.js +15 -0
  9. package/dist/git/models/pullRequest.js.map +1 -1
  10. package/dist/git/utils/issue.utils.d.ts +41 -1
  11. package/dist/git/utils/issue.utils.d.ts.map +1 -1
  12. package/dist/git/utils/issue.utils.js +80 -0
  13. package/dist/git/utils/issue.utils.js.map +1 -1
  14. package/dist/git/utils/pullRequest.utils.d.ts +11 -1
  15. package/dist/git/utils/pullRequest.utils.d.ts.map +1 -1
  16. package/dist/git/utils/pullRequest.utils.js +40 -0
  17. package/dist/git/utils/pullRequest.utils.js.map +1 -1
  18. package/dist/plus/git-github/api/github.d.ts +70 -10
  19. package/dist/plus/git-github/api/github.d.ts.map +1 -1
  20. package/dist/plus/git-github/api/github.js +284 -31
  21. package/dist/plus/git-github/api/github.js.map +1 -1
  22. package/dist/plus/git-github/api/issueSearchQuery.d.ts +49 -5
  23. package/dist/plus/git-github/api/issueSearchQuery.d.ts.map +1 -1
  24. package/dist/plus/git-github/api/issueSearchQuery.js +64 -6
  25. package/dist/plus/git-github/api/issueSearchQuery.js.map +1 -1
  26. package/dist/plus/git-github/api/pullRequestSearchQuery.d.ts +27 -1
  27. package/dist/plus/git-github/api/pullRequestSearchQuery.d.ts.map +1 -1
  28. package/dist/plus/git-github/api/pullRequestSearchQuery.js +73 -6
  29. package/dist/plus/git-github/api/pullRequestSearchQuery.js.map +1 -1
  30. package/dist/plus/git-github/models.d.ts +17 -5
  31. package/dist/plus/git-github/models.d.ts.map +1 -1
  32. package/dist/plus/git-github/models.js +14 -1
  33. package/dist/plus/git-github/models.js.map +1 -1
  34. package/dist/plus/integrations/collectionMetadata.d.ts +18 -0
  35. package/dist/plus/integrations/collectionMetadata.d.ts.map +1 -1
  36. package/dist/plus/integrations/collectionMetadata.js +24 -0
  37. package/dist/plus/integrations/collectionMetadata.js.map +1 -1
  38. package/dist/plus/integrations/integrationService.d.ts +31 -10
  39. package/dist/plus/integrations/integrationService.d.ts.map +1 -1
  40. package/dist/plus/integrations/integrationService.js +9 -1
  41. package/dist/plus/integrations/integrationService.js.map +1 -1
  42. package/dist/plus/integrations/manager.d.ts +85 -31
  43. package/dist/plus/integrations/manager.d.ts.map +1 -1
  44. package/dist/plus/integrations/models/gitHostIntegration.d.ts +18 -31
  45. package/dist/plus/integrations/models/gitHostIntegration.d.ts.map +1 -1
  46. package/dist/plus/integrations/models/gitHostIntegration.js +29 -1
  47. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  48. package/dist/plus/integrations/models/integration.d.ts +1 -1
  49. package/dist/plus/integrations/models/integration.d.ts.map +1 -1
  50. package/dist/plus/integrations/models/integration.js.map +1 -1
  51. package/dist/plus/integrations/models/issueReads.d.ts +63 -1
  52. package/dist/plus/integrations/models/issueReads.d.ts.map +1 -1
  53. package/dist/plus/integrations/models/issuesIntegration.d.ts +7 -21
  54. package/dist/plus/integrations/models/issuesIntegration.d.ts.map +1 -1
  55. package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
  56. package/dist/plus/integrations/providerFilters.d.ts +2 -2
  57. package/dist/plus/integrations/providerFilters.d.ts.map +1 -1
  58. package/dist/plus/integrations/providers/accounts.d.ts +6 -0
  59. package/dist/plus/integrations/providers/accounts.d.ts.map +1 -0
  60. package/dist/plus/integrations/providers/accounts.js +30 -0
  61. package/dist/plus/integrations/providers/accounts.js.map +1 -0
  62. package/dist/plus/integrations/providers/azureDevOps.d.ts.map +1 -1
  63. package/dist/plus/integrations/providers/azureDevOps.js +1 -0
  64. package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
  65. package/dist/plus/integrations/providers/github.d.ts +9 -0
  66. package/dist/plus/integrations/providers/github.d.ts.map +1 -1
  67. package/dist/plus/integrations/providers/github.js +115 -135
  68. package/dist/plus/integrations/providers/github.js.map +1 -1
  69. package/dist/plus/integrations/providers/gitlab.d.ts +9 -0
  70. package/dist/plus/integrations/providers/gitlab.d.ts.map +1 -1
  71. package/dist/plus/integrations/providers/gitlab.js +24 -4
  72. package/dist/plus/integrations/providers/gitlab.js.map +1 -1
  73. package/dist/plus/integrations/providers/issueSorts.d.ts +36 -0
  74. package/dist/plus/integrations/providers/issueSorts.d.ts.map +1 -0
  75. package/dist/plus/integrations/providers/issueSorts.js +76 -0
  76. package/dist/plus/integrations/providers/issueSorts.js.map +1 -0
  77. package/dist/plus/integrations/providers/jira.d.ts +3 -9
  78. package/dist/plus/integrations/providers/jira.d.ts.map +1 -1
  79. package/dist/plus/integrations/providers/jira.js +5 -1
  80. package/dist/plus/integrations/providers/jira.js.map +1 -1
  81. package/dist/plus/integrations/providers/linear.d.ts +24 -9
  82. package/dist/plus/integrations/providers/linear.d.ts.map +1 -1
  83. package/dist/plus/integrations/providers/linear.js +63 -5
  84. package/dist/plus/integrations/providers/linear.js.map +1 -1
  85. package/dist/plus/integrations/providers/models.d.ts +76 -38
  86. package/dist/plus/integrations/providers/models.d.ts.map +1 -1
  87. package/dist/plus/integrations/providers/models.js +91 -105
  88. package/dist/plus/integrations/providers/models.js.map +1 -1
  89. package/dist/plus/integrations/providers/providersApi.d.ts +18 -3
  90. package/dist/plus/integrations/providers/providersApi.d.ts.map +1 -1
  91. package/dist/plus/integrations/providers/providersApi.js +24 -7
  92. package/dist/plus/integrations/providers/providersApi.js.map +1 -1
  93. package/dist/plus/integrations/providers/pullRequestReviews.d.ts +50 -0
  94. package/dist/plus/integrations/providers/pullRequestReviews.d.ts.map +1 -0
  95. package/dist/plus/integrations/providers/pullRequestReviews.js +99 -0
  96. package/dist/plus/integrations/providers/pullRequestReviews.js.map +1 -0
  97. package/dist/plus/integrations/providers/trello.d.ts +3 -9
  98. package/dist/plus/integrations/providers/trello.d.ts.map +1 -1
  99. package/dist/plus/integrations/providers/trello.js +1 -0
  100. package/dist/plus/integrations/providers/trello.js.map +1 -1
  101. package/dist/plus/integrations/providers/utils/providerPaging.d.ts.map +1 -1
  102. package/dist/plus/integrations/providers/utils/providerPaging.js +5 -1
  103. package/dist/plus/integrations/providers/utils/providerPaging.js.map +1 -1
  104. package/dist/plus/integrations/reads/broaden.d.ts +6 -0
  105. package/dist/plus/integrations/reads/broaden.d.ts.map +1 -1
  106. package/dist/plus/integrations/reads/broaden.js.map +1 -1
  107. package/dist/plus/integrations/reads/counts.d.ts +63 -1
  108. package/dist/plus/integrations/reads/counts.d.ts.map +1 -1
  109. package/dist/plus/integrations/reads/counts.js +146 -2
  110. package/dist/plus/integrations/reads/counts.js.map +1 -1
  111. package/dist/plus/integrations/reads/drains.d.ts +7 -1
  112. package/dist/plus/integrations/reads/drains.d.ts.map +1 -1
  113. package/dist/plus/integrations/reads/drains.js +13 -2
  114. package/dist/plus/integrations/reads/drains.js.map +1 -1
  115. package/dist/plus/integrations/reads/filters.d.ts +59 -9
  116. package/dist/plus/integrations/reads/filters.d.ts.map +1 -1
  117. package/dist/plus/integrations/reads/filters.js +39 -0
  118. package/dist/plus/integrations/reads/filters.js.map +1 -1
  119. package/dist/plus/integrations/reads/issueTracker.d.ts +18 -1
  120. package/dist/plus/integrations/reads/issueTracker.d.ts.map +1 -1
  121. package/dist/plus/integrations/reads/issueTracker.js +40 -3
  122. package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
  123. package/dist/plus/integrations/reads/issues.d.ts +27 -3
  124. package/dist/plus/integrations/reads/issues.d.ts.map +1 -1
  125. package/dist/plus/integrations/reads/issues.js +223 -123
  126. package/dist/plus/integrations/reads/issues.js.map +1 -1
  127. package/dist/plus/integrations/reads/ordering.d.ts +101 -0
  128. package/dist/plus/integrations/reads/ordering.d.ts.map +1 -0
  129. package/dist/plus/integrations/reads/ordering.js +48 -0
  130. package/dist/plus/integrations/reads/ordering.js.map +1 -0
  131. package/dist/plus/integrations/reads/searchIssues.d.ts +7 -2
  132. package/dist/plus/integrations/reads/searchIssues.d.ts.map +1 -1
  133. package/dist/plus/integrations/reads/searchIssues.js +9 -3
  134. package/dist/plus/integrations/reads/searchIssues.js.map +1 -1
  135. package/dist/plus/integrations/reads/searchPullRequests.d.ts +4 -3
  136. package/dist/plus/integrations/reads/searchPullRequests.d.ts.map +1 -1
  137. package/dist/plus/integrations/reads/searchPullRequests.js +6 -4
  138. package/dist/plus/integrations/reads/searchPullRequests.js.map +1 -1
  139. package/dist/plus/integrations/reads/sweeps.js +1 -1
  140. package/dist/plus/integrations/reads/sweeps.js.map +1 -1
  141. package/dist/plus/integrations/reads/warnings.d.ts +35 -3
  142. package/dist/plus/integrations/reads/warnings.d.ts.map +1 -1
  143. package/dist/plus/integrations/reads/warnings.js +58 -4
  144. package/dist/plus/integrations/reads/warnings.js.map +1 -1
  145. package/dist/plus/integrations/results.d.ts +15 -0
  146. package/dist/plus/integrations/results.d.ts.map +1 -1
  147. package/dist/plus/integrations/results.js.map +1 -1
  148. package/docs/integrations.md +76 -10
  149. package/docs/kepler-read-api-parity.md +9 -0
  150. package/package.json +2 -2
  151. package/src/git/models/issue.ts +73 -0
  152. package/src/git/models/pullRequest.ts +84 -0
  153. package/src/git/utils/issue.utils.ts +91 -1
  154. package/src/git/utils/pullRequest.utils.ts +50 -0
  155. package/src/plus/git-github/api/github.ts +356 -36
  156. package/src/plus/git-github/api/issueSearchQuery.ts +67 -7
  157. package/src/plus/git-github/api/pullRequestSearchQuery.ts +81 -7
  158. package/src/plus/git-github/models.ts +29 -5
  159. package/src/plus/integrations/collectionMetadata.ts +24 -0
  160. package/src/plus/integrations/integrationService.ts +40 -12
  161. package/src/plus/integrations/manager.ts +89 -35
  162. package/src/plus/integrations/models/gitHostIntegration.ts +47 -31
  163. package/src/plus/integrations/models/integration.ts +1 -0
  164. package/src/plus/integrations/models/issueReads.ts +65 -1
  165. package/src/plus/integrations/models/issuesIntegration.ts +7 -6
  166. package/src/plus/integrations/providerFilters.ts +8 -1
  167. package/src/plus/integrations/providers/accounts.ts +34 -0
  168. package/src/plus/integrations/providers/azureDevOps.ts +1 -0
  169. package/src/plus/integrations/providers/github.ts +134 -144
  170. package/src/plus/integrations/providers/gitlab.ts +29 -10
  171. package/src/plus/integrations/providers/issueSorts.ts +89 -0
  172. package/src/plus/integrations/providers/jira.ts +9 -3
  173. package/src/plus/integrations/providers/linear.ts +77 -8
  174. package/src/plus/integrations/providers/models.ts +185 -128
  175. package/src/plus/integrations/providers/providersApi.ts +51 -13
  176. package/src/plus/integrations/providers/pullRequestReviews.ts +122 -0
  177. package/src/plus/integrations/providers/trello.ts +4 -3
  178. package/src/plus/integrations/providers/utils/providerPaging.ts +6 -1
  179. package/src/plus/integrations/reads/broaden.ts +6 -0
  180. package/src/plus/integrations/reads/counts.ts +281 -3
  181. package/src/plus/integrations/reads/drains.ts +12 -2
  182. package/src/plus/integrations/reads/filters.ts +92 -8
  183. package/src/plus/integrations/reads/issueTracker.ts +78 -4
  184. package/src/plus/integrations/reads/issues.ts +386 -194
  185. package/src/plus/integrations/reads/ordering.ts +124 -0
  186. package/src/plus/integrations/reads/searchIssues.ts +15 -3
  187. package/src/plus/integrations/reads/searchPullRequests.ts +12 -4
  188. package/src/plus/integrations/reads/sweeps.ts +1 -1
  189. package/src/plus/integrations/reads/warnings.ts +84 -2
  190. package/src/plus/integrations/results.ts +15 -0
@@ -4,6 +4,7 @@ import type {
4
4
  IssueSearchCapabilities,
5
5
  IssueSearchCriteria,
6
6
  IssueSearchRelationship,
7
+ IssueSorting,
7
8
  PullRequestFilter,
8
9
  PullRequestSearchCapabilities,
9
10
  PullRequestSearchCriteria,
@@ -103,9 +104,25 @@ export function resolvePullRequestSearchCriteria(
103
104
  if (criteria.text != null && criteria.text.trim().length > 0 && !supported.text) {
104
105
  unsupported.push('text');
105
106
  }
107
+ if (criteria.updatedAfter != null && !supported.updatedAfter) {
108
+ unsupported.push('updatedAfter');
109
+ }
110
+ if (criteria.createdAfter != null && !supported.createdAfter) {
111
+ unsupported.push('createdAfter');
112
+ }
106
113
  if (criteria.includeArchived === true && !supported.includeArchived) {
107
114
  unsupported.push('includeArchived');
108
115
  }
116
+ // `!= null`, not truthy: `draft: false` (only ready-for-review) is as much a request as `draft: true`.
117
+ if (criteria.draft != null && !supported.draft) {
118
+ unsupported.push('draft');
119
+ }
120
+ // Folded into the same `unsupported-criteria` rejection rather than its own reason, so a caller asking for one
121
+ // inexpressible filter and one inexpressible sort learns about both at once. `updated:desc` is always in
122
+ // `sorts` for a usable search, so the omitted (default) case never rejects.
123
+ if (criteria.sort != null && !supported.sorts.includes(criteria.sort)) {
124
+ unsupported.push(`sort:${criteria.sort}`);
125
+ }
109
126
 
110
127
  return unsupported.length > 0 ? { rejection: { reason: 'unsupported-criteria', criteria: unsupported } } : {};
111
128
  }
@@ -237,6 +254,12 @@ export function resolveIssueSearchCriteria(
237
254
  if (criteria.state != null && criteria.state !== 'open' && !supported.states) {
238
255
  unsupported.push('state');
239
256
  }
257
+ // Folded into the same `unsupported-criteria` rejection rather than given its own reason, so a caller asking
258
+ // for one inexpressible filter and one inexpressible sort learns about both at once instead of fixing them one
259
+ // refusal at a time. The `sorts` list is `supported`'s own, so this needs no branch on which surface it is.
260
+ if (criteria.sort != null && !supported.sorts.includes(criteria.sort)) {
261
+ unsupported.push(`sort:${criteria.sort}`);
262
+ }
240
263
 
241
264
  if (unsupported.length > 0) return { rejection: { reason: 'unsupported-criteria', criteria: unsupported } };
242
265
 
@@ -264,6 +287,7 @@ const unsupportedIssueSearchCapabilities: IssueSearchCapabilities = {
264
287
  createdAfter: false,
265
288
  withoutLinkedPullRequest: false,
266
289
  states: false,
290
+ sorts: [],
267
291
  };
268
292
 
269
293
  /** What a provider with no filtered pull-request search reports. */
@@ -271,9 +295,13 @@ const unsupportedPullRequestSearchCapabilities: PullRequestSearchCapabilities =
271
295
  relationships: [],
272
296
  states: [],
273
297
  text: false,
298
+ updatedAfter: false,
299
+ createdAfter: false,
274
300
  includeArchived: false,
301
+ draft: false,
275
302
  repositoryScope: false,
276
303
  organizationScope: false,
304
+ sorts: [],
277
305
  };
278
306
 
279
307
  /** Why a filtered issue search's scope was refused, or `undefined` when it is usable. */
@@ -313,6 +341,57 @@ export function resolveIssueSearchScope(
313
341
  return { rejection: 'unscoped' };
314
342
  }
315
343
 
344
+ /**
345
+ * What {@link getSupportedFilters} reports: every vocabulary a provider's reads can express, so a consumer can
346
+ * narrow to it BEFORE issuing a read. See that function for the capability table itself and why intersecting
347
+ * against this is what keeps a filtered read from being refused.
348
+ *
349
+ * Named rather than written inline, because the same shape is the declared return type of THREE declarations that
350
+ * all describe one value — this function, `IntegrationService.getSupportedFilters`, which only forwards to it, and
351
+ * `IntegrationManager.getSupportedFilters`, which publishes it. Spelled out three times, a member added here
352
+ * reached one of them and left the others a compile error away from the truth; that is exactly how the two sort
353
+ * members below came to be written out three times.
354
+ */
355
+ export type SupportedFilters = {
356
+ pullRequests: PullRequestFilter[];
357
+ pullRequestsAccountWide: PullRequestFilter[];
358
+ /**
359
+ * Criteria and scopes the filtered pull-request search can express. Always present; an empty relationship list
360
+ * means the provider exposes no filtered pull-request search.
361
+ */
362
+ pullRequestSearch: PullRequestSearchCapabilities;
363
+ /**
364
+ * Filters the repo-scoped issue read accepts — and, for an issue tracker (Jira/Linear/Trello), its
365
+ * project-scoped read, that being its only issue surface. A tracker therefore reports here and leaves
366
+ * `issuesAccountWide` empty, so intersecting a tracker against that field would read "cannot filter" for a
367
+ * provider that filters fine.
368
+ */
369
+ issues: IssueFilter[];
370
+ issuesAccountWide: IssueFilter[];
371
+ /**
372
+ * What the filtered issue search (and the count probe over the same criteria) can express. Always present: a
373
+ * provider with no filtered issue search reports an empty `relationships` and all-false flags, which is the
374
+ * signal to hide that surface rather than to hide individual chips.
375
+ */
376
+ issueSearch: IssueSearchCapabilities;
377
+ /**
378
+ * Sort keys the repo-scoped issue read can express — and, for an issue tracker, its project-scoped read. Empty
379
+ * means the read can't be ordered, so pass no `sort`.
380
+ *
381
+ * A key here is expressible on ONE provider query. A page spanning several repositories or projects is a merge,
382
+ * and a merge can only order by what a normalized issue carries, so `priority`/`dueDate`/`resolved` are refused
383
+ * there even where they are listed. Read one scope at a time to use them.
384
+ */
385
+ issueSorts: IssueSorting[];
386
+ /**
387
+ * Sort keys the account-wide issue read can express — a different vocabulary, not a subset: for GitLab the two
388
+ * reads are different APIs. Needs no mergeability caveat, because every account-wide read is a union of several
389
+ * queries, so only keys a merge can honor are listed at all. Empty for an issue tracker, which reports under
390
+ * `issueSorts`.
391
+ */
392
+ issueSortsAccountWide: IssueSorting[];
393
+ };
394
+
316
395
  /**
317
396
  * The filters `listPullRequestsPage`/`listIssuesPage` (and the sweeps) accept for a provider, so a caller can
318
397
  * narrow to what the provider can express BEFORE issuing the read.
@@ -345,19 +424,19 @@ export function resolveIssueSearchScope(
345
424
  * `pullRequestSearch` describes the separate filtered PR search. Its table declares each criteria vocabulary
346
425
  * plus repository/organization scope support; an empty relationship list means the search itself is absent.
347
426
  *
427
+ * `issueSorts` / `issueSortsAccountWide` / `issueSearch.sorts` are the ORDERING vocabulary of those same three
428
+ * issue reads, split the same way and for the same reason (GitLab's repo-scoped read is GraphQL and its
429
+ * account-wide read is REST, with genuinely different sort vocabularies). Empty means the read can't be ordered,
430
+ * so pass no `sort`; a key not listed refuses the whole read exactly like an inexpressible filter. Note a key
431
+ * listed here is expressible on ONE provider query: a read that fans out across projects can only honor a key
432
+ * derivable from a normalized issue, so it refuses `priority`/`dueDate`/`resolved` on top of this table.
433
+ *
348
434
  * Note this is a CAPABILITY table — "what the provider can express" — not a recommendation. A consumer
349
435
  * matching another tool's behavior may deliberately pass fewer filters than are listed here (or none, where an
350
436
  * already-scoped read would only be narrowed by them). Intersecting against this table is what keeps a
351
437
  * filtered read from being refused; it isn't a directive to use every filter in it.
352
438
  */
353
- export function getSupportedFilters(providerId: IntegrationIds): {
354
- pullRequests: PullRequestFilter[];
355
- pullRequestsAccountWide: PullRequestFilter[];
356
- pullRequestSearch: PullRequestSearchCapabilities;
357
- issues: IssueFilter[];
358
- issuesAccountWide: IssueFilter[];
359
- issueSearch: IssueSearchCapabilities;
360
- } {
439
+ export function getSupportedFilters(providerId: IntegrationIds): SupportedFilters {
361
440
  const metadata = providersMetadata[providerId];
362
441
  const issueSearch = metadata?.supportedIssueSearch;
363
442
  const pullRequestSearch = metadata?.supportedPullRequestSearch;
@@ -369,6 +448,8 @@ export function getSupportedFilters(providerId: IntegrationIds): {
369
448
  ...pullRequestSearch,
370
449
  relationships: [...(pullRequestSearch?.relationships ?? [])],
371
450
  states: [...(pullRequestSearch?.states ?? [])],
451
+ // Copied, so mutating the result can't corrupt the metadata table.
452
+ sorts: [...(pullRequestSearch?.sorts ?? [])],
372
453
  },
373
454
  issues: [...(metadata?.supportedIssueFilters ?? [])],
374
455
  issuesAccountWide: [...(metadata?.supportedAccountWideIssueFilters ?? [])],
@@ -385,6 +466,9 @@ export function getSupportedFilters(providerId: IntegrationIds): {
385
466
  ...issueSearch,
386
467
  // Copied, so mutating the result can't corrupt the metadata table.
387
468
  relationships: [...(issueSearch?.relationships ?? [])],
469
+ sorts: [...(issueSearch?.sorts ?? [])],
388
470
  },
471
+ issueSorts: [...(metadata?.supportedIssueSorts ?? [])],
472
+ issueSortsAccountWide: [...(metadata?.supportedAccountWideIssueSorts ?? [])],
389
473
  };
390
474
  }
@@ -1,5 +1,5 @@
1
1
  import type { CollectionMetadata } from '@gitkraken/provider-apis';
2
- import type { IssueShape } from '../../../git/models/issue.js';
2
+ import type { IssueShape, IssueSorting } from '../../../git/models/issue.js';
3
3
  import type { ResourceDescriptor } from '../../../git/models/resourceDescriptor.js';
4
4
  import { mapBounded } from '../../../utils/promise.js';
5
5
  import { mergeAssessmentInto } from '../collectionMetadata.js';
@@ -15,7 +15,14 @@ import type { ProviderReadContext } from './context.js';
15
15
  import { parseIssueTrackerPageCursor, toIssueTrackerPageCursor } from './cursors.js';
16
16
  import { runCaptured } from './drains.js';
17
17
  import { projectKey, resourceIdForProject, resourceLabel, resourceMatchesOrg } from './hierarchy.utils.js';
18
- import { incompleteReadWarning, issueTrackerOnlySurfaceWarning, otherWarning } from './warnings.js';
18
+ import { resolveIssueSort, toIssueOrdering } from './ordering.js';
19
+ import {
20
+ incompleteReadWarning,
21
+ issueTrackerOnlySurfaceWarning,
22
+ otherWarning,
23
+ unmergeableIssueSortWarning,
24
+ unsupportedIssueSortWarning,
25
+ } from './warnings.js';
19
26
 
20
27
  export async function listIssueTrackerIssuesPage(
21
28
  ctx: ProviderReadContext,
@@ -26,6 +33,23 @@ export async function listIssueTrackerIssuesPage(
26
33
  filters?: IssueFilter[];
27
34
  /** Broadens the read to every assignee. Scopes to user-assigned issues when omitted. */
28
35
  includeAllAssignees?: boolean;
36
+ /**
37
+ * How to order the issues, as `field:direction`. Omitted orders most-recently-updated-first where the
38
+ * tracker can express it.
39
+ *
40
+ * Validated against `getSupportedFilters().issues`' sibling `issueSorts` — a tracker reports there, not
41
+ * under the account-wide table, because resource -> project IS its only issue surface. A key it can't
42
+ * express refuses the read rather than serving a differently-ordered list.
43
+ *
44
+ * One page can span SEVERAL projects, whose issues are merged here, so a key no normalized issue carries
45
+ * (`priority`, `dueDate`, `resolved`) is refused for a multi-project page even though the tracker orders by
46
+ * it perfectly well within one project.
47
+ *
48
+ * Unlike the git-host reads this one is safe to change mid-pagination: its cursor windows PROJECTS and
49
+ * drains each to exhaustion, so which projects a round covers doesn't depend on how their issues are
50
+ * ordered. Only the order within a page changes.
51
+ */
52
+ sort?: IssueSorting;
29
53
  forceSync?: boolean;
30
54
  page?: number;
31
55
  cursor?: string;
@@ -128,6 +152,19 @@ export async function listIssueTrackerIssuesPage(
128
152
 
129
153
  const domain = ctx.domainForRead(integration, options.providerId, options.connectionId);
130
154
 
155
+ // Before any upstream request: the discovery fan-outs below (resources, projects, per-resource accounts) are
156
+ // three round trips, and an order this tracker can't express refuses the read whatever they return. Placed after
157
+ // `domain` only because every warning carries it.
158
+ const resolvedSort = resolveIssueSort(providersMetadata[options.providerId]?.supportedIssueSorts, options.sort);
159
+ if (resolvedSort.rejection != null) {
160
+ warnings.push(
161
+ unsupportedIssueSortWarning(options.providerId, domain, options.connectionId, resolvedSort.rejection),
162
+ );
163
+ return emptyPage(true);
164
+ }
165
+
166
+ const sort = resolvedSort.sort;
167
+
131
168
  await ctx.forceRefreshIfRequested(integration, options.forceSync, options.connectionId);
132
169
 
133
170
  const { value: resources, warning: resourcesWarning } = await runCaptured(
@@ -362,6 +399,37 @@ export async function listIssueTrackerIssuesPage(
362
399
  });
363
400
  }
364
401
 
402
+ // A read spanning several projects is a merge, so it can only honor a key a normalized issue carries. Bound here
403
+ // rather than next to the key's validation above, because the project count is only known now — while still
404
+ // before any project read, so a refusal costs no request. Refused rather than served as concatenated
405
+ // per-project runs, which would look ordered without being so.
406
+ //
407
+ // Counted over every project the READ covers, not the `scopedProjects` window this page happens to hold: the
408
+ // window is `itemsPerPage` projects wide, so counting it would refuse a 20-project page and then serve the
409
+ // final 1-project page of the same paginated call — the same key answered two ways, with the earlier pages'
410
+ // issues missing from a read that reported success.
411
+ //
412
+ // Projects are not the only thing a tracker read merges: a user-scoped read asking for several relationships
413
+ // issues one provider query PER relationship and unions them within each project (Jira runs one JQL drain per
414
+ // filter and folds them into one map, in filter order), so a single-project page of two filters is just as
415
+ // concatenated as a two-project one. Counting only projects published those runs under the requested key.
416
+ // Not gated on the provider: the two trackers that don't fan out (Linear, Trello) declare `Assignee` as their
417
+ // only supported filter, so a multi-filter read is already refused above and can never reach this.
418
+ const mergesFilters = options.includeAllAssignees !== true && (options.filters?.length ?? 0) > 1;
419
+ const ordering = toIssueOrdering(sort, scopedProjectsWithUsers.length > 1 || mergesFilters);
420
+ if (ordering.unmergeable != null) {
421
+ warnings.push(
422
+ unmergeableIssueSortWarning(
423
+ options.providerId,
424
+ domain,
425
+ options.connectionId,
426
+ ordering.unmergeable,
427
+ scopedProjectsWithUsers.length > 1 ? 'projects' : 'filters',
428
+ ),
429
+ );
430
+ return emptyPage(true);
431
+ }
432
+
365
433
  const perProject = await mapBounded(scopedProjects, providerFanOutConcurrency, async project => ({
366
434
  project: project,
367
435
  ...(await runCaptured(options.providerId, domain, options.connectionId, () =>
@@ -370,6 +438,7 @@ export async function listIssueTrackerIssuesPage(
370
438
  {
371
439
  user: userForProject(project),
372
440
  filters: options.filters,
441
+ sort: sort,
373
442
  },
374
443
  options.connectionId,
375
444
  ),
@@ -417,6 +486,11 @@ export async function listIssueTrackerIssuesPage(
417
486
  }
418
487
  }
419
488
 
489
+ // Each provider query arrived ordered by the tracker; the union of them — across projects, and across the
490
+ // per-relationship queries a multi-filter read fans out into — is not, and the union is what this read
491
+ // publishes. A no-op when nothing merged, since the tracker already ordered that single run.
492
+ const orderedItems = ordering.order(items);
493
+
420
494
  const drainAssessment = mergeAssessmentInto(
421
495
  warnings,
422
496
  options.providerId,
@@ -463,9 +537,9 @@ export async function listIssueTrackerIssuesPage(
463
537
  : [],
464
538
  });
465
539
  return {
466
- items: items,
540
+ items: orderedItems,
467
541
  warnings: warnings,
468
- page: { currentPage: page, itemsPerPage: items.length, truncated: projectTruncated || undefined },
542
+ page: { currentPage: page, itemsPerPage: orderedItems.length, truncated: projectTruncated || undefined },
469
543
  // Failed-project retries alone are manual. Only an untouched project window is automatic progress.
470
544
  hasMore: nextPage != null && cursor != null,
471
545
  cursor: cursor,