@gitkraken/core-gitlens 0.5.108 → 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 (137) hide show
  1. package/CHANGELOG.md +17 -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/integrationService.d.ts +15 -9
  23. package/dist/plus/integrations/integrationService.d.ts.map +1 -1
  24. package/dist/plus/integrations/integrationService.js.map +1 -1
  25. package/dist/plus/integrations/manager.d.ts +40 -26
  26. package/dist/plus/integrations/manager.d.ts.map +1 -1
  27. package/dist/plus/integrations/models/gitHostIntegration.d.ts +6 -31
  28. package/dist/plus/integrations/models/gitHostIntegration.d.ts.map +1 -1
  29. package/dist/plus/integrations/models/gitHostIntegration.js +11 -1
  30. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  31. package/dist/plus/integrations/models/issueReads.d.ts +63 -1
  32. package/dist/plus/integrations/models/issueReads.d.ts.map +1 -1
  33. package/dist/plus/integrations/models/issuesIntegration.d.ts +7 -21
  34. package/dist/plus/integrations/models/issuesIntegration.d.ts.map +1 -1
  35. package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
  36. package/dist/plus/integrations/providerFilters.d.ts +1 -1
  37. package/dist/plus/integrations/providerFilters.d.ts.map +1 -1
  38. package/dist/plus/integrations/providers/azureDevOps.d.ts.map +1 -1
  39. package/dist/plus/integrations/providers/azureDevOps.js +1 -0
  40. package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
  41. package/dist/plus/integrations/providers/github.d.ts.map +1 -1
  42. package/dist/plus/integrations/providers/github.js +1 -0
  43. package/dist/plus/integrations/providers/github.js.map +1 -1
  44. package/dist/plus/integrations/providers/gitlab.d.ts.map +1 -1
  45. package/dist/plus/integrations/providers/gitlab.js +1 -0
  46. package/dist/plus/integrations/providers/gitlab.js.map +1 -1
  47. package/dist/plus/integrations/providers/issueSorts.d.ts +36 -0
  48. package/dist/plus/integrations/providers/issueSorts.d.ts.map +1 -0
  49. package/dist/plus/integrations/providers/issueSorts.js +76 -0
  50. package/dist/plus/integrations/providers/issueSorts.js.map +1 -0
  51. package/dist/plus/integrations/providers/jira.d.ts +3 -9
  52. package/dist/plus/integrations/providers/jira.d.ts.map +1 -1
  53. package/dist/plus/integrations/providers/jira.js +5 -1
  54. package/dist/plus/integrations/providers/jira.js.map +1 -1
  55. package/dist/plus/integrations/providers/linear.d.ts +24 -9
  56. package/dist/plus/integrations/providers/linear.d.ts.map +1 -1
  57. package/dist/plus/integrations/providers/linear.js +63 -5
  58. package/dist/plus/integrations/providers/linear.js.map +1 -1
  59. package/dist/plus/integrations/providers/models.d.ts +63 -7
  60. package/dist/plus/integrations/providers/models.d.ts.map +1 -1
  61. package/dist/plus/integrations/providers/models.js +55 -0
  62. package/dist/plus/integrations/providers/models.js.map +1 -1
  63. package/dist/plus/integrations/providers/providersApi.d.ts +18 -3
  64. package/dist/plus/integrations/providers/providersApi.d.ts.map +1 -1
  65. package/dist/plus/integrations/providers/providersApi.js +24 -7
  66. package/dist/plus/integrations/providers/providersApi.js.map +1 -1
  67. package/dist/plus/integrations/providers/trello.d.ts +3 -9
  68. package/dist/plus/integrations/providers/trello.d.ts.map +1 -1
  69. package/dist/plus/integrations/providers/trello.js +1 -0
  70. package/dist/plus/integrations/providers/trello.js.map +1 -1
  71. package/dist/plus/integrations/providers/utils/providerPaging.d.ts.map +1 -1
  72. package/dist/plus/integrations/providers/utils/providerPaging.js +5 -1
  73. package/dist/plus/integrations/providers/utils/providerPaging.js.map +1 -1
  74. package/dist/plus/integrations/reads/broaden.d.ts +6 -0
  75. package/dist/plus/integrations/reads/broaden.d.ts.map +1 -1
  76. package/dist/plus/integrations/reads/broaden.js.map +1 -1
  77. package/dist/plus/integrations/reads/counts.d.ts +8 -1
  78. package/dist/plus/integrations/reads/counts.d.ts.map +1 -1
  79. package/dist/plus/integrations/reads/counts.js.map +1 -1
  80. package/dist/plus/integrations/reads/filters.d.ts +59 -9
  81. package/dist/plus/integrations/reads/filters.d.ts.map +1 -1
  82. package/dist/plus/integrations/reads/filters.js +17 -0
  83. package/dist/plus/integrations/reads/filters.js.map +1 -1
  84. package/dist/plus/integrations/reads/issueTracker.d.ts +18 -1
  85. package/dist/plus/integrations/reads/issueTracker.d.ts.map +1 -1
  86. package/dist/plus/integrations/reads/issueTracker.js +40 -3
  87. package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
  88. package/dist/plus/integrations/reads/issues.d.ts +27 -3
  89. package/dist/plus/integrations/reads/issues.d.ts.map +1 -1
  90. package/dist/plus/integrations/reads/issues.js +223 -123
  91. package/dist/plus/integrations/reads/issues.js.map +1 -1
  92. package/dist/plus/integrations/reads/ordering.d.ts +101 -0
  93. package/dist/plus/integrations/reads/ordering.d.ts.map +1 -0
  94. package/dist/plus/integrations/reads/ordering.js +48 -0
  95. package/dist/plus/integrations/reads/ordering.js.map +1 -0
  96. package/dist/plus/integrations/reads/searchIssues.d.ts +7 -2
  97. package/dist/plus/integrations/reads/searchIssues.d.ts.map +1 -1
  98. package/dist/plus/integrations/reads/searchIssues.js +9 -3
  99. package/dist/plus/integrations/reads/searchIssues.js.map +1 -1
  100. package/dist/plus/integrations/reads/warnings.d.ts +34 -2
  101. package/dist/plus/integrations/reads/warnings.d.ts.map +1 -1
  102. package/dist/plus/integrations/reads/warnings.js +47 -2
  103. package/dist/plus/integrations/reads/warnings.js.map +1 -1
  104. package/dist/plus/integrations/results.d.ts +10 -0
  105. package/dist/plus/integrations/results.d.ts.map +1 -1
  106. package/dist/plus/integrations/results.js.map +1 -1
  107. package/package.json +2 -2
  108. package/src/git/models/issue.ts +73 -0
  109. package/src/git/utils/issue.utils.ts +91 -1
  110. package/src/plus/git-github/api/github.ts +180 -20
  111. package/src/plus/git-github/api/issueSearchQuery.ts +67 -7
  112. package/src/plus/integrations/collectionMetadata.ts +24 -0
  113. package/src/plus/integrations/integrationService.ts +15 -10
  114. package/src/plus/integrations/manager.ts +40 -31
  115. package/src/plus/integrations/models/gitHostIntegration.ts +18 -31
  116. package/src/plus/integrations/models/issueReads.ts +65 -1
  117. package/src/plus/integrations/models/issuesIntegration.ts +7 -6
  118. package/src/plus/integrations/providerFilters.ts +2 -0
  119. package/src/plus/integrations/providers/azureDevOps.ts +1 -0
  120. package/src/plus/integrations/providers/github.ts +1 -0
  121. package/src/plus/integrations/providers/gitlab.ts +1 -0
  122. package/src/plus/integrations/providers/issueSorts.ts +89 -0
  123. package/src/plus/integrations/providers/jira.ts +9 -3
  124. package/src/plus/integrations/providers/linear.ts +77 -8
  125. package/src/plus/integrations/providers/models.ts +122 -5
  126. package/src/plus/integrations/providers/providersApi.ts +51 -13
  127. package/src/plus/integrations/providers/trello.ts +4 -3
  128. package/src/plus/integrations/providers/utils/providerPaging.ts +6 -1
  129. package/src/plus/integrations/reads/broaden.ts +6 -0
  130. package/src/plus/integrations/reads/counts.ts +8 -1
  131. package/src/plus/integrations/reads/filters.ts +70 -8
  132. package/src/plus/integrations/reads/issueTracker.ts +78 -4
  133. package/src/plus/integrations/reads/issues.ts +386 -194
  134. package/src/plus/integrations/reads/ordering.ts +124 -0
  135. package/src/plus/integrations/reads/searchIssues.ts +15 -3
  136. package/src/plus/integrations/reads/warnings.ts +72 -1
  137. package/src/plus/integrations/results.ts +10 -0
@@ -1,7 +1,7 @@
1
1
  import type { CollectionMetadata, CollectionScopeFailure } from '@gitkraken/provider-apis';
2
2
  import type { Account, UnidentifiedAuthor } from '../../../git/models/author.js';
3
3
  import type { DefaultBranch } from '../../../git/models/defaultBranch.js';
4
- import type { IssueSearchCriteria, IssueShape, IssueStateFilter } from '../../../git/models/issue.js';
4
+ import type { IssueSearchCriteria, IssueShape } from '../../../git/models/issue.js';
5
5
  import type { IssueOrPullRequestState as PullRequestState } from '../../../git/models/issueOrPullRequest.js';
6
6
  import type {
7
7
  PullRequest,
@@ -20,7 +20,7 @@ import { getScopedLogger } from '../../../utils/logger.scoped.js';
20
20
  import type { PagedResult } from '../../../utils/paging.js';
21
21
  import type { ProviderAuthenticationSession } from '../authentication/models.js';
22
22
  import { toTokenWithInfo } from '../authentication/models.js';
23
- import { toCollectionScopeFailure } from '../collectionMetadata.js';
23
+ import { throwIfCallerContractError, toCollectionScopeFailure } from '../collectionMetadata.js';
24
24
  import type { IntegrationIds } from '../constants.js';
25
25
  import { GitCloudHostIntegrationId, GitSelfManagedHostIntegrationId } from '../constants.js';
26
26
  import { toError } from '../errors.js';
@@ -56,6 +56,7 @@ import type {
56
56
  ProviderPullRequestSearchPage,
57
57
  } from './integration.js';
58
58
  import { IntegrationBase } from './integration.js';
59
+ import type { MyIssuesForReposOptions } from './issueReads.js';
59
60
 
60
61
  function isAzureDevOpsProvider(
61
62
  providerId: IntegrationIds,
@@ -673,17 +674,7 @@ export abstract class GitHostIntegration<
673
674
 
674
675
  async getMyIssuesForRepos(
675
676
  reposOrRepoIds: ProviderReposInput,
676
- options?: {
677
- filters?: IssueFilter[];
678
- cursor?: string;
679
- customUrl?: string;
680
- page?: number;
681
- pageSize?: number;
682
- /** When true, don't constrain to the current user's assigned issues even if the Assignee filter is set. */
683
- includeAllAssignees?: boolean;
684
- /** Issue states to include; when omitted the provider returns its default (open only). */
685
- state?: IssueStateFilter;
686
- },
677
+ options?: MyIssuesForReposOptions,
687
678
  connectionId?: string,
688
679
  ): Promise<PagedResult<ProviderIssue> | undefined> {
689
680
  return (await this.getMyIssuesForReposResult(reposOrRepoIds, options, connectionId))?.value;
@@ -697,15 +688,7 @@ export abstract class GitHostIntegration<
697
688
  */
698
689
  async getMyIssuesForReposResult(
699
690
  reposOrRepoIds: ProviderReposInput,
700
- options?: {
701
- filters?: IssueFilter[];
702
- cursor?: string;
703
- customUrl?: string;
704
- page?: number;
705
- pageSize?: number;
706
- includeAllAssignees?: boolean;
707
- state?: IssueStateFilter;
708
- },
691
+ options?: MyIssuesForReposOptions,
709
692
  connectionId?: string,
710
693
  ): Promise<IntegrationResult<(PagedResult<ProviderIssue> & { metadata?: CollectionMetadata }) | undefined>> {
711
694
  const scope = getScopedLogger();
@@ -828,6 +811,7 @@ export abstract class GitHostIntegration<
828
811
  page: projectInput.cursor == null ? options?.page : undefined,
829
812
  pageSize: options?.pageSize,
830
813
  states: states,
814
+ sort: options?.sort,
831
815
  },
832
816
  );
833
817
  return { projectInput: projectInput, results: results };
@@ -838,6 +822,10 @@ export abstract class GitHostIntegration<
838
822
  const outcome = settled[i];
839
823
  const projectInput = projectInputs[i];
840
824
  if (outcome.status !== 'fulfilled') {
825
+ // Errors that really are per-scope (auth, rate limit, a missing project) degrade below; one
826
+ // that is a fact about the call is re-thrown instead — see `throwIfCallerContractError`.
827
+ throwIfCallerContractError(outcome.reason);
828
+
841
829
  truncated = true;
842
830
  const failure = toCollectionScopeFailure(
843
831
  {
@@ -963,6 +951,7 @@ export abstract class GitHostIntegration<
963
951
  page: repoInput.cursor == null ? options?.page : undefined,
964
952
  pageSize: options?.pageSize,
965
953
  states: states,
954
+ sort: options?.sort,
966
955
  },
967
956
  );
968
957
  return { repoInput: repoInput, results: results };
@@ -973,6 +962,11 @@ export abstract class GitHostIntegration<
973
962
  const outcome = settled[i];
974
963
  const repoInput = repoInputs[i];
975
964
  if (outcome.status !== 'fulfilled') {
965
+ // See the project fan-out above. Reachable in practice even though the facade validates the
966
+ // key first — a self-managed GitLab can reject an `IssueSort` member the SDK declares, which
967
+ // is only discoverable from the response.
968
+ throwIfCallerContractError(outcome.reason);
969
+
976
970
  truncated = true;
977
971
  const failure = toCollectionScopeFailure(
978
972
  {
@@ -1030,6 +1024,7 @@ export abstract class GitHostIntegration<
1030
1024
  page: options?.page,
1031
1025
  pageSize: options?.pageSize,
1032
1026
  states: states,
1027
+ sort: options?.sort,
1033
1028
  });
1034
1029
  return { value: result, duration: performance.now() - start };
1035
1030
  } catch (ex) {
@@ -1050,15 +1045,7 @@ export abstract class GitHostIntegration<
1050
1045
  */
1051
1046
  async getMyIssuesForReposAsShapesResult(
1052
1047
  reposOrRepoIds: ProviderReposInput,
1053
- options?: {
1054
- filters?: IssueFilter[];
1055
- cursor?: string;
1056
- customUrl?: string;
1057
- page?: number;
1058
- pageSize?: number;
1059
- includeAllAssignees?: boolean;
1060
- state?: IssueStateFilter;
1061
- },
1048
+ options?: MyIssuesForReposOptions,
1062
1049
  connectionId?: string,
1063
1050
  ): Promise<IntegrationResult<(PagedResult<IssueShape> & { metadata?: CollectionMetadata }) | undefined>> {
1064
1051
  const result = await this.getMyIssuesForReposResult(reposOrRepoIds, options, connectionId);
@@ -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, IssueStateFilter } from '../../../git/models/issue.js';
3
3
  import type { IssueFilter } from '../providerFilters.js';
4
4
 
5
5
  /**
@@ -42,6 +42,58 @@ export type ProviderIssueSearchPage = {
42
42
  totalCount?: number;
43
43
  };
44
44
 
45
+ /**
46
+ * Options for an issue tracker's PROJECT-scoped read (`IssuesIntegration.getIssuesForProject*`), which is the
47
+ * only issue surface a tracker has — its issues live under resource -> project, so there is no account-wide read
48
+ * to pair this with.
49
+ *
50
+ * Named rather than repeated inline at each of the six declarations this shape appears in (the two public
51
+ * `Result` methods, the two protected provider cores, and their Jira/Linear/Trello overrides), so a field added
52
+ * here can't reach five of them and be forgotten in the sixth.
53
+ */
54
+ export type IssuesForProjectOptions = {
55
+ /** The account handle to scope to, resolved per resource by the caller. Omitted reads every assignee. */
56
+ user?: string;
57
+ /** Validated by the caller against `ProviderMetadata.supportedIssueFilters`; unsupported refuses the read. */
58
+ filters?: IssueFilter[];
59
+ /**
60
+ * How the provider should order the project's issues, as `field:direction`.
61
+ *
62
+ * Validated against `ProviderMetadata.supportedIssueSorts` — a tracker reports there, not under the
63
+ * account-wide table. Note the caller reads MANY projects and merges them, so it additionally refuses a key
64
+ * no normalized issue carries; this option orders one project's query.
65
+ */
66
+ sort?: IssueSorting;
67
+ };
68
+
69
+ /**
70
+ * Options for the REPO-scoped issue read (`GitHostIntegration.getMyIssuesForRepos*`), the git-host counterpart of
71
+ * {@link IssuesForProjectOptions}.
72
+ *
73
+ * Named for the same reason that one is: the shape appears at three declarations — the public method, its
74
+ * result-returning core, and the shapes-returning core that maps it — and was previously spelled out inline at
75
+ * each, so `sort` had to be added three times and two of the copies grew a `See {@link getMyIssuesForRepos}`
76
+ * breadcrumb to admit they were the same contract. One name means a field added here reaches all three.
77
+ */
78
+ export type MyIssuesForReposOptions = {
79
+ /** Validated by the caller against `ProviderMetadata.supportedIssueFilters`; unsupported refuses the read. */
80
+ filters?: IssueFilter[];
81
+ cursor?: string;
82
+ customUrl?: string;
83
+ page?: number;
84
+ pageSize?: number;
85
+ /** When true, don't constrain to the current user's assigned issues even if the Assignee filter is set. */
86
+ includeAllAssignees?: boolean;
87
+ /** Issue states to include; when omitted the provider returns its default (open only). */
88
+ state?: IssueStateFilter;
89
+ /**
90
+ * How the provider should order the read. Forwarded to every query this read issues, so the ORDER WITHIN each
91
+ * scope is the provider's; the union across scopes is not ordered here — the facade (`listIssuesPage`) owns
92
+ * that, because it is also the layer that can refuse a key no merge can honor.
93
+ */
94
+ sort?: IssueSorting;
95
+ };
96
+
45
97
  /**
46
98
  * Options for the account-wide issue read. `includeAllAssignees` drops the "assigned to me" scoping so the
47
99
  * read broadens to issues assigned to anyone (the account-wide equivalent of the repo-scoped
@@ -70,4 +122,16 @@ export type SearchMyIssuesOptions = {
70
122
  */
71
123
  org?: string;
72
124
  project?: string;
125
+ /**
126
+ * How the provider should order the read, as `field:direction`.
127
+ *
128
+ * Validated by the facade against `ProviderMetadata.supportedAccountWideIssueSorts` — a DIFFERENT table from
129
+ * the repo-scoped one, because for GitLab the two reads are different APIs with different sort vocabularies —
130
+ * and refused rather than downgraded when the provider can't express it.
131
+ *
132
+ * Supplied on every facade read, never omitted: an omitted sort leaves each provider's own default, which is
133
+ * exactly the cross-provider incoherence this layer exists to remove. A provider that reaches this with
134
+ * `undefined` was called directly, not through the facade, and keeps whatever order it had.
135
+ */
136
+ sort?: IssueSorting;
73
137
  };
@@ -8,9 +8,10 @@ import { getScopedLogger } from '../../../utils/logger.scoped.js';
8
8
  import type { ProviderAuthenticationSession } from '../authentication/models.js';
9
9
  import type { IntegrationIds } from '../constants.js';
10
10
  import { toError } from '../errors.js';
11
- import type { IssueFilter, ProviderApiCollectionResult } from '../providers/models.js';
11
+ import type { ProviderApiCollectionResult } from '../providers/models.js';
12
12
  import type { Integration, IntegrationResult, IntegrationType } from './integration.js';
13
13
  import { IntegrationBase } from './integration.js';
14
+ import type { IssuesForProjectOptions } from './issueReads.js';
14
15
 
15
16
  export function isIssuesIntegration(integration: Integration): integration is IssuesIntegration {
16
17
  return integration.type === 'issues';
@@ -135,7 +136,7 @@ export abstract class IssuesIntegration<
135
136
  @trace()
136
137
  async getIssuesForProject(
137
138
  project: T,
138
- options?: { user?: string; filters?: IssueFilter[] },
139
+ options?: IssuesForProjectOptions,
139
140
  connectionId?: string,
140
141
  ): Promise<IssueShape[] | undefined> {
141
142
  return (await this.getIssuesForProjectResult(project, options, connectionId))?.value;
@@ -148,7 +149,7 @@ export abstract class IssuesIntegration<
148
149
  */
149
150
  async getIssuesForProjectResult(
150
151
  project: T,
151
- options?: { user?: string; filters?: IssueFilter[] },
152
+ options?: IssuesForProjectOptions,
152
153
  connectionId?: string,
153
154
  ): Promise<IntegrationResult<IssueShape[] | undefined>> {
154
155
  const scope = getScopedLogger();
@@ -174,7 +175,7 @@ export abstract class IssuesIntegration<
174
175
  */
175
176
  async getIssuesForProjectWithTruncationResult(
176
177
  project: T,
177
- options?: { user?: string; filters?: IssueFilter[] },
178
+ options?: IssuesForProjectOptions,
178
179
  connectionId?: string,
179
180
  ): Promise<
180
181
  IntegrationResult<{ values: IssueShape[]; truncated: boolean; metadata?: CollectionMetadata } | undefined>
@@ -196,7 +197,7 @@ export abstract class IssuesIntegration<
196
197
  protected abstract getProviderIssuesForProject(
197
198
  session: ProviderAuthenticationSession,
198
199
  project: T,
199
- options?: { user?: string; filters?: IssueFilter[] },
200
+ options?: IssuesForProjectOptions,
200
201
  ): Promise<IssueShape[] | undefined>;
201
202
 
202
203
  /**
@@ -208,7 +209,7 @@ export abstract class IssuesIntegration<
208
209
  protected async getProviderIssuesForProjectWithTruncation(
209
210
  session: ProviderAuthenticationSession,
210
211
  project: T,
211
- options?: { user?: string; filters?: IssueFilter[] },
212
+ options?: IssuesForProjectOptions,
212
213
  ): Promise<{ values: IssueShape[]; truncated: boolean; metadata?: CollectionMetadata } | undefined> {
213
214
  const values = await this.getProviderIssuesForProject(session, project, options);
214
215
  if (values == null) return undefined;
@@ -19,6 +19,8 @@ export type {
19
19
  IssueSearchCapabilities,
20
20
  IssueSearchCriteria,
21
21
  IssueSearchRelationship,
22
+ IssueSortField,
23
+ IssueSorting,
22
24
  } from '../../git/models/issue.js';
23
25
 
24
26
  /**
@@ -1057,6 +1057,7 @@ export abstract class AzureDevOpsIntegrationBase<
1057
1057
  ...options,
1058
1058
  ...filter,
1059
1059
  cursor: cursor,
1060
+ sort: searchOptions?.sort,
1060
1061
  }),
1061
1062
  20,
1062
1063
  { providerId: this.id, resourceId: p.resourceId, projectId: p.name },
@@ -618,6 +618,7 @@ abstract class GitHubIntegrationBase<ID extends GitHubIntegrationIds> extends Gi
618
618
  includeBody: true,
619
619
  includeAllAssignees: options?.includeAllAssignees,
620
620
  cursor: options?.cursor,
621
+ sort: options?.sort,
621
622
  categories: options?.filters?.length
622
623
  ? {
623
624
  authored: options.filters.includes(IssueFilter.Author),
@@ -668,6 +668,7 @@ abstract class GitLabIntegrationBase<ID extends GitLabIntegrationIds> extends Gi
668
668
  isPAT: this.isEnterprise,
669
669
  baseUrl: baseUrl,
670
670
  cursor: cursor,
671
+ sort: options?.sort,
671
672
  });
672
673
  if (cancellation?.aborted) throw new CancellationError();
673
674
  return page;
@@ -0,0 +1,89 @@
1
+ import { SUPPORTED_ISSUE_SORTS } from '@gitkraken/provider-apis';
2
+ import { getIssueComparator } from '../../../git/utils/issue.utils.js';
3
+ import type { IssueSorting } from '../providerFilters.js';
4
+
5
+ /**
6
+ * The per-provider ordering vocabulary of the issue reads: which neutral `field:direction` keys each provider
7
+ * surface can express SERVER-SIDE.
8
+ *
9
+ * DERIVED from `@gitkraken/provider-apis`, not transcribed from it. The SDK is where a neutral key becomes a
10
+ * provider query, so it is the only place that can say what a provider accepts; a copy kept here would be a
11
+ * second hand-maintained table on a second release cadence, and the symptom of drift is not a failed build but a
12
+ * read this facade promised and the SDK then rejects at runtime — which is exactly what happened while these were
13
+ * transcribed, on the one key (`reactions`) whose meaning is narrower than its name.
14
+ *
15
+ * Why the asymmetries between surfaces exist (GitHub cannot order by close date, GitLab's two reads are different
16
+ * APIs with different vocabularies, Azure has no `dueDate` outside the Agile process, Trello has only its
17
+ * `sort:edited` modifier, Jira has votes rather than reactions) is documented on the SDK's own maps. This module
18
+ * adds exactly one rule of its own — {@link mergeableSorts} — and everything else is a rename from the SDK's
19
+ * surface names to the two fields `ProviderMetadata` publishes.
20
+ *
21
+ * A caveat for anything building ONE ordering control across providers: a neutral key does not always mean the
22
+ * same thing. `priority:desc` is most-important-first on Jira and Linear, and least-important-first on Azure
23
+ * DevOps, whose WIQL orders the raw numeric column where 1 is the highest priority — so no single direction
24
+ * means "highest priority first" everywhere, and a unified control has to pick the direction per provider.
25
+ * `title` likewise orders by each backing database's collation, never JavaScript's.
26
+ */
27
+
28
+ /** One SDK read surface, widened to the union this package uses (structurally the same `field:direction` type). */
29
+ const surface = (name: keyof typeof SUPPORTED_ISSUE_SORTS): IssueSorting[] => [...SUPPORTED_ISSUE_SORTS[name]];
30
+
31
+ /**
32
+ * The subset a MERGED page can honor: the keys a normalized issue carries.
33
+ *
34
+ * Applied to the account-wide surfaces because every account-wide read is a union of several provider queries
35
+ * merged in this facade — GitHub's three `@me` searches, GitLab's one REST call per relationship, Azure's
36
+ * (project × relationship) drains — and the caller has no scope count to reduce, so it ALWAYS merges. GitLab's
37
+ * REST endpoint really does order by `priority` and `dueDate`, and Azure's WIQL by `resolved` and `priority`;
38
+ * none of them survive a merge, so the surface cannot honor them and must not advertise them.
39
+ *
40
+ * Expressed as the rule rather than as a hand-removed list, so a key the SDK adds later is classified by the same
41
+ * predicate that will decide at read time whether the merge can order by it, instead of by whoever last edited a
42
+ * literal here. The repo-scoped surfaces are NOT filtered: those merge only when the caller passes several
43
+ * scopes, which is a property of the call rather than of the provider (see `mergesProviderQueries`).
44
+ */
45
+ const mergeableSorts = (sorts: readonly IssueSorting[]): IssueSorting[] =>
46
+ sorts.filter(sort => getIssueComparator(sort) != null);
47
+
48
+ /** GitHub and GHE: one `search` channel serves all three issue reads, so all three order the same way. */
49
+ export const githubIssueSorts: IssueSorting[] = surface('github');
50
+
51
+ /**
52
+ * The same `search` channel, narrowed to what the three `@me` searches can order once this facade has merged them.
53
+ *
54
+ * Identical to {@link githubIssueSorts} today — every qualifier the SDK's GitHub surface carries is a field an
55
+ * `IssueShape` models — so this exists for the drift, not for a difference: `readAccountWideIssuesPage`
56
+ * deliberately runs no `unmergeable` check, on the stated grounds that an account-wide table only lists keys a
57
+ * merge can honor. Left as the raw surface, one qualifier added upstream (a `closed` or `interactions` sort) would
58
+ * make that assumption false for GitHub alone and publish three concatenated alias runs under it, unwarned.
59
+ */
60
+ export const githubAccountWideIssueSorts: IssueSorting[] = mergeableSorts(surface('github'));
61
+
62
+ /** GitLab's repository-scoped read, which goes through GraphQL `IssueSort`. */
63
+ export const gitlabIssueSorts: IssueSorting[] = surface('gitlabRepository');
64
+
65
+ /** GitLab's account-wide read: the REST `order_by`/`sort` pair, narrowed to what its merge can order. */
66
+ export const gitlabAccountWideIssueSorts: IssueSorting[] = mergeableSorts(surface('gitlabAccountWide'));
67
+
68
+ /** Azure DevOps, as WIQL `ORDER BY` columns. */
69
+ export const azureIssueSorts: IssueSorting[] = surface('azureDevOps');
70
+
71
+ /** The same WIQL, narrowed to what the per-project fan-out can order once it has merged. */
72
+ export const azureAccountWideIssueSorts: IssueSorting[] = mergeableSorts(surface('azureDevOps'));
73
+
74
+ /** Jira Cloud and Server, as JQL `ORDER BY` fields — one JQL builder, so one surface. */
75
+ export const jiraIssueSorts: IssueSorting[] = surface('jira');
76
+
77
+ /**
78
+ * Linear's one root `issues` query, which both of its reads share and which takes the same `sort` argument in
79
+ * either direction — so this is one surface, not a repo-scoped and an account-wide pair.
80
+ *
81
+ * NOT filtered by {@link mergeableSorts} even though the account-wide read is "my issues": that read is a
82
+ * single server-ordered query (one `or` filter over the four relationships), so nothing is merged in this
83
+ * facade and there is no comparator to satisfy. Filtering it would drop `priority` and `dueDate` — keys the
84
+ * server really does order by — for a merge that does not happen.
85
+ */
86
+ export const linearIssueSorts: IssueSorting[] = surface('linear');
87
+
88
+ /** Trello's search modifiers: `sort:edited` / `sort:-edited`, and nothing else usable for issues. */
89
+ export const trelloIssueSorts: IssueSorting[] = surface('trello');
@@ -8,8 +8,9 @@ import { Logger } from '../../../utils/logger.js';
8
8
  import type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider.js';
9
9
  import type { ProviderAuthenticationSession } from '../authentication/models.js';
10
10
  import { toTokenWithInfo } from '../authentication/models.js';
11
- import { toCollectionScopeFailure } from '../collectionMetadata.js';
11
+ import { throwIfCallerContractError, toCollectionScopeFailure } from '../collectionMetadata.js';
12
12
  import { IssuesCloudHostIntegrationId } from '../constants.js';
13
+ import type { IssuesForProjectOptions } from '../models/issueReads.js';
13
14
  import { IssuesIntegration } from '../models/issuesIntegration.js';
14
15
  import type { ProviderApiCollectionResult, ProviderIssue } from './models.js';
15
16
  import { IssueFilter, providersMetadata, toAccount, toIssueShape } from './models.js';
@@ -234,7 +235,7 @@ export class JiraIntegration extends IssuesIntegration<IssuesCloudHostIntegratio
234
235
  protected override async getProviderIssuesForProject(
235
236
  session: ProviderAuthenticationSession,
236
237
  project: JiraProjectDescriptor,
237
- options?: { user?: string; filters?: IssueFilter[] },
238
+ options?: IssuesForProjectOptions,
238
239
  ): Promise<IssueShape[] | undefined> {
239
240
  return (await this.getProviderIssuesForProjectWithTruncation(session, project, options))?.values;
240
241
  }
@@ -242,7 +243,7 @@ export class JiraIntegration extends IssuesIntegration<IssuesCloudHostIntegratio
242
243
  protected override async getProviderIssuesForProjectWithTruncation(
243
244
  session: ProviderAuthenticationSession,
244
245
  project: JiraProjectDescriptor,
245
- options?: { user?: string; filters?: IssueFilter[] },
246
+ options?: IssuesForProjectOptions,
246
247
  ): Promise<{ values: IssueShape[]; truncated: boolean; metadata?: CollectionMetadata } | undefined> {
247
248
  const tokenWithInfo = toTokenWithInfo(this.id, session);
248
249
 
@@ -273,6 +274,7 @@ export class JiraIntegration extends IssuesIntegration<IssuesCloudHostIntegratio
273
274
  result = await api.getIssuesForProjectPaged(tokenWithInfo, project.name, project.resourceId, {
274
275
  ...scope,
275
276
  cursor: cursor,
277
+ sort: options?.sort,
276
278
  });
277
279
  } catch (ex) {
278
280
  // A page failure after the first page leaves the already-drained prefix intact; record the
@@ -390,6 +392,10 @@ export class JiraIntegration extends IssuesIntegration<IssuesCloudHostIntegratio
390
392
  // incomplete: keep the sibling results but record a structured failure so the facade can warn on
391
393
  // the specific filter (auth/rate-limit) instead of just a generic truncation flag.
392
394
  if (outcome.status !== 'fulfilled') {
395
+ // Identical for every filter branch, so degrading it would report one failure per branch for a
396
+ // single invalid call — see `throwIfCallerContractError`.
397
+ throwIfCallerContractError(outcome.reason);
398
+
393
399
  truncated = true;
394
400
  const failure = toCollectionScopeFailure(
395
401
  { providerId: this.id, resourceId: project.resourceId, projectId: project.name },
@@ -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,