@gitkraken/core-gitlens 0.9.2 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -1
- package/dist/plus/git-github/api/github.js +1 -1
- package/dist/plus/git-github/api/github.js.map +1 -1
- package/dist/plus/integrations/integrationService.d.ts +8 -2
- package/dist/plus/integrations/integrationService.js +3 -1
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/manager.d.ts +6 -1
- package/dist/plus/integrations/models/integration.d.ts +8 -0
- package/dist/plus/integrations/models/integration.js +9 -0
- package/dist/plus/integrations/models/integration.js.map +1 -1
- package/dist/plus/integrations/models/issueReads.d.ts +14 -0
- package/dist/plus/integrations/providers/azure/azure.d.ts +3 -0
- package/dist/plus/integrations/providers/azure/azure.js +18 -0
- package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
- package/dist/plus/integrations/providers/azure/azure.utils.d.ts +12 -0
- package/dist/plus/integrations/providers/azure/azure.utils.js +46 -0
- package/dist/plus/integrations/providers/azure/azure.utils.js.map +1 -0
- package/dist/plus/integrations/providers/azure/models.d.ts +1 -0
- package/dist/plus/integrations/providers/azure/models.js +1 -1
- package/dist/plus/integrations/providers/azure/models.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOps.d.ts +3 -0
- package/dist/plus/integrations/providers/azureDevOps.js +7 -0
- package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket/bitbucket.utils.d.ts +17 -0
- package/dist/plus/integrations/providers/bitbucket/bitbucket.utils.js +25 -0
- package/dist/plus/integrations/providers/bitbucket/bitbucket.utils.js.map +1 -0
- package/dist/plus/integrations/providers/bitbucket/models.d.ts +5 -5
- package/dist/plus/integrations/providers/bitbucket/models.js +2 -2
- package/dist/plus/integrations/providers/bitbucket/models.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket-server.d.ts +3 -0
- package/dist/plus/integrations/providers/bitbucket-server.js +7 -0
- package/dist/plus/integrations/providers/bitbucket-server.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket.d.ts +3 -0
- package/dist/plus/integrations/providers/bitbucket.js +7 -0
- package/dist/plus/integrations/providers/bitbucket.js.map +1 -1
- package/dist/plus/integrations/providers/issueSorts.d.ts +0 -1
- package/dist/plus/integrations/providers/jira-server.d.ts +5 -3
- package/dist/plus/integrations/providers/jira-server.js +28 -8
- package/dist/plus/integrations/providers/jira-server.js.map +1 -1
- package/dist/plus/integrations/providers/jira.d.ts +26 -4
- package/dist/plus/integrations/providers/jira.js +148 -53
- package/dist/plus/integrations/providers/jira.js.map +1 -1
- package/dist/plus/integrations/providers/linear.d.ts +13 -2
- package/dist/plus/integrations/providers/linear.js +49 -12
- package/dist/plus/integrations/providers/linear.js.map +1 -1
- package/dist/plus/integrations/providers/models.d.ts +19 -1
- package/dist/plus/integrations/providers/models.js +14 -5
- package/dist/plus/integrations/providers/models.js.map +1 -1
- package/dist/plus/integrations/providers/providerErrors.d.ts +11 -0
- package/dist/plus/integrations/providers/providerErrors.js +22 -1
- package/dist/plus/integrations/providers/providerErrors.js.map +1 -1
- package/dist/plus/integrations/providers/providersApi.d.ts +8 -1
- package/dist/plus/integrations/providers/providersApi.js +3 -0
- package/dist/plus/integrations/providers/providersApi.js.map +1 -1
- package/dist/plus/integrations/providers/trello.js +1 -0
- package/dist/plus/integrations/providers/trello.js.map +1 -1
- package/dist/plus/integrations/providers/utils/discoveryCache.d.ts +36 -0
- package/dist/plus/integrations/providers/utils/discoveryCache.js +59 -0
- package/dist/plus/integrations/providers/utils/discoveryCache.js.map +1 -0
- package/dist/plus/integrations/reads/cursors.d.ts +8 -0
- package/dist/plus/integrations/reads/cursors.js +5 -3
- package/dist/plus/integrations/reads/cursors.js.map +1 -1
- package/dist/plus/integrations/reads/drains.d.ts +0 -1
- package/dist/plus/integrations/reads/drains.js +4 -4
- package/dist/plus/integrations/reads/drains.js.map +1 -1
- package/dist/plus/integrations/reads/filters.d.ts +8 -0
- package/dist/plus/integrations/reads/filters.js +2 -1
- package/dist/plus/integrations/reads/filters.js.map +1 -1
- package/dist/plus/integrations/reads/issueTracker.d.ts +9 -1
- package/dist/plus/integrations/reads/issueTracker.js +20 -3
- package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
- package/dist/plus/integrations/reads/warnings.d.ts +11 -0
- package/dist/plus/integrations/reads/warnings.js +14 -1
- package/dist/plus/integrations/reads/warnings.js.map +1 -1
- package/docs/integrations.md +4 -0
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filters.js","names":[],"sources":["../../../../../plus/integrations/src/reads/filters.ts"],"sourcesContent":["import type { IntegrationIds } from '../constants.js';\nimport type {\n\tIssueFilter,\n\tIssueSearchCapabilities,\n\tIssueSearchCriteria,\n\tIssueSearchRelationship,\n\tIssueSorting,\n\tPullRequestFilter,\n\tPullRequestSearchCapabilities,\n\tPullRequestSearchCriteria,\n} from '../providerFilters.js';\nimport type { ProviderRepoInput, ProviderReposInput } from '../providers/models.js';\nimport { providersMetadata } from '../providers/models.js';\n\n/**\n * Validation of a caller-supplied relationship filter set against what a provider can express server-side.\n *\n * Every one of these is ALL-OR-NOTHING, and deliberately so: dropping the members a provider can't express\n * would silently widen the read past what was asked for (requesting Author+Mention on a host without Mention\n * would return every authored item rather than the requested set), and narrowing the returned page afterward\n * would leave `items` describing a different result set than the `hasMore`/`cursor` the provider produced with\n * it. So an inexpressible set is refused whole, and the caller surfaces a warning instead of reading unfiltered.\n *\n * The three differ only in WHICH capability they check, because they govern three different provider queries —\n * see `IntegrationManager.getSupportedFilters`, which exposes the same capability table so a consumer can\n * intersect against it and never reach a refusal.\n */\nexport interface ResolvedFilters<T> {\n\tfilters?: T[];\n\tunsupported: boolean;\n}\n\n/**\n * Validates a repo-scoped PR filter set, so an unsupported filter never trips the read core's \"Unsupported\n * filters\" guard. `unsupported: true` when the caller DID request filters and the provider can't express even\n * ONE of them — the exact negation of the read core's `providerSupportsPullRequestFilters` (`every`), so this\n * can only ever pre-empt that guard, never disagree with it.\n *\n * Returns `{ filters }` (possibly undefined when none were requested — an unfiltered read is intended). Genuine\n * \"my pull requests\" self-scoping is delivered by the account-wide path\n * (`GitHostIntegration.getMyPullRequestsForUserResult`); this only governs the optional repo-scoped narrowing.\n */\nexport function resolvePullRequestFilters(\n\tid: IntegrationIds,\n\tfilters: PullRequestFilter[] | undefined,\n): ResolvedFilters<PullRequestFilter> {\n\tif (filters == null || filters.length === 0) return { unsupported: false };\n\n\tconst supported = providersMetadata[id]?.supportedPullRequestFilters;\n\tif (supported == null || filters.some(f => !supported.includes(f))) return { unsupported: true };\n\n\treturn { filters: filters, unsupported: false };\n}\n\n/**\n * Validates an account-wide PR relationship union independently from the repo-scoped capability. Dropping an\n * unsupported member would change the requested OR set, so validation is all-or-nothing.\n */\nexport function resolveAccountWidePullRequestFilters(\n\tid: IntegrationIds,\n\tfilters: PullRequestFilter[] | undefined,\n): ResolvedFilters<PullRequestFilter> {\n\tif (filters == null || filters.length === 0) return { unsupported: false };\n\n\tconst supported = providersMetadata[id]?.supportedAccountWidePullRequestFilters;\n\tif (supported == null || filters.some(f => !supported.includes(f))) return { unsupported: true };\n\n\treturn { filters: [...new Set(filters)], unsupported: false };\n}\n\n/** Why {@link resolvePullRequestSearchCriteria} refused a criteria set. */\nexport type PullRequestSearchCriteriaRejection =\n\t/** The provider has no filtered pull-request search. */\n\t| { reason: 'unsupported-search' }\n\t/** The provider has one, but cannot express every requested criterion server-side. */\n\t| { reason: 'unsupported-criteria'; criteria: string[] };\n\n/**\n * Validates filtered pull-request search criteria all-or-nothing. Dropping an unsupported criterion would\n * widen the provider query while leaving its page and cursor describing the wider set, so the facade refuses the\n * request before any upstream call instead.\n */\nexport function resolvePullRequestSearchCriteria(\n\tid: IntegrationIds,\n\tcriteria: PullRequestSearchCriteria | undefined,\n): { rejection?: PullRequestSearchCriteriaRejection } {\n\tconst supported = providersMetadata[id]?.supportedPullRequestSearch;\n\tif (supported == null || supported.relationships.length === 0) {\n\t\treturn { rejection: { reason: 'unsupported-search' } };\n\t}\n\tif (criteria == null) return {};\n\n\tconst unsupported: string[] = [];\n\tfor (const relationship of criteria.relationships ?? []) {\n\t\tif (!supported.relationships.includes(relationship)) {\n\t\t\tunsupported.push(`relationships:${relationship}`);\n\t\t}\n\t}\n\tfor (const state of criteria.states ?? []) {\n\t\tif (!supported.states.includes(state)) {\n\t\t\tunsupported.push(`states:${state}`);\n\t\t}\n\t}\n\tif (criteria.text != null && criteria.text.trim().length > 0 && !supported.text) {\n\t\tunsupported.push('text');\n\t}\n\tif (criteria.updatedAfter != null && !supported.updatedAfter) {\n\t\tunsupported.push('updatedAfter');\n\t}\n\tif (criteria.createdAfter != null && !supported.createdAfter) {\n\t\tunsupported.push('createdAfter');\n\t}\n\tif (criteria.includeArchived === true && !supported.includeArchived) {\n\t\tunsupported.push('includeArchived');\n\t}\n\t// `!= null`, not truthy: `draft: false` (only ready-for-review) is as much a request as `draft: true`.\n\tif (criteria.draft != null && !supported.draft) {\n\t\tunsupported.push('draft');\n\t}\n\t// Folded into the same `unsupported-criteria` rejection rather than its own reason, so a caller asking for one\n\t// inexpressible filter and one inexpressible sort learns about both at once. `updated:desc` is always in\n\t// `sorts` for a usable search, so the omitted (default) case never rejects.\n\tif (criteria.sort != null && !supported.sorts.includes(criteria.sort)) {\n\t\tunsupported.push(`sort:${criteria.sort}`);\n\t}\n\n\treturn unsupported.length > 0 ? { rejection: { reason: 'unsupported-criteria', criteria: unsupported } } : {};\n}\n\n/** Why a filtered pull-request search's repository/organization boundary was refused. */\nexport type PullRequestSearchScopeRejection =\n\t| { reason: 'unscoped' }\n\t| { reason: 'repo-ids' }\n\t| { reason: 'unsupported-repository-scope' }\n\t| { reason: 'unsupported-organization-scope' }\n\t/** Scope names a query cannot carry AS GIVEN; see {@link unusableSearchScopeNames}. */\n\t| { reason: 'unusable-scope'; scopes: string[] };\n\n/**\n * Validates the search boundary independently from its criteria. A current-user relationship is itself a safe\n * account-wide boundary; without one, a repository or organization scope is mandatory.\n *\n * Scope names go through the same {@link unusableSearchScopeNames} rule the issue search uses — shared rather\n * than re-derived, because the defect is identical on both reads.\n */\nexport function resolvePullRequestSearchScope(\n\tid: IntegrationIds,\n\trepos: ProviderReposInput | undefined,\n\torg: string | undefined,\n\tcriteria: PullRequestSearchCriteria | undefined,\n): { rejection?: PullRequestSearchScopeRejection; repos?: ProviderRepoInput[] } {\n\tconst supported = providersMetadata[id]?.supportedPullRequestSearch;\n\tlet resolvedRepos: ProviderRepoInput[] | undefined;\n\n\tif (repos?.length) {\n\t\tif (repos.some(r => typeof r === 'string' || typeof r === 'number')) {\n\t\t\treturn { rejection: { reason: 'repo-ids' } };\n\t\t}\n\t\tif (supported?.repositoryScope !== true) return { rejection: { reason: 'unsupported-repository-scope' } };\n\n\t\tresolvedRepos = repos as ProviderRepoInput[];\n\t}\n\n\tconst hasOrganizationScope = org != null && org.length > 0;\n\tif (hasOrganizationScope && supported?.organizationScope !== true) {\n\t\treturn { rejection: { reason: 'unsupported-organization-scope' } };\n\t}\n\n\t// After the capability checks and before the \"is it scoped at all\" rule: a scope the provider cannot express\n\t// at all is the more fundamental refusal, and an unusable value that WAS supplied must not be reported as a\n\t// missing one. `org` is passed bare, matching the issue twin — the helper applies its own empty-means-\n\t// unsupplied guard, so re-testing `hasOrganizationScope` here would only make the two resolvers look like\n\t// they differ.\n\tconst unusable = unusableSearchScopeNames(id, org, resolvedRepos);\n\tif (unusable.length > 0) return { rejection: { reason: 'unusable-scope', scopes: unusable } };\n\n\tif (resolvedRepos != null || hasOrganizationScope || (criteria?.relationships?.length ?? 0) > 0) {\n\t\treturn { repos: resolvedRepos };\n\t}\n\n\treturn { rejection: { reason: 'unscoped' } };\n}\n\n/**\n * Validates a caller-provided issue filter set against what the provider's ACCOUNT-WIDE issue read can express\n * server-side (`ProviderMetadata.supportedAccountWideIssueFilters`), which is a different — usually narrower —\n * set than the repo-scoped `ProviderMetadata.supportedIssueFilters`.\n *\n * Dropping the unexpressible members would silently widen the read back toward the provider's own union\n * (authored ∪ assigned ∪ mentioned for GitHub), which is the opposite of what a caller narrowing to\n * `[Assignee]` asked for.\n */\nexport function resolveAccountWideIssueFilters(\n\tid: IntegrationIds,\n\tfilters: IssueFilter[] | undefined,\n): ResolvedFilters<IssueFilter> {\n\tif (filters == null || filters.length === 0) return { unsupported: false };\n\n\tconst supported = providersMetadata[id]?.supportedAccountWideIssueFilters;\n\tif (supported == null || filters.some(f => !supported.includes(f))) return { unsupported: true };\n\n\treturn { filters: filters, unsupported: false };\n}\n\n/** Why {@link resolveIssueSearchCriteria} refused a criteria set, so the caller can word the refusal exactly. */\nexport type IssueSearchCriteriaRejection =\n\t/** The provider has no filtered issue search at all. */\n\t| { reason: 'unsupported-search' }\n\t/** The provider has one, but can't express these criteria server-side. */\n\t| { reason: 'unsupported-criteria'; criteria: string[] }\n\t/** `any-assignee` and `unassigned` partition the scope between them; asking for both asks for nothing. */\n\t| { reason: 'contradictory-relationships' };\n\n/**\n * Whether a provider exposes the FILTERED issue search at all — the one predicate a caller can check BEFORE\n * building criteria, so a read that would only be refused is never issued.\n *\n * The same test {@link resolveIssueSearchCriteria} makes for its `unsupported-search` rejection, named rather\n * than re-derived at each call site: a caller writing `supportedIssueSearch != null` itself is a copy free to\n * disagree with the validator about what \"has a search\" means. `broadenIssues` reads it to pick its engine —\n * the org-scoped search where there is one, the repository drain where there isn't — which is a CHOICE rather\n * than a refusal, so it needs the predicate without the rejection.\n */\nexport function supportsFilteredIssueSearch(id: IntegrationIds): boolean {\n\treturn providersMetadata[id]?.supportedIssueSearch != null;\n}\n\n/**\n * Validates a filtered issue search's criteria against {@link ProviderMetadata.supportedIssueSearch}.\n *\n * All-or-nothing like its three siblings above, and for the same reason: a criterion dropped because the\n * provider can't express it would serve a WIDER result than was asked for, and narrowing the returned page\n * afterward would leave `items` describing a different result set than the `hasMore`/`cursor` the provider\n * produced with it. So an inexpressible set is refused whole and the caller surfaces a warning.\n *\n * Unlike the sibling validators this also rejects a set that is internally contradictory, because this criteria\n * model is the only one with two members that partition the scope between them (`any-assignee` ∪ `unassigned` =\n * everything, ∩ = nothing). Silently keeping one would answer a question the caller didn't ask.\n *\n * `undefined` (or an empty relationship list plus no other criterion) is a valid UNNARROWED search of the given\n * scope, not an error — the caller's `repos`/`org` is what bounds it.\n */\nexport function resolveIssueSearchCriteria(\n\tid: IntegrationIds,\n\tcriteria: IssueSearchCriteria | undefined,\n): { rejection?: IssueSearchCriteriaRejection } {\n\tconst supported = providersMetadata[id]?.supportedIssueSearch;\n\tif (supported == null) return { rejection: { reason: 'unsupported-search' } };\n\tif (criteria == null) return {};\n\n\tconst relationships = criteria.relationships;\n\tif (relationships?.includes('any-assignee') && relationships.includes('unassigned')) {\n\t\treturn { rejection: { reason: 'contradictory-relationships' } };\n\t}\n\n\tconst unsupported: string[] = [];\n\tfor (const relationship of relationships ?? []) {\n\t\tif (!supported.relationships.includes(relationship)) {\n\t\t\tunsupported.push(`relationships:${relationship}`);\n\t\t}\n\t}\n\t// Only a criterion the caller actually SET can be unsupported. `false`/empty means \"don't narrow on this\",\n\t// which every provider can honor by doing nothing, so it must not be validated as a request.\n\tif (criteria.text != null && criteria.text.trim().length > 0 && !supported.text) {\n\t\tunsupported.push('text');\n\t}\n\tif (criteria.labels?.length && !supported.labels) {\n\t\tunsupported.push('labels');\n\t}\n\tif (criteria.milestone != null && !supported.milestone) {\n\t\tunsupported.push('milestone');\n\t}\n\tif (criteria.updatedAfter != null && !supported.updatedAfter) {\n\t\tunsupported.push('updatedAfter');\n\t}\n\tif (criteria.createdAfter != null && !supported.createdAfter) {\n\t\tunsupported.push('createdAfter');\n\t}\n\tif (criteria.withoutLinkedPullRequest === true && !supported.withoutLinkedPullRequest) {\n\t\tunsupported.push('withoutLinkedPullRequest');\n\t}\n\t// `'open'` is every provider's own default, so asking for it needs no state capability.\n\tif (criteria.state != null && criteria.state !== 'open' && !supported.states) {\n\t\tunsupported.push('state');\n\t}\n\t// Folded into the same `unsupported-criteria` rejection rather than given its own reason, so a caller asking\n\t// for one inexpressible filter and one inexpressible sort learns about both at once instead of fixing them one\n\t// refusal at a time. The `sorts` list is `supported`'s own, so this needs no branch on which surface it is.\n\tif (criteria.sort != null && !supported.sorts.includes(criteria.sort)) {\n\t\tunsupported.push(`sort:${criteria.sort}`);\n\t}\n\n\tif (unsupported.length > 0) return { rejection: { reason: 'unsupported-criteria', criteria: unsupported } };\n\n\treturn {};\n}\n\n/**\n * The relationships that bound a filtered issue search to the CURRENT USER, and so can stand in for a\n * repository/org scope.\n *\n * Derived from the union rather than spelled out per call site, because the distinction it encodes is the whole\n * reason the scope rule exists: `any-assignee` and `unassigned` are deliberately absent — they describe the\n * ISSUE, not the caller, so neither reduces the search to anyone's own world (measured: unscoped `no:assignee`\n * matches ~45 million issues on GitHub).\n */\nconst userScopingIssueSearchRelationships: readonly IssueSearchRelationship[] = ['authored', 'assigned', 'mentioned'];\n\n/** What a provider with NO filtered issue search reports: nothing expressible, spelled out rather than absent. */\nconst unsupportedIssueSearchCapabilities: IssueSearchCapabilities = {\n\trelationships: [],\n\ttext: false,\n\tlabels: false,\n\tmilestone: false,\n\tupdatedAfter: false,\n\tcreatedAfter: false,\n\twithoutLinkedPullRequest: false,\n\tstates: false,\n\tsorts: [],\n};\n\n/** What a provider with no filtered pull-request search reports. */\nconst unsupportedPullRequestSearchCapabilities: PullRequestSearchCapabilities = {\n\trelationships: [],\n\tstates: [],\n\ttext: false,\n\tupdatedAfter: false,\n\tcreatedAfter: false,\n\tincludeArchived: false,\n\tdraft: false,\n\trepositoryScope: false,\n\torganizationScope: false,\n\tsorts: [],\n};\n\n/** Why a filtered issue search's scope was refused, or `undefined` when it is usable. */\nexport type IssueSearchScopeRejection =\n\t/** No repositories, no org, and no user-relative relationship: a search of the whole host. */\n\t| { reason: 'unscoped' }\n\t/** Repositories given as ids. A search names repositories by PATH, so ids can't express a scope. */\n\t| { reason: 'repo-ids' }\n\t/** Scope names a query cannot carry AS GIVEN; see {@link unusableSearchScopeNames}. */\n\t| { reason: 'unusable-scope'; scopes: string[] };\n\n/**\n * Whether a scope name reaches the provider NAMING THE SAME SCOPE — the one rule behind every `unusable-scope`\n * refusal, and the canonical home for its reasoning: the call sites point here rather than restating it.\n *\n * The defect it exists to prevent: the boundary is checked against the value AS SUPPLIED while the request is\n * built from the value AFTER the provider sanitizes it, so a name a query cannot spell produces a read that is no\n * longer the read that was authorized. Three outcomes, all of which LOOK LIKE SUCCESS, so there is nothing for a\n * consumer to branch on:\n * - **emptied** — a value of only quotes/whitespace/control characters emits no scope qualifier at all. A scope is\n * also what makes a relationship-less read legal, so the request carries neither and every item on the host\n * matches: measured at 52 million issues across unrelated accounts.\n * - **altered** — a quote inside a real name sanitizes to a real but DIFFERENT scope (`git\"kraken` -> `gitkraken`),\n * whose answer looks entirely normal.\n * - **split** — whitespace delimits qualifiers, so `my org` emits `org:my org`: a search of `my` additionally\n * filtered by the free text `org`. Measured against the live API this is a wrong NARROWING rather than a\n * widening — `org:gitkraken bar` returns 12 where `org:gitkraken` returns 379.\n *\n * Refusing beats sanitizing, and the count probes are why it matters most: they deliberately apply exactly the\n * qualifiers their search would, so a sanitized scope makes the count AGREE with the wrong search rather than\n * disagree with it, and a consumer cross-checking \"N matched\" cannot detect it by construction. Nor can it\n * pre-empt the rule — the free-text sanitizing rules are published precisely so a caller can mirror them, the\n * scope rules are not — so only the caller knows which scope it meant, and the refusal goes back to it NAMING the\n * offending value.\n *\n * Provider-NEUTRAL by design, and deliberately not an import of GitHub's `sanitizeGitHubQualifierValue`: this\n * module validates for every provider, and a GitHub-specific rule reaching in here would be wrong for the next\n * one that declares a search. The character class below is stated as the common part of any query language,\n * because each class breaks a query on its own terms — a quote closes its own qualifier, a control character\n * cannot appear at all, whitespace delimits the next qualifier.\n *\n * A provider that never puts a scope name into a query string declares\n * {@link ProviderMetadata.exactSearchScopeNames}, and only blank names and control characters are refused for it:\n * Azure DevOps Server addresses a collection or repository as an encoded URL segment and matches a project against\n * the names discovery reported, so a space or a quote reaches it naming the same scope — and its names legitimately\n * contain both.\n *\n * EDGES ARE STRIPPED before the test, which is what keeps the predicate no stricter than the provider's own\n * sanitizing — the invariant that makes refusing safe to add, since it means this can only reject a name the\n * provider would have altered, never one it would have resolved correctly. Leading and trailing whitespace AND\n * control characters both qualify: a sanitizer maps a control character to a space, then collapses and trims, so\n * `'gitkraken\\n'` and `'gitkraken\\u0000'` alike emit `org:gitkraken` — the scope that was asked for. Note this\n * is wider than `String.trim()`, which leaves control characters in place.\n *\n * None of the three outcomes above survives the stripping, so the rule loses nothing: an all-edge value still\n * empties, an INNER space or control character still splits (`'git\\u0000kraken'` emits `git kraken`, two\n * tokens), and a quote still alters wherever it sits.\n */\nfunction isUsableSearchScopeName(id: IntegrationIds, name: string): boolean {\n\tconst stripped = stripSearchScopeEdges(name);\n\tif (providersMetadata[id]?.exactSearchScopeNames === true) {\n\t\t// eslint-disable-next-line no-control-regex\n\t\treturn stripped.length > 0 && !/[\\u0000-\\u001f\\u007f]/.test(stripped);\n\t}\n\n\t// eslint-disable-next-line no-control-regex\n\treturn stripped.length > 0 && !/[\"\\u0000-\\u001f\\u007f\\s]/.test(stripped);\n}\n\n/** The leading/trailing run a provider's sanitizing removes — see {@link isUsableSearchScopeName}. */\nfunction stripSearchScopeEdges(value: string): string {\n\t// eslint-disable-next-line no-control-regex\n\treturn value.replace(/^[\\s\\u0000-\\u001f\\u007f]+|[\\s\\u0000-\\u001f\\u007f]+$/g, '');\n}\n\n/**\n * The scope names a search cannot carry as given, as the strings to name in the refusal — empty when every one is\n * usable. See {@link isUsableSearchScopeName} for the rule and why it refuses rather than sanitizes.\n */\nfunction unusableSearchScopeNames(\n\tid: IntegrationIds,\n\torg: string | undefined,\n\trepos: readonly ProviderRepoInput[] | undefined,\n): string[] {\n\tconst unusable: string[] = [];\n\n\t// An EMPTY org is \"no org supplied\" and falls through to the remaining scopes; any other unusable value WAS\n\t// supplied, so it is refused rather than dropped.\n\tif (org != null && org.length > 0 && !isUsableSearchScopeName(id, org)) {\n\t\tunusable.push(org);\n\t}\n\n\t// A `repo:` qualifier names a repository by its JOINED `namespace/name` path, which is the value the rule\n\t// below is applied to. Both halves are read defensively, through the SAME locals the label is built from: the\n\t// descriptor form is only narrowed from a union by an element-type check, so a half-built descriptor reaches\n\t// here as `undefined` and must refuse rather than throw out of a facade that reports refusals as warnings —\n\t// and reporting it as `undefined/a` would name a value the caller never passed.\n\tfor (const repo of repos ?? []) {\n\t\tconst namespace = repo.namespace ?? '';\n\t\tconst name = repo.name ?? '';\n\t\tconst path = `${namespace}/${name}`;\n\t\t// BOTH the composite and each half, because neither sees what the other does:\n\t\t// - the composite catches an offender the halves cannot, since an edge of a half is an INTERIOR character\n\t\t// of the path — `{ 'git ', 'kraken' }` has two usable-looking halves and emits `repo:git /kraken`.\n\t\t// - the halves catch a BLANK one the composite cannot, since that offender sits at a composite EDGE where\n\t\t// stripping removes it — `' /a'` strips to `'/a'`, a perfectly spellable qualifier naming no repository.\n\t\t// Each half is measured after the same stripping, so `' '` and `''` are one case rather than two.\n\t\tif (\n\t\t\t!isUsableSearchScopeName(id, path) ||\n\t\t\tstripSearchScopeEdges(namespace).length === 0 ||\n\t\t\tstripSearchScopeEdges(name).length === 0\n\t\t) {\n\t\t\tunusable.push(path);\n\t\t}\n\t}\n\n\treturn unusable;\n}\n\n/**\n * Validates that a filtered issue search is scoped at all, and narrows `repos` to the descriptor form the\n * provider query needs.\n *\n * Shared by `searchIssuesPage` and `countIssues` because a count computed under different constraints than the\n * read it previews is a WRONG number rather than a missing one. The rejection is returned as a reason code, not a\n * warning: the two callers word it differently (whole-read vs naming the offending scope's key), and wording is\n * the warning layer's business.\n *\n * `repo-ids` and `unscoped` are mutually exclusive — one requires repositories and the other requires none — so\n * their relative order is free. `unusable-scope` is NOT: it must precede `unscoped`, so a value that was supplied\n * but cannot be used is never reported as a missing one.\n *\n * Every scope name is checked for naming the same scope after the provider sanitizes it, not merely for being\n * non-empty, and that distinction is a SECURITY one rather than a nicety — see {@link isUsableSearchScopeName}.\n */\nexport function resolveIssueSearchScope(\n\tid: IntegrationIds,\n\trepos: ProviderReposInput | undefined,\n\torg: string | undefined,\n\tcriteria: IssueSearchCriteria | undefined,\n): { rejection?: IssueSearchScopeRejection; repos?: ProviderRepoInput[] } {\n\tlet resolvedRepos: ProviderRepoInput[] | undefined;\n\n\tif (repos?.length) {\n\t\t// `ProviderReposInput` is a union of descriptor and id arrays; only the descriptor form is usable here.\n\t\tif (repos.some(r => typeof r === 'string' || typeof r === 'number')) {\n\t\t\treturn { rejection: { reason: 'repo-ids' } };\n\t\t}\n\n\t\tresolvedRepos = repos as ProviderRepoInput[];\n\t}\n\n\t// Checked BEFORE the \"is it scoped at all\" rule below, so an unusable value is never reported as a missing\n\t// one: it was supplied, and telling the caller to pass a scope it already passed names the wrong defect.\n\tconst unusable = unusableSearchScopeNames(id, org, resolvedRepos);\n\tif (unusable.length > 0) return { rejection: { reason: 'unusable-scope', scopes: unusable } };\n\n\tif (resolvedRepos != null) return { repos: resolvedRepos };\n\tif (org != null && org.length > 0) return {};\n\tif (criteria?.relationships?.some(r => userScopingIssueSearchRelationships.includes(r)) === true) return {};\n\n\treturn { rejection: { reason: 'unscoped' } };\n}\n\n/**\n * What {@link getSupportedFilters} reports: every vocabulary a provider's reads can express, so a consumer can\n * narrow to it BEFORE issuing a read. See that function for the capability table itself and why intersecting\n * against this is what keeps a filtered read from being refused.\n *\n * Named rather than written inline, because the same shape is the declared return type of THREE declarations that\n * all describe one value — this function, `IntegrationService.getSupportedFilters`, which only forwards to it, and\n * `IntegrationManager.getSupportedFilters`, which publishes it. Spelled out three times, a member added here\n * reached one of them and left the others a compile error away from the truth; that is exactly how the two sort\n * members below came to be written out three times.\n */\nexport type SupportedFilters = {\n\tpullRequests: PullRequestFilter[];\n\tpullRequestsAccountWide: PullRequestFilter[];\n\t/**\n\t * Criteria and scopes the filtered pull-request search can express. Always present; an empty relationship list\n\t * means the provider exposes no filtered pull-request search.\n\t */\n\tpullRequestSearch: PullRequestSearchCapabilities;\n\t/**\n\t * Filters the repo-scoped issue read accepts — and, for an issue tracker (Jira/Linear/Trello), its\n\t * project-scoped read, that being its only issue surface. A tracker therefore reports here and leaves\n\t * `issuesAccountWide` empty, so intersecting a tracker against that field would read \"cannot filter\" for a\n\t * provider that filters fine.\n\t */\n\tissues: IssueFilter[];\n\tissuesAccountWide: IssueFilter[];\n\t/**\n\t * What the filtered issue search (and the count probe over the same criteria) can express. Always present: a\n\t * provider with no filtered issue search reports an empty `relationships` and all-false flags, which is the\n\t * signal to hide that surface rather than to hide individual chips.\n\t */\n\tissueSearch: IssueSearchCapabilities;\n\t/**\n\t * Sort keys the repo-scoped issue read can express — and, for an issue tracker, its project-scoped read. Empty\n\t * means the read can't be ordered, so pass no `sort`.\n\t *\n\t * A key here is expressible on ONE provider query. A page spanning several repositories or projects is a merge,\n\t * and a merge can only order by what a normalized issue carries, so `priority`/`dueDate`/`resolved` are refused\n\t * there even where they are listed. Read one scope at a time to use them.\n\t */\n\tissueSorts: IssueSorting[];\n\t/**\n\t * Sort keys the account-wide issue read can express — a different vocabulary, not a subset: for GitLab the two\n\t * reads are different APIs. Needs no mergeability caveat, because every account-wide read is a union of several\n\t * queries, so only keys a merge can honor are listed at all. Empty for an issue tracker, which reports under\n\t * `issueSorts`.\n\t */\n\tissueSortsAccountWide: IssueSorting[];\n};\n\n/**\n * The filters `listPullRequestsPage`/`listIssuesPage` (and the sweeps) accept for a provider, so a caller can\n * narrow to what the provider can express BEFORE issuing the read.\n *\n * This matters because the filter contract is all-or-nothing: a set containing even one unsupported filter is\n * refused outright ({@link resolvePullRequestFilters}) rather than silently narrowed, since falling through to\n * an unfiltered fetch would return every PR instead of the user's. Without this accessor a consumer had to\n * hardcode its own copy of the table — reachable only by importing the internal `providers/models.js`\n * subpath — and a copy that drifts turns a supported read into an empty page with `fetchFailed`.\n *\n * Empty means \"no filter of that kind is expressible\": either the provider has no such surface (issue trackers\n * have no pull requests; Bitbucket exposes no issues) or its metadata declares none. Callers should treat it as\n * \"don't pass filters\", not as an error. Returns copies, so mutating the result can't corrupt the metadata.\n *\n * `issues` and `issuesAccountWide` are separate because the repo-scoped and account-wide issue reads are\n * different provider queries with different filter surfaces, and the same `filters` input is validated against\n * whichever one the read uses (`repos` present or not). `issuesAccountWide` is generally the narrower of the\n * two: GitLab, for instance, can express `Assignee` and `Author` account-wide, but not `Mention`.\n *\n * That split describes the GIT-HOST reads only. An issue tracker (Jira/Linear/Trello) has neither — its issues\n * live under resource → project — so it reports its filters under `issues`, which is what\n * {@link IntegrationService.listIssueTrackerIssuesPage} validates against, and leaves `issuesAccountWide`\n * empty. Reading a tracker's capability off `issuesAccountWide` therefore under-reports it.\n *\n * `issueSearch` is a third, wider surface: the FILTERED issue search (`searchIssuesPage`, and the `countIssues`\n * probe over the same criteria), which is not bound to the user at all. It is a record of per-criterion flags\n * rather than a list, because its criteria aren't a single kind. An empty `relationships` means the provider has\n * no filtered issue search — hide the surface, not just its chips.\n *\n * `pullRequestSearch` describes the separate filtered PR search. Its table declares each criteria vocabulary\n * plus repository/organization scope support; an empty relationship list means the search itself is absent.\n *\n * `issueSorts` / `issueSortsAccountWide` / `issueSearch.sorts` are the ORDERING vocabulary of those same three\n * issue reads, split the same way and for the same reason (GitLab's repo-scoped read is GraphQL and its\n * account-wide read is REST, with genuinely different sort vocabularies). Empty means the read can't be ordered,\n * so pass no `sort`; a key not listed refuses the whole read exactly like an inexpressible filter. Note a key\n * listed here is expressible on ONE provider query: a read that fans out across projects can only honor a key\n * derivable from a normalized issue, so it refuses `priority`/`dueDate`/`resolved` on top of this table.\n *\n * Note this is a CAPABILITY table — \"what the provider can express\" — not a recommendation. A consumer\n * matching another tool's behavior may deliberately pass fewer filters than are listed here (or none, where an\n * already-scoped read would only be narrowed by them). Intersecting against this table is what keeps a\n * filtered read from being refused; it isn't a directive to use every filter in it.\n */\nexport function getSupportedFilters(providerId: IntegrationIds): SupportedFilters {\n\tconst metadata = providersMetadata[providerId];\n\tconst issueSearch = metadata?.supportedIssueSearch;\n\tconst pullRequestSearch = metadata?.supportedPullRequestSearch;\n\treturn {\n\t\tpullRequests: [...(metadata?.supportedPullRequestFilters ?? [])],\n\t\tpullRequestsAccountWide: [...(metadata?.supportedAccountWidePullRequestFilters ?? [])],\n\t\tpullRequestSearch: {\n\t\t\t...unsupportedPullRequestSearchCapabilities,\n\t\t\t...pullRequestSearch,\n\t\t\trelationships: [...(pullRequestSearch?.relationships ?? [])],\n\t\t\tstates: [...(pullRequestSearch?.states ?? [])],\n\t\t\t// Copied, so mutating the result can't corrupt the metadata table.\n\t\t\tsorts: [...(pullRequestSearch?.sorts ?? [])],\n\t\t},\n\t\tissues: [...(metadata?.supportedIssueFilters ?? [])],\n\t\tissuesAccountWide: [...(metadata?.supportedAccountWideIssueFilters ?? [])],\n\t\t// Always an object, never `undefined`: a provider WITHOUT a filtered issue search reports one whose\n\t\t// `relationships` is empty and whose flags are all false, so a consumer reads capabilities the same way\n\t\t// for every provider (and an empty `relationships` is the signal to hide the surface itself).\n\t\t//\n\t\t// Spread over the all-false baseline rather than defaulting each flag: a criterion added to\n\t\t// `IssueSearchCapabilities` then can't be forgotten here and silently reported as `undefined` (which a\n\t\t// consumer's `if (caps.x)` would read as unsupported — right answer, wrong reason, and untyped).\n\t\t// `supportedIssueSearch` is typed as the complete shape, so no member can arrive as an explicit undefined.\n\t\tissueSearch: {\n\t\t\t...unsupportedIssueSearchCapabilities,\n\t\t\t...issueSearch,\n\t\t\t// Copied, so mutating the result can't corrupt the metadata table.\n\t\t\trelationships: [...(issueSearch?.relationships ?? [])],\n\t\t\tsorts: [...(issueSearch?.sorts ?? [])],\n\t\t},\n\t\tissueSorts: [...(metadata?.supportedIssueSorts ?? [])],\n\t\tissueSortsAccountWide: [...(metadata?.supportedAccountWideIssueSorts ?? [])],\n\t};\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"filters.js","names":[],"sources":["../../../../../plus/integrations/src/reads/filters.ts"],"sourcesContent":["import type { IntegrationIds } from '../constants.js';\nimport type {\n\tIssueFilter,\n\tIssueSearchCapabilities,\n\tIssueSearchCriteria,\n\tIssueSearchRelationship,\n\tIssueSorting,\n\tPullRequestFilter,\n\tPullRequestSearchCapabilities,\n\tPullRequestSearchCriteria,\n} from '../providerFilters.js';\nimport type { ProviderRepoInput, ProviderReposInput } from '../providers/models.js';\nimport { providersMetadata } from '../providers/models.js';\n\n/**\n * Validation of a caller-supplied relationship filter set against what a provider can express server-side.\n *\n * Every one of these is ALL-OR-NOTHING, and deliberately so: dropping the members a provider can't express\n * would silently widen the read past what was asked for (requesting Author+Mention on a host without Mention\n * would return every authored item rather than the requested set), and narrowing the returned page afterward\n * would leave `items` describing a different result set than the `hasMore`/`cursor` the provider produced with\n * it. So an inexpressible set is refused whole, and the caller surfaces a warning instead of reading unfiltered.\n *\n * The three differ only in WHICH capability they check, because they govern three different provider queries —\n * see `IntegrationManager.getSupportedFilters`, which exposes the same capability table so a consumer can\n * intersect against it and never reach a refusal.\n */\nexport interface ResolvedFilters<T> {\n\tfilters?: T[];\n\tunsupported: boolean;\n}\n\n/**\n * Validates a repo-scoped PR filter set, so an unsupported filter never trips the read core's \"Unsupported\n * filters\" guard. `unsupported: true` when the caller DID request filters and the provider can't express even\n * ONE of them — the exact negation of the read core's `providerSupportsPullRequestFilters` (`every`), so this\n * can only ever pre-empt that guard, never disagree with it.\n *\n * Returns `{ filters }` (possibly undefined when none were requested — an unfiltered read is intended). Genuine\n * \"my pull requests\" self-scoping is delivered by the account-wide path\n * (`GitHostIntegration.getMyPullRequestsForUserResult`); this only governs the optional repo-scoped narrowing.\n */\nexport function resolvePullRequestFilters(\n\tid: IntegrationIds,\n\tfilters: PullRequestFilter[] | undefined,\n): ResolvedFilters<PullRequestFilter> {\n\tif (filters == null || filters.length === 0) return { unsupported: false };\n\n\tconst supported = providersMetadata[id]?.supportedPullRequestFilters;\n\tif (supported == null || filters.some(f => !supported.includes(f))) return { unsupported: true };\n\n\treturn { filters: filters, unsupported: false };\n}\n\n/**\n * Validates an account-wide PR relationship union independently from the repo-scoped capability. Dropping an\n * unsupported member would change the requested OR set, so validation is all-or-nothing.\n */\nexport function resolveAccountWidePullRequestFilters(\n\tid: IntegrationIds,\n\tfilters: PullRequestFilter[] | undefined,\n): ResolvedFilters<PullRequestFilter> {\n\tif (filters == null || filters.length === 0) return { unsupported: false };\n\n\tconst supported = providersMetadata[id]?.supportedAccountWidePullRequestFilters;\n\tif (supported == null || filters.some(f => !supported.includes(f))) return { unsupported: true };\n\n\treturn { filters: [...new Set(filters)], unsupported: false };\n}\n\n/** Why {@link resolvePullRequestSearchCriteria} refused a criteria set. */\nexport type PullRequestSearchCriteriaRejection =\n\t/** The provider has no filtered pull-request search. */\n\t| { reason: 'unsupported-search' }\n\t/** The provider has one, but cannot express every requested criterion server-side. */\n\t| { reason: 'unsupported-criteria'; criteria: string[] };\n\n/**\n * Validates filtered pull-request search criteria all-or-nothing. Dropping an unsupported criterion would\n * widen the provider query while leaving its page and cursor describing the wider set, so the facade refuses the\n * request before any upstream call instead.\n */\nexport function resolvePullRequestSearchCriteria(\n\tid: IntegrationIds,\n\tcriteria: PullRequestSearchCriteria | undefined,\n): { rejection?: PullRequestSearchCriteriaRejection } {\n\tconst supported = providersMetadata[id]?.supportedPullRequestSearch;\n\tif (supported == null || supported.relationships.length === 0) {\n\t\treturn { rejection: { reason: 'unsupported-search' } };\n\t}\n\tif (criteria == null) return {};\n\n\tconst unsupported: string[] = [];\n\tfor (const relationship of criteria.relationships ?? []) {\n\t\tif (!supported.relationships.includes(relationship)) {\n\t\t\tunsupported.push(`relationships:${relationship}`);\n\t\t}\n\t}\n\tfor (const state of criteria.states ?? []) {\n\t\tif (!supported.states.includes(state)) {\n\t\t\tunsupported.push(`states:${state}`);\n\t\t}\n\t}\n\tif (criteria.text != null && criteria.text.trim().length > 0 && !supported.text) {\n\t\tunsupported.push('text');\n\t}\n\tif (criteria.updatedAfter != null && !supported.updatedAfter) {\n\t\tunsupported.push('updatedAfter');\n\t}\n\tif (criteria.createdAfter != null && !supported.createdAfter) {\n\t\tunsupported.push('createdAfter');\n\t}\n\tif (criteria.includeArchived === true && !supported.includeArchived) {\n\t\tunsupported.push('includeArchived');\n\t}\n\t// `!= null`, not truthy: `draft: false` (only ready-for-review) is as much a request as `draft: true`.\n\tif (criteria.draft != null && !supported.draft) {\n\t\tunsupported.push('draft');\n\t}\n\t// Folded into the same `unsupported-criteria` rejection rather than its own reason, so a caller asking for one\n\t// inexpressible filter and one inexpressible sort learns about both at once. `updated:desc` is always in\n\t// `sorts` for a usable search, so the omitted (default) case never rejects.\n\tif (criteria.sort != null && !supported.sorts.includes(criteria.sort)) {\n\t\tunsupported.push(`sort:${criteria.sort}`);\n\t}\n\n\treturn unsupported.length > 0 ? { rejection: { reason: 'unsupported-criteria', criteria: unsupported } } : {};\n}\n\n/** Why a filtered pull-request search's repository/organization boundary was refused. */\nexport type PullRequestSearchScopeRejection =\n\t| { reason: 'unscoped' }\n\t| { reason: 'repo-ids' }\n\t| { reason: 'unsupported-repository-scope' }\n\t| { reason: 'unsupported-organization-scope' }\n\t/** Scope names a query cannot carry AS GIVEN; see {@link unusableSearchScopeNames}. */\n\t| { reason: 'unusable-scope'; scopes: string[] };\n\n/**\n * Validates the search boundary independently from its criteria. A current-user relationship is itself a safe\n * account-wide boundary; without one, a repository or organization scope is mandatory.\n *\n * Scope names go through the same {@link unusableSearchScopeNames} rule the issue search uses — shared rather\n * than re-derived, because the defect is identical on both reads.\n */\nexport function resolvePullRequestSearchScope(\n\tid: IntegrationIds,\n\trepos: ProviderReposInput | undefined,\n\torg: string | undefined,\n\tcriteria: PullRequestSearchCriteria | undefined,\n): { rejection?: PullRequestSearchScopeRejection; repos?: ProviderRepoInput[] } {\n\tconst supported = providersMetadata[id]?.supportedPullRequestSearch;\n\tlet resolvedRepos: ProviderRepoInput[] | undefined;\n\n\tif (repos?.length) {\n\t\tif (repos.some(r => typeof r === 'string' || typeof r === 'number')) {\n\t\t\treturn { rejection: { reason: 'repo-ids' } };\n\t\t}\n\t\tif (supported?.repositoryScope !== true) return { rejection: { reason: 'unsupported-repository-scope' } };\n\n\t\tresolvedRepos = repos as ProviderRepoInput[];\n\t}\n\n\tconst hasOrganizationScope = org != null && org.length > 0;\n\tif (hasOrganizationScope && supported?.organizationScope !== true) {\n\t\treturn { rejection: { reason: 'unsupported-organization-scope' } };\n\t}\n\n\t// After the capability checks and before the \"is it scoped at all\" rule: a scope the provider cannot express\n\t// at all is the more fundamental refusal, and an unusable value that WAS supplied must not be reported as a\n\t// missing one. `org` is passed bare, matching the issue twin — the helper applies its own empty-means-\n\t// unsupplied guard, so re-testing `hasOrganizationScope` here would only make the two resolvers look like\n\t// they differ.\n\tconst unusable = unusableSearchScopeNames(id, org, resolvedRepos);\n\tif (unusable.length > 0) return { rejection: { reason: 'unusable-scope', scopes: unusable } };\n\n\tif (resolvedRepos != null || hasOrganizationScope || (criteria?.relationships?.length ?? 0) > 0) {\n\t\treturn { repos: resolvedRepos };\n\t}\n\n\treturn { rejection: { reason: 'unscoped' } };\n}\n\n/**\n * Validates a caller-provided issue filter set against what the provider's ACCOUNT-WIDE issue read can express\n * server-side (`ProviderMetadata.supportedAccountWideIssueFilters`), which is a different — usually narrower —\n * set than the repo-scoped `ProviderMetadata.supportedIssueFilters`.\n *\n * Dropping the unexpressible members would silently widen the read back toward the provider's own union\n * (authored ∪ assigned ∪ mentioned for GitHub), which is the opposite of what a caller narrowing to\n * `[Assignee]` asked for.\n */\nexport function resolveAccountWideIssueFilters(\n\tid: IntegrationIds,\n\tfilters: IssueFilter[] | undefined,\n): ResolvedFilters<IssueFilter> {\n\tif (filters == null || filters.length === 0) return { unsupported: false };\n\n\tconst supported = providersMetadata[id]?.supportedAccountWideIssueFilters;\n\tif (supported == null || filters.some(f => !supported.includes(f))) return { unsupported: true };\n\n\treturn { filters: filters, unsupported: false };\n}\n\n/** Why {@link resolveIssueSearchCriteria} refused a criteria set, so the caller can word the refusal exactly. */\nexport type IssueSearchCriteriaRejection =\n\t/** The provider has no filtered issue search at all. */\n\t| { reason: 'unsupported-search' }\n\t/** The provider has one, but can't express these criteria server-side. */\n\t| { reason: 'unsupported-criteria'; criteria: string[] }\n\t/** `any-assignee` and `unassigned` partition the scope between them; asking for both asks for nothing. */\n\t| { reason: 'contradictory-relationships' };\n\n/**\n * Whether a provider exposes the FILTERED issue search at all — the one predicate a caller can check BEFORE\n * building criteria, so a read that would only be refused is never issued.\n *\n * The same test {@link resolveIssueSearchCriteria} makes for its `unsupported-search` rejection, named rather\n * than re-derived at each call site: a caller writing `supportedIssueSearch != null` itself is a copy free to\n * disagree with the validator about what \"has a search\" means. `broadenIssues` reads it to pick its engine —\n * the org-scoped search where there is one, the repository drain where there isn't — which is a CHOICE rather\n * than a refusal, so it needs the predicate without the rejection.\n */\nexport function supportsFilteredIssueSearch(id: IntegrationIds): boolean {\n\treturn providersMetadata[id]?.supportedIssueSearch != null;\n}\n\n/**\n * Validates a filtered issue search's criteria against {@link ProviderMetadata.supportedIssueSearch}.\n *\n * All-or-nothing like its three siblings above, and for the same reason: a criterion dropped because the\n * provider can't express it would serve a WIDER result than was asked for, and narrowing the returned page\n * afterward would leave `items` describing a different result set than the `hasMore`/`cursor` the provider\n * produced with it. So an inexpressible set is refused whole and the caller surfaces a warning.\n *\n * Unlike the sibling validators this also rejects a set that is internally contradictory, because this criteria\n * model is the only one with two members that partition the scope between them (`any-assignee` ∪ `unassigned` =\n * everything, ∩ = nothing). Silently keeping one would answer a question the caller didn't ask.\n *\n * `undefined` (or an empty relationship list plus no other criterion) is a valid UNNARROWED search of the given\n * scope, not an error — the caller's `repos`/`org` is what bounds it.\n */\nexport function resolveIssueSearchCriteria(\n\tid: IntegrationIds,\n\tcriteria: IssueSearchCriteria | undefined,\n): { rejection?: IssueSearchCriteriaRejection } {\n\tconst supported = providersMetadata[id]?.supportedIssueSearch;\n\tif (supported == null) return { rejection: { reason: 'unsupported-search' } };\n\tif (criteria == null) return {};\n\n\tconst relationships = criteria.relationships;\n\tif (relationships?.includes('any-assignee') && relationships.includes('unassigned')) {\n\t\treturn { rejection: { reason: 'contradictory-relationships' } };\n\t}\n\n\tconst unsupported: string[] = [];\n\tfor (const relationship of relationships ?? []) {\n\t\tif (!supported.relationships.includes(relationship)) {\n\t\t\tunsupported.push(`relationships:${relationship}`);\n\t\t}\n\t}\n\t// Only a criterion the caller actually SET can be unsupported. `false`/empty means \"don't narrow on this\",\n\t// which every provider can honor by doing nothing, so it must not be validated as a request.\n\tif (criteria.text != null && criteria.text.trim().length > 0 && !supported.text) {\n\t\tunsupported.push('text');\n\t}\n\tif (criteria.labels?.length && !supported.labels) {\n\t\tunsupported.push('labels');\n\t}\n\tif (criteria.milestone != null && !supported.milestone) {\n\t\tunsupported.push('milestone');\n\t}\n\tif (criteria.updatedAfter != null && !supported.updatedAfter) {\n\t\tunsupported.push('updatedAfter');\n\t}\n\tif (criteria.createdAfter != null && !supported.createdAfter) {\n\t\tunsupported.push('createdAfter');\n\t}\n\tif (criteria.withoutLinkedPullRequest === true && !supported.withoutLinkedPullRequest) {\n\t\tunsupported.push('withoutLinkedPullRequest');\n\t}\n\t// `'open'` is every provider's own default, so asking for it needs no state capability.\n\tif (criteria.state != null && criteria.state !== 'open' && !supported.states) {\n\t\tunsupported.push('state');\n\t}\n\t// Folded into the same `unsupported-criteria` rejection rather than given its own reason, so a caller asking\n\t// for one inexpressible filter and one inexpressible sort learns about both at once instead of fixing them one\n\t// refusal at a time. The `sorts` list is `supported`'s own, so this needs no branch on which surface it is.\n\tif (criteria.sort != null && !supported.sorts.includes(criteria.sort)) {\n\t\tunsupported.push(`sort:${criteria.sort}`);\n\t}\n\n\tif (unsupported.length > 0) return { rejection: { reason: 'unsupported-criteria', criteria: unsupported } };\n\n\treturn {};\n}\n\n/**\n * The relationships that bound a filtered issue search to the CURRENT USER, and so can stand in for a\n * repository/org scope.\n *\n * Derived from the union rather than spelled out per call site, because the distinction it encodes is the whole\n * reason the scope rule exists: `any-assignee` and `unassigned` are deliberately absent — they describe the\n * ISSUE, not the caller, so neither reduces the search to anyone's own world (measured: unscoped `no:assignee`\n * matches ~45 million issues on GitHub).\n */\nconst userScopingIssueSearchRelationships: readonly IssueSearchRelationship[] = ['authored', 'assigned', 'mentioned'];\n\n/** What a provider with NO filtered issue search reports: nothing expressible, spelled out rather than absent. */\nconst unsupportedIssueSearchCapabilities: IssueSearchCapabilities = {\n\trelationships: [],\n\ttext: false,\n\tlabels: false,\n\tmilestone: false,\n\tupdatedAfter: false,\n\tcreatedAfter: false,\n\twithoutLinkedPullRequest: false,\n\tstates: false,\n\tsorts: [],\n};\n\n/** What a provider with no filtered pull-request search reports. */\nconst unsupportedPullRequestSearchCapabilities: PullRequestSearchCapabilities = {\n\trelationships: [],\n\tstates: [],\n\ttext: false,\n\tupdatedAfter: false,\n\tcreatedAfter: false,\n\tincludeArchived: false,\n\tdraft: false,\n\trepositoryScope: false,\n\torganizationScope: false,\n\tsorts: [],\n};\n\n/** Why a filtered issue search's scope was refused, or `undefined` when it is usable. */\nexport type IssueSearchScopeRejection =\n\t/** No repositories, no org, and no user-relative relationship: a search of the whole host. */\n\t| { reason: 'unscoped' }\n\t/** Repositories given as ids. A search names repositories by PATH, so ids can't express a scope. */\n\t| { reason: 'repo-ids' }\n\t/** Scope names a query cannot carry AS GIVEN; see {@link unusableSearchScopeNames}. */\n\t| { reason: 'unusable-scope'; scopes: string[] };\n\n/**\n * Whether a scope name reaches the provider NAMING THE SAME SCOPE — the one rule behind every `unusable-scope`\n * refusal, and the canonical home for its reasoning: the call sites point here rather than restating it.\n *\n * The defect it exists to prevent: the boundary is checked against the value AS SUPPLIED while the request is\n * built from the value AFTER the provider sanitizes it, so a name a query cannot spell produces a read that is no\n * longer the read that was authorized. Three outcomes, all of which LOOK LIKE SUCCESS, so there is nothing for a\n * consumer to branch on:\n * - **emptied** — a value of only quotes/whitespace/control characters emits no scope qualifier at all. A scope is\n * also what makes a relationship-less read legal, so the request carries neither and every item on the host\n * matches: measured at 52 million issues across unrelated accounts.\n * - **altered** — a quote inside a real name sanitizes to a real but DIFFERENT scope (`git\"kraken` -> `gitkraken`),\n * whose answer looks entirely normal.\n * - **split** — whitespace delimits qualifiers, so `my org` emits `org:my org`: a search of `my` additionally\n * filtered by the free text `org`. Measured against the live API this is a wrong NARROWING rather than a\n * widening — `org:gitkraken bar` returns 12 where `org:gitkraken` returns 379.\n *\n * Refusing beats sanitizing, and the count probes are why it matters most: they deliberately apply exactly the\n * qualifiers their search would, so a sanitized scope makes the count AGREE with the wrong search rather than\n * disagree with it, and a consumer cross-checking \"N matched\" cannot detect it by construction. Nor can it\n * pre-empt the rule — the free-text sanitizing rules are published precisely so a caller can mirror them, the\n * scope rules are not — so only the caller knows which scope it meant, and the refusal goes back to it NAMING the\n * offending value.\n *\n * Provider-NEUTRAL by design, and deliberately not an import of GitHub's `sanitizeGitHubQualifierValue`: this\n * module validates for every provider, and a GitHub-specific rule reaching in here would be wrong for the next\n * one that declares a search. The character class below is stated as the common part of any query language,\n * because each class breaks a query on its own terms — a quote closes its own qualifier, a control character\n * cannot appear at all, whitespace delimits the next qualifier.\n *\n * A provider that never puts a scope name into a query string declares\n * {@link ProviderMetadata.exactSearchScopeNames}, and only blank names and control characters are refused for it:\n * Azure DevOps Server addresses a collection or repository as an encoded URL segment and matches a project against\n * the names discovery reported, so a space or a quote reaches it naming the same scope — and its names legitimately\n * contain both.\n *\n * EDGES ARE STRIPPED before the test, which is what keeps the predicate no stricter than the provider's own\n * sanitizing — the invariant that makes refusing safe to add, since it means this can only reject a name the\n * provider would have altered, never one it would have resolved correctly. Leading and trailing whitespace AND\n * control characters both qualify: a sanitizer maps a control character to a space, then collapses and trims, so\n * `'gitkraken\\n'` and `'gitkraken\\u0000'` alike emit `org:gitkraken` — the scope that was asked for. Note this\n * is wider than `String.trim()`, which leaves control characters in place.\n *\n * None of the three outcomes above survives the stripping, so the rule loses nothing: an all-edge value still\n * empties, an INNER space or control character still splits (`'git\\u0000kraken'` emits `git kraken`, two\n * tokens), and a quote still alters wherever it sits.\n */\nfunction isUsableSearchScopeName(id: IntegrationIds, name: string): boolean {\n\tconst stripped = stripSearchScopeEdges(name);\n\tif (providersMetadata[id]?.exactSearchScopeNames === true) {\n\t\t// eslint-disable-next-line no-control-regex\n\t\treturn stripped.length > 0 && !/[\\u0000-\\u001f\\u007f]/.test(stripped);\n\t}\n\n\t// eslint-disable-next-line no-control-regex\n\treturn stripped.length > 0 && !/[\"\\u0000-\\u001f\\u007f\\s]/.test(stripped);\n}\n\n/** The leading/trailing run a provider's sanitizing removes — see {@link isUsableSearchScopeName}. */\nfunction stripSearchScopeEdges(value: string): string {\n\t// eslint-disable-next-line no-control-regex\n\treturn value.replace(/^[\\s\\u0000-\\u001f\\u007f]+|[\\s\\u0000-\\u001f\\u007f]+$/g, '');\n}\n\n/**\n * The scope names a search cannot carry as given, as the strings to name in the refusal — empty when every one is\n * usable. See {@link isUsableSearchScopeName} for the rule and why it refuses rather than sanitizes.\n */\nfunction unusableSearchScopeNames(\n\tid: IntegrationIds,\n\torg: string | undefined,\n\trepos: readonly ProviderRepoInput[] | undefined,\n): string[] {\n\tconst unusable: string[] = [];\n\n\t// An EMPTY org is \"no org supplied\" and falls through to the remaining scopes; any other unusable value WAS\n\t// supplied, so it is refused rather than dropped.\n\tif (org != null && org.length > 0 && !isUsableSearchScopeName(id, org)) {\n\t\tunusable.push(org);\n\t}\n\n\t// A `repo:` qualifier names a repository by its JOINED `namespace/name` path, which is the value the rule\n\t// below is applied to. Both halves are read defensively, through the SAME locals the label is built from: the\n\t// descriptor form is only narrowed from a union by an element-type check, so a half-built descriptor reaches\n\t// here as `undefined` and must refuse rather than throw out of a facade that reports refusals as warnings —\n\t// and reporting it as `undefined/a` would name a value the caller never passed.\n\tfor (const repo of repos ?? []) {\n\t\tconst namespace = repo.namespace ?? '';\n\t\tconst name = repo.name ?? '';\n\t\tconst path = `${namespace}/${name}`;\n\t\t// BOTH the composite and each half, because neither sees what the other does:\n\t\t// - the composite catches an offender the halves cannot, since an edge of a half is an INTERIOR character\n\t\t// of the path — `{ 'git ', 'kraken' }` has two usable-looking halves and emits `repo:git /kraken`.\n\t\t// - the halves catch a BLANK one the composite cannot, since that offender sits at a composite EDGE where\n\t\t// stripping removes it — `' /a'` strips to `'/a'`, a perfectly spellable qualifier naming no repository.\n\t\t// Each half is measured after the same stripping, so `' '` and `''` are one case rather than two.\n\t\tif (\n\t\t\t!isUsableSearchScopeName(id, path) ||\n\t\t\tstripSearchScopeEdges(namespace).length === 0 ||\n\t\t\tstripSearchScopeEdges(name).length === 0\n\t\t) {\n\t\t\tunusable.push(path);\n\t\t}\n\t}\n\n\treturn unusable;\n}\n\n/**\n * Validates that a filtered issue search is scoped at all, and narrows `repos` to the descriptor form the\n * provider query needs.\n *\n * Shared by `searchIssuesPage` and `countIssues` because a count computed under different constraints than the\n * read it previews is a WRONG number rather than a missing one. The rejection is returned as a reason code, not a\n * warning: the two callers word it differently (whole-read vs naming the offending scope's key), and wording is\n * the warning layer's business.\n *\n * `repo-ids` and `unscoped` are mutually exclusive — one requires repositories and the other requires none — so\n * their relative order is free. `unusable-scope` is NOT: it must precede `unscoped`, so a value that was supplied\n * but cannot be used is never reported as a missing one.\n *\n * Every scope name is checked for naming the same scope after the provider sanitizes it, not merely for being\n * non-empty, and that distinction is a SECURITY one rather than a nicety — see {@link isUsableSearchScopeName}.\n */\nexport function resolveIssueSearchScope(\n\tid: IntegrationIds,\n\trepos: ProviderReposInput | undefined,\n\torg: string | undefined,\n\tcriteria: IssueSearchCriteria | undefined,\n): { rejection?: IssueSearchScopeRejection; repos?: ProviderRepoInput[] } {\n\tlet resolvedRepos: ProviderRepoInput[] | undefined;\n\n\tif (repos?.length) {\n\t\t// `ProviderReposInput` is a union of descriptor and id arrays; only the descriptor form is usable here.\n\t\tif (repos.some(r => typeof r === 'string' || typeof r === 'number')) {\n\t\t\treturn { rejection: { reason: 'repo-ids' } };\n\t\t}\n\n\t\tresolvedRepos = repos as ProviderRepoInput[];\n\t}\n\n\t// Checked BEFORE the \"is it scoped at all\" rule below, so an unusable value is never reported as a missing\n\t// one: it was supplied, and telling the caller to pass a scope it already passed names the wrong defect.\n\tconst unusable = unusableSearchScopeNames(id, org, resolvedRepos);\n\tif (unusable.length > 0) return { rejection: { reason: 'unusable-scope', scopes: unusable } };\n\n\tif (resolvedRepos != null) return { repos: resolvedRepos };\n\tif (org != null && org.length > 0) return {};\n\tif (criteria?.relationships?.some(r => userScopingIssueSearchRelationships.includes(r)) === true) return {};\n\n\treturn { rejection: { reason: 'unscoped' } };\n}\n\n/**\n * What {@link getSupportedFilters} reports: every vocabulary a provider's reads can express, so a consumer can\n * narrow to it BEFORE issuing a read. See that function for the capability table itself and why intersecting\n * against this is what keeps a filtered read from being refused.\n *\n * Named rather than written inline, because the same shape is the declared return type of THREE declarations that\n * all describe one value — this function, `IntegrationService.getSupportedFilters`, which only forwards to it, and\n * `IntegrationManager.getSupportedFilters`, which publishes it. Spelled out three times, a member added here\n * reached one of them and left the others a compile error away from the truth; that is exactly how the two sort\n * members below came to be written out three times.\n */\nexport type SupportedFilters = {\n\tpullRequests: PullRequestFilter[];\n\tpullRequestsAccountWide: PullRequestFilter[];\n\t/**\n\t * Criteria and scopes the filtered pull-request search can express. Always present; an empty relationship list\n\t * means the provider exposes no filtered pull-request search.\n\t */\n\tpullRequestSearch: PullRequestSearchCapabilities;\n\t/**\n\t * Filters the repo-scoped issue read accepts — and, for an issue tracker (Jira/Linear/Trello), its\n\t * project-scoped read, that being its only issue surface. A tracker therefore reports here and leaves\n\t * `issuesAccountWide` empty, so intersecting a tracker against that field would read \"cannot filter\" for a\n\t * provider that filters fine.\n\t */\n\tissues: IssueFilter[];\n\tissuesAccountWide: IssueFilter[];\n\t/**\n\t * What the filtered issue search (and the count probe over the same criteria) can express. Always present: a\n\t * provider with no filtered issue search reports an empty `relationships` and all-false flags, which is the\n\t * signal to hide that surface rather than to hide individual chips.\n\t */\n\tissueSearch: IssueSearchCapabilities;\n\t/**\n\t * Sort keys the repo-scoped issue read can express — and, for an issue tracker, its project-scoped read. Empty\n\t * means the read can't be ordered, so pass no `sort`.\n\t *\n\t * A key here is expressible on ONE provider query. A page spanning several repositories or projects is a merge,\n\t * and a merge can only order by what a normalized issue carries, so `priority`/`dueDate`/`resolved` are refused\n\t * there even where they are listed. Read one scope at a time to use them.\n\t */\n\tissueSorts: IssueSorting[];\n\t/**\n\t * Sort keys the account-wide issue read can express — a different vocabulary, not a subset: for GitLab the two\n\t * reads are different APIs. Needs no mergeability caveat, because every account-wide read is a union of several\n\t * queries, so only keys a merge can honor are listed at all. Empty for an issue tracker, which reports under\n\t * `issueSorts`.\n\t */\n\tissueSortsAccountWide: IssueSorting[];\n\t/**\n\t * Whether an issue tracker's reads can be asked for a `state` other than open (`closed`, `all`). False means\n\t * only open issues are readable, and the read refuses the other two rather than serving the open list.\n\t *\n\t * Tracker-only, like `issues`: the git hosts' state support belongs to the filtered search, under\n\t * `issueSearch.states`.\n\t */\n\tissueStates: boolean;\n};\n\n/**\n * The filters `listPullRequestsPage`/`listIssuesPage` (and the sweeps) accept for a provider, so a caller can\n * narrow to what the provider can express BEFORE issuing the read.\n *\n * This matters because the filter contract is all-or-nothing: a set containing even one unsupported filter is\n * refused outright ({@link resolvePullRequestFilters}) rather than silently narrowed, since falling through to\n * an unfiltered fetch would return every PR instead of the user's. Without this accessor a consumer had to\n * hardcode its own copy of the table — reachable only by importing the internal `providers/models.js`\n * subpath — and a copy that drifts turns a supported read into an empty page with `fetchFailed`.\n *\n * Empty means \"no filter of that kind is expressible\": either the provider has no such surface (issue trackers\n * have no pull requests; Bitbucket exposes no issues) or its metadata declares none. Callers should treat it as\n * \"don't pass filters\", not as an error. Returns copies, so mutating the result can't corrupt the metadata.\n *\n * `issues` and `issuesAccountWide` are separate because the repo-scoped and account-wide issue reads are\n * different provider queries with different filter surfaces, and the same `filters` input is validated against\n * whichever one the read uses (`repos` present or not). `issuesAccountWide` is generally the narrower of the\n * two: GitLab, for instance, can express `Assignee` and `Author` account-wide, but not `Mention`.\n *\n * That split describes the GIT-HOST reads only. An issue tracker (Jira/Linear/Trello) has neither — its issues\n * live under resource → project — so it reports its filters under `issues`, which is what\n * {@link IntegrationService.listIssueTrackerIssuesPage} validates against, and leaves `issuesAccountWide`\n * empty. Reading a tracker's capability off `issuesAccountWide` therefore under-reports it.\n *\n * `issueSearch` is a third, wider surface: the FILTERED issue search (`searchIssuesPage`, and the `countIssues`\n * probe over the same criteria), which is not bound to the user at all. It is a record of per-criterion flags\n * rather than a list, because its criteria aren't a single kind. An empty `relationships` means the provider has\n * no filtered issue search — hide the surface, not just its chips.\n *\n * `pullRequestSearch` describes the separate filtered PR search. Its table declares each criteria vocabulary\n * plus repository/organization scope support; an empty relationship list means the search itself is absent.\n *\n * `issueSorts` / `issueSortsAccountWide` / `issueSearch.sorts` are the ORDERING vocabulary of those same three\n * issue reads, split the same way and for the same reason (GitLab's repo-scoped read is GraphQL and its\n * account-wide read is REST, with genuinely different sort vocabularies). Empty means the read can't be ordered,\n * so pass no `sort`; a key not listed refuses the whole read exactly like an inexpressible filter. Note a key\n * listed here is expressible on ONE provider query: a read that fans out across projects can only honor a key\n * derivable from a normalized issue, so it refuses `priority`/`dueDate`/`resolved` on top of this table.\n *\n * Note this is a CAPABILITY table — \"what the provider can express\" — not a recommendation. A consumer\n * matching another tool's behavior may deliberately pass fewer filters than are listed here (or none, where an\n * already-scoped read would only be narrowed by them). Intersecting against this table is what keeps a\n * filtered read from being refused; it isn't a directive to use every filter in it.\n */\nexport function getSupportedFilters(providerId: IntegrationIds): SupportedFilters {\n\tconst metadata = providersMetadata[providerId];\n\tconst issueSearch = metadata?.supportedIssueSearch;\n\tconst pullRequestSearch = metadata?.supportedPullRequestSearch;\n\treturn {\n\t\tpullRequests: [...(metadata?.supportedPullRequestFilters ?? [])],\n\t\tpullRequestsAccountWide: [...(metadata?.supportedAccountWidePullRequestFilters ?? [])],\n\t\tpullRequestSearch: {\n\t\t\t...unsupportedPullRequestSearchCapabilities,\n\t\t\t...pullRequestSearch,\n\t\t\trelationships: [...(pullRequestSearch?.relationships ?? [])],\n\t\t\tstates: [...(pullRequestSearch?.states ?? [])],\n\t\t\t// Copied, so mutating the result can't corrupt the metadata table.\n\t\t\tsorts: [...(pullRequestSearch?.sorts ?? [])],\n\t\t},\n\t\tissues: [...(metadata?.supportedIssueFilters ?? [])],\n\t\tissuesAccountWide: [...(metadata?.supportedAccountWideIssueFilters ?? [])],\n\t\t// Always an object, never `undefined`: a provider WITHOUT a filtered issue search reports one whose\n\t\t// `relationships` is empty and whose flags are all false, so a consumer reads capabilities the same way\n\t\t// for every provider (and an empty `relationships` is the signal to hide the surface itself).\n\t\t//\n\t\t// Spread over the all-false baseline rather than defaulting each flag: a criterion added to\n\t\t// `IssueSearchCapabilities` then can't be forgotten here and silently reported as `undefined` (which a\n\t\t// consumer's `if (caps.x)` would read as unsupported — right answer, wrong reason, and untyped).\n\t\t// `supportedIssueSearch` is typed as the complete shape, so no member can arrive as an explicit undefined.\n\t\tissueSearch: {\n\t\t\t...unsupportedIssueSearchCapabilities,\n\t\t\t...issueSearch,\n\t\t\t// Copied, so mutating the result can't corrupt the metadata table.\n\t\t\trelationships: [...(issueSearch?.relationships ?? [])],\n\t\t\tsorts: [...(issueSearch?.sorts ?? [])],\n\t\t},\n\t\tissueSorts: [...(metadata?.supportedIssueSorts ?? [])],\n\t\tissueSortsAccountWide: [...(metadata?.supportedAccountWideIssueSorts ?? [])],\n\t\tissueStates: metadata?.supportsIssueStates === true,\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IssueShape, IssueSorting } from "../../../git/models/issue.js";
|
|
1
|
+
import { IssueShape, IssueSorting, IssueStateFilter } from "../../../git/models/issue.js";
|
|
2
2
|
import { IntegrationIds } from "../constants.js";
|
|
3
3
|
import { IssueFilter } from "../providerFilters.js";
|
|
4
4
|
import { ProviderPagedResult } from "../results.js";
|
|
@@ -29,6 +29,14 @@ export declare function listIssueTrackerIssuesPage(ctx: ProviderReadContext, opt
|
|
|
29
29
|
* ordered. Only the order within a page changes.
|
|
30
30
|
*/
|
|
31
31
|
sort?: IssueSorting;
|
|
32
|
+
/**
|
|
33
|
+
* Which issue states to read. Omitted reads open issues only.
|
|
34
|
+
*
|
|
35
|
+
* Applied server-side by every project read, so it narrows each project's drain rather than the merged page.
|
|
36
|
+
* Only a tracker declaring `ProviderMetadata.supportsIssueStates` can express anything but `'open'`; asking
|
|
37
|
+
* another for `'closed'`/`'all'` refuses the read rather than serving its open issues.
|
|
38
|
+
*/
|
|
39
|
+
state?: IssueStateFilter;
|
|
32
40
|
forceSync?: boolean;
|
|
33
41
|
page?: number;
|
|
34
42
|
cursor?: string;
|
|
@@ -13,6 +13,11 @@ import { isIssuesIntegration } from "../models/issuesIntegration.js";
|
|
|
13
13
|
import { projectKey, resourceIdForProject, resourceLabel, resourceMatchesOrg } from "./hierarchy.utils.js";
|
|
14
14
|
import { resolveIssueSort, toIssueOrdering } from "./ordering.js";
|
|
15
15
|
//#region ../plus/integrations/src/reads/issueTracker.ts
|
|
16
|
+
const issueStateFilters = [
|
|
17
|
+
"open",
|
|
18
|
+
"closed",
|
|
19
|
+
"all"
|
|
20
|
+
];
|
|
16
21
|
async function listIssueTrackerIssuesPage(ctx, options) {
|
|
17
22
|
const compositeCursor = parseIssueTrackerPageCursor(options.cursor);
|
|
18
23
|
const paginated = compositeCursor?.unpaged !== true && (options.page != null || options.cursor != null || options.itemsPerPage != null);
|
|
@@ -27,7 +32,8 @@ async function listIssueTrackerIssuesPage(ctx, options) {
|
|
|
27
32
|
nextPage: retry.nextPage,
|
|
28
33
|
retryPages: retry.pages ?? [],
|
|
29
34
|
retryProjects: retry.projects ?? [],
|
|
30
|
-
completedProjects: retry.completedProjects ?? []
|
|
35
|
+
completedProjects: retry.completedProjects ?? [],
|
|
36
|
+
state: options.state
|
|
31
37
|
}) : void 0;
|
|
32
38
|
return {
|
|
33
39
|
items,
|
|
@@ -74,6 +80,15 @@ async function listIssueTrackerIssuesPage(ctx, options) {
|
|
|
74
80
|
return emptyPage(true);
|
|
75
81
|
}
|
|
76
82
|
const sort = resolvedSort.sort;
|
|
83
|
+
if (options.state != null && (!issueStateFilters.includes(options.state) || options.state !== "open" && !providersMetadata[options.providerId]?.supportsIssueStates)) {
|
|
84
|
+
warnings.push(otherWarning(options.providerId, domain, options.connectionId, issueStateFilters.includes(options.state) ? `Issue state '${options.state}' is not supported by '${options.providerId}'.` : `Unknown issue state; expected one of ${issueStateFilters.join(", ")}.`));
|
|
85
|
+
return emptyPage(true);
|
|
86
|
+
}
|
|
87
|
+
const cursorState = compositeCursor != null ? compositeCursor.state ?? "open" : options.cursor != null ? "open" : void 0;
|
|
88
|
+
if (cursorState != null && cursorState !== (options.state ?? "open")) {
|
|
89
|
+
warnings.push(otherWarning(options.providerId, domain, options.connectionId, "The cursor belongs to a read of another issue state; start again without it."));
|
|
90
|
+
return emptyPage(true);
|
|
91
|
+
}
|
|
77
92
|
await ctx.forceRefreshIfRequested(integration, options.forceSync, options.connectionId);
|
|
78
93
|
const { value: resources, warning: resourcesWarning } = await runCaptured(options.providerId, domain, options.connectionId, () => integration.getResourcesForUserResult(options.connectionId));
|
|
79
94
|
if (resourcesWarning != null) warnings.push(resourcesWarning);
|
|
@@ -198,7 +213,8 @@ async function listIssueTrackerIssuesPage(ctx, options) {
|
|
|
198
213
|
user: userForProject(project),
|
|
199
214
|
userId: userIdForProject(project),
|
|
200
215
|
filters: options.filters,
|
|
201
|
-
sort
|
|
216
|
+
sort,
|
|
217
|
+
state: options.state
|
|
202
218
|
}, options.connectionId))
|
|
203
219
|
}));
|
|
204
220
|
let fetchFailed = projectDiscoveryFailed || accountLookupFailed;
|
|
@@ -246,7 +262,8 @@ async function listIssueTrackerIssuesPage(ctx, options) {
|
|
|
246
262
|
nextPage,
|
|
247
263
|
retryPages,
|
|
248
264
|
retryProjects: [...retryProjectKeys],
|
|
249
|
-
completedProjects: (trackCompletedProjects || projectDiscoveryFailed || projectDiscoveryTruncated || retryProjectKeys.size > 0) && (retryPages.length > 0 || nextPage != null) ? [...completedProjectKeys] : []
|
|
265
|
+
completedProjects: (trackCompletedProjects || projectDiscoveryFailed || projectDiscoveryTruncated || retryProjectKeys.size > 0) && (retryPages.length > 0 || nextPage != null) ? [...completedProjectKeys] : [],
|
|
266
|
+
state: options.state
|
|
250
267
|
});
|
|
251
268
|
return {
|
|
252
269
|
items: orderedItems,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"issueTracker.js","names":[],"sources":["../../../../../plus/integrations/src/reads/issueTracker.ts"],"sourcesContent":["import type { CollectionMetadata } from '@gitkraken/provider-apis';\nimport type { IssueShape, IssueSorting } from '@gitlens/git/models/issue.js';\nimport type { ResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport { mapBounded } from '@gitlens/utils/promise.js';\nimport { assessCollectionMetadata, mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { providerFanOutConcurrency } from '../constants.js';\nimport { isIssuesIntegration } from '../models/issuesIntegration.js';\nimport { IssueFilter, providersMetadata } from '../providers/models.js';\nimport { mergeCollectionMetadata, parsePageCursor } from '../providers/utils/providerPaging.js';\nimport type { ProviderPagedResult, ProviderWarning } from '../results.js';\nimport { appendDedupedWarning, reconcileOmissionsWithFailure } from '../results.js';\nimport { isIssuesHostIntegrationId } from '../utils/integration.utils.js';\nimport type { ProviderReadContext } from './context.js';\nimport { parseIssueTrackerPageCursor, toIssueTrackerPageCursor } from './cursors.js';\nimport { runCaptured } from './drains.js';\nimport { projectKey, resourceIdForProject, resourceLabel, resourceMatchesOrg } from './hierarchy.utils.js';\nimport { resolveIssueSort, toIssueOrdering } from './ordering.js';\nimport {\n\tincompleteReadWarning,\n\tissueTrackerOnlySurfaceWarning,\n\totherWarning,\n\tunmergeableIssueSortWarning,\n\tunsupportedIssueSortWarning,\n} from './warnings.js';\n\nexport async function listIssueTrackerIssuesPage(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\torg?: string;\n\t\tproject?: string;\n\t\tfilters?: IssueFilter[];\n\t\t/** Broadens the read to every assignee. Scopes to user-assigned issues when omitted. */\n\t\tincludeAllAssignees?: boolean;\n\t\t/**\n\t\t * How to order the issues, as `field:direction`. Omitted orders most-recently-updated-first where the\n\t\t * tracker can express it.\n\t\t *\n\t\t * Validated against `getSupportedFilters().issues`' sibling `issueSorts` — a tracker reports there, not\n\t\t * under the account-wide table, because resource -> project IS its only issue surface. A key it can't\n\t\t * express refuses the read rather than serving a differently-ordered list.\n\t\t *\n\t\t * One page can span SEVERAL projects, whose issues are merged here, so a key no normalized issue carries\n\t\t * (`priority`, `dueDate`, `resolved`) is refused for a multi-project page even though the tracker orders by\n\t\t * it perfectly well within one project.\n\t\t *\n\t\t * Unlike the git-host reads this one is safe to change mid-pagination: its cursor windows PROJECTS and\n\t\t * drains each to exhaustion, so which projects a round covers doesn't depend on how their issues are\n\t\t * ordered. Only the order within a page changes.\n\t\t */\n\t\tsort?: IssueSorting;\n\t\tforceSync?: boolean;\n\t\tpage?: number;\n\t\tcursor?: string;\n\t\titemsPerPage?: number;\n\t\tconnectionId?: string;\n\t\t/**\n\t\t * Explicit self-managed tracker host. Used to select the instance when the requested connection has no\n\t\t * configured domain; it must come from the trusted authentication configuration, never from repository\n\t\t * or remote data. Ignored for the cloud trackers.\n\t\t */\n\t\tdomain?: string;\n\t},\n): Promise<ProviderPagedResult<IssueShape>> {\n\t// Pagination is opt-in: only window the projects when the caller actually asked to page. A caller that\n\t// passes none of page/cursor/itemsPerPage keeps the \"aggregate every matched project\" contract, so an\n\t// existing consumer that doesn't inspect `hasMore` never silently loses projects past a default window.\n\tconst compositeCursor = parseIssueTrackerPageCursor(options.cursor);\n\tconst paginated =\n\t\tcompositeCursor?.unpaged !== true &&\n\t\t(options.page != null || options.cursor != null || options.itemsPerPage != null);\n\t// A composite cursor can carry retries from earlier project windows alongside the next untouched window.\n\t// `currentPage` remains the caller-facing position; `nextPage` is the window this round should advance.\n\tconst page = Math.max(\n\t\t1,\n\t\tMath.trunc(compositeCursor?.currentPage ?? parsePageCursor(options.cursor) ?? options.page ?? 1),\n\t);\n\tconst projectsPerPage = Math.max(1, Math.trunc(options.itemsPerPage ?? 20));\n\n\tconst items: IssueShape[] = [];\n\tconst warnings: ProviderWarning[] = [];\n\tconst emptyPage = (\n\t\tfetchFailed?: boolean,\n\t\ttruncated?: boolean,\n\t\tretry?: {\n\t\t\tnextPage?: number;\n\t\t\tpages?: readonly number[];\n\t\t\tprojects?: readonly string[];\n\t\t\tcompletedProjects?: readonly string[];\n\t\t},\n\t): ProviderPagedResult<IssueShape> => {\n\t\tconst cursor =\n\t\t\tretry != null\n\t\t\t\t? toIssueTrackerPageCursor({\n\t\t\t\t\t\tcurrentPage: page + 1,\n\t\t\t\t\t\tunpaged: !paginated,\n\t\t\t\t\t\tnextPage: retry.nextPage,\n\t\t\t\t\t\tretryPages: retry.pages ?? [],\n\t\t\t\t\t\tretryProjects: retry.projects ?? [],\n\t\t\t\t\t\tcompletedProjects: retry.completedProjects ?? [],\n\t\t\t\t\t})\n\t\t\t\t: undefined;\n\t\treturn {\n\t\t\titems: items,\n\t\t\twarnings: warnings,\n\t\t\tpage: { currentPage: page, itemsPerPage: items.length, truncated: truncated || undefined },\n\t\t\t// Retry-only state is deliberately manual: advertising it as forward progress would make a\n\t\t\t// persistent provider failure spin forever in a normal `while (hasMore)` consumer. An untouched\n\t\t\t// project window remains normal forward progress even when this window failed completely.\n\t\t\thasMore: retry?.nextPage != null && cursor != null,\n\t\t\tcursor: cursor,\n\t\t\tfetchFailed: fetchFailed || undefined,\n\t\t};\n\t};\n\tconst normalWindowPage = paginated\n\t\t? (compositeCursor?.nextPage ?? (compositeCursor == null ? page : undefined))\n\t\t: undefined;\n\tconst priorRetryPages = compositeCursor?.retryPages ?? [];\n\tconst priorRetryProjects = compositeCursor?.retryProjects ?? [];\n\tconst priorCompletedProjects = compositeCursor?.completedProjects ?? [];\n\tconst trackCompletedProjects =\n\t\tpriorCompletedProjects.length > 0 || priorRetryPages.length > 0 || compositeCursor?.completedProjects != null;\n\tconst retryPagesForDiscoveryFailure = (): number[] => [\n\t\t...priorRetryPages,\n\t\t...(normalWindowPage != null ? [normalWindowPage] : compositeCursor == null ? [1] : []),\n\t];\n\tconst discoveryFailureRetry = (): {\n\t\tpages: readonly number[];\n\t\tprojects: readonly string[];\n\t\tcompletedProjects: readonly string[];\n\t} => ({\n\t\tpages: retryPagesForDiscoveryFailure(),\n\t\tprojects: priorRetryProjects,\n\t\tcompletedProjects: priorCompletedProjects,\n\t});\n\n\tif (!isIssuesHostIntegrationId(options.providerId)) {\n\t\twarnings.push(\n\t\t\tissueTrackerOnlySurfaceWarning(options.providerId, options.connectionId, 'Issue-tracker project reads'),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\t// A supplied connectionId or domain that no longer resolves is a broken target, not an empty account.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\twarnings.push(...early.warnings);\n\t\treturn emptyPage(early.fetchFailed);\n\t}\n\tif (!isIssuesIntegration(integration)) {\n\t\twarnings.push(\n\t\t\tissueTrackerOnlySurfaceWarning(options.providerId, options.connectionId, 'Issue-tracker project reads'),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\n\t// Before any upstream request: the discovery fan-outs below (resources, projects, per-resource accounts) are\n\t// three round trips, and an order this tracker can't express refuses the read whatever they return. Placed after\n\t// `domain` only because every warning carries it.\n\tconst resolvedSort = resolveIssueSort(providersMetadata[options.providerId]?.supportedIssueSorts, options.sort);\n\tif (resolvedSort.rejection != null) {\n\t\twarnings.push(\n\t\t\tunsupportedIssueSortWarning(options.providerId, domain, options.connectionId, resolvedSort.rejection),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\tconst sort = resolvedSort.sort;\n\n\tawait ctx.forceRefreshIfRequested(integration, options.forceSync, options.connectionId);\n\n\tconst { value: resources, warning: resourcesWarning } = await runCaptured(\n\t\toptions.providerId,\n\t\tdomain,\n\t\toptions.connectionId,\n\t\t() => integration.getResourcesForUserResult(options.connectionId),\n\t);\n\tif (resourcesWarning != null) {\n\t\twarnings.push(resourcesWarning);\n\t}\n\tif (resources == null || resources.length === 0) {\n\t\tconst fetchFailed = resourcesWarning != null && resources == null;\n\t\treturn emptyPage(fetchFailed, false, fetchFailed ? discoveryFailureRetry() : undefined);\n\t}\n\n\tconst scopedResources =\n\t\toptions.org != null ? resources.filter(r => resourceMatchesOrg(r, options.org!)) : resources;\n\tif (scopedResources.length === 0) {\n\t\treturn emptyPage();\n\t}\n\n\tconst { value: projectsResult, warning: projectsWarning } = await runCaptured(\n\t\toptions.providerId,\n\t\tdomain,\n\t\toptions.connectionId,\n\t\t() => integration.getProjectsForResourcesWithMetadataResult(scopedResources, options.connectionId),\n\t);\n\tif (projectsWarning != null) {\n\t\twarnings.push(projectsWarning);\n\t}\n\t// Partial project discovery: continue with the resources that succeeded, but surface per-resource\n\t// failures as warnings and remember to mark the page fetchFailed so the caller knows some issues may be\n\t// missing. `projectDiscoveryFailed`/`projectDiscoveryTruncated` are OR-ed into the page's\n\t// fetchFailed/truncated at every return below (a truncated-but-not-failed discovery, e.g. a paging\n\t// backstop, still means the project set is incomplete).\n\tconst projectDiscoveryAssessment = mergeAssessmentInto(\n\t\twarnings,\n\t\toptions.providerId,\n\t\tdomain,\n\t\toptions.connectionId,\n\t\tprojectsResult?.metadata,\n\t);\n\tconst projectDiscoveryFailed = projectDiscoveryAssessment.fetchFailed;\n\tconst projectDiscoveryTruncated = projectDiscoveryAssessment.truncated;\n\tconst projects = projectsResult?.values;\n\tif (projects == null || projects.length === 0) {\n\t\tconst fetchFailed = (projectsWarning != null && projectsResult == null) || projectDiscoveryFailed;\n\t\treturn emptyPage(\n\t\t\tfetchFailed,\n\t\t\tprojectDiscoveryTruncated,\n\t\t\tfetchFailed || projectDiscoveryTruncated ? discoveryFailureRetry() : undefined,\n\t\t);\n\t}\n\n\tconst matchedProjects =\n\t\toptions.project != null ? projects.filter(p => resourceMatchesOrg(p, options.project!)) : projects;\n\n\t// Validate the requested filters against what this provider supports (e.g. Linear/Trello support only\n\t// Assignee). An unsupported filter must not silently degrade — Linear/Trello ignore the requested type\n\t// and apply Assignee regardless — so warn + fetchFailed instead of returning a differently-scoped set.\n\tif (options.filters?.length) {\n\t\tconst supported = providersMetadata[options.providerId]?.supportedIssueFilters;\n\t\tconst allSupported = supported != null && options.filters.every(f => supported.includes(f));\n\t\tif (!allSupported) {\n\t\t\twarnings.push(\n\t\t\t\totherWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t`One or more requested issue filters are not supported by '${options.providerId}'.`,\n\t\t\t\t),\n\t\t\t);\n\t\t\treturn emptyPage(true);\n\t\t}\n\t}\n\n\t// `includeAllAssignees` drops the user scope, but a user-relative filter (Author/Mention) is meaningless\n\t// without a user. Passing both to the provider degrades silently: Jira, seeing no user, falls through to\n\t// an unscoped project fetch and returns EVERY issue instead of the requested author's/mentions. Reject\n\t// the incompatible combination up front rather than publishing a differently-scoped set as the result.\n\tif (options.includeAllAssignees === true && options.filters?.some(f => f !== IssueFilter.Assignee)) {\n\t\twarnings.push(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`\\`includeAllAssignees\\` cannot be combined with an author/mention filter for '${options.providerId}' (those filters require a user scope).`,\n\t\t\t),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\t// Scope to the current user's assigned issues unless the caller broadens to all assignees. Resolve the\n\t// handle from each resource's own account (multi-account safe), capturing any error so its kind\n\t// (e.g. auth) is preserved rather than collapsed to a generic warning.\n\tlet usersByResourceId: Map<string, string> | undefined;\n\t// The same accounts keyed by the same resource, holding the provider's stable id rather than the handle. A\n\t// separate map rather than a widened value because the two are resolved and consumed independently: a\n\t// provider may carry an account with no id, and `scopedProjectsWithUsers` gates on the HANDLE alone, so\n\t// folding them together would drop a resource whose id happened to be missing.\n\tlet userIdsByResourceId: Map<string, string> | undefined;\n\tlet accountLookupFailed = false;\n\tif (options.includeAllAssignees !== true) {\n\t\tusersByResourceId = new Map<string, string>();\n\t\tuserIdsByResourceId = new Map<string, string>();\n\t\tconst accounts = await mapBounded(scopedResources, providerFanOutConcurrency, async resource => ({\n\t\t\tresource: resource,\n\t\t\t...(await runCaptured(options.providerId, domain, options.connectionId, () =>\n\t\t\t\tintegration.getAccountForResourceResult(resource, options.connectionId),\n\t\t\t)),\n\t\t}));\n\n\t\tfor (const { resource, value: account, warning: accountWarning } of accounts) {\n\t\t\tconst user = account?.username ?? account?.name ?? undefined;\n\t\t\tif (user != null) {\n\t\t\t\tconst resourceKey = resourceIdForProject(resource) ?? resource.key;\n\t\t\t\tusersByResourceId.set(resourceKey, user);\n\t\t\t\t// Empty-checked, not just null-checked: an account shape declares `id` as a plain string, and a\n\t\t\t\t// provider with nothing to put there sets `''` rather than omitting it — which would otherwise\n\t\t\t\t// reach a user-field query as an empty identity and match nothing.\n\t\t\t\tif (account?.id != null && account.id !== '') {\n\t\t\t\t\tuserIdsByResourceId.set(resourceKey, account.id);\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Deduped: a captured `accountWarning` carries only the provider error, with no resource in its\n\t\t\t// message, so one expired token across N resources would otherwise repeat verbatim N times. The\n\t\t\t// synthesized fallback names the resource, so those stay distinct on their own.\n\t\t\tappendDedupedWarning(\n\t\t\t\twarnings,\n\t\t\t\taccountWarning ??\n\t\t\t\t\totherWarning(\n\t\t\t\t\t\toptions.providerId,\n\t\t\t\t\t\tdomain,\n\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t\t`Could not resolve the current user for '${resourceLabel(resource)}'; skipping that resource to avoid returning issues assigned to others.`,\n\t\t\t\t\t),\n\t\t\t);\n\t\t\taccountLookupFailed = true;\n\t\t}\n\t}\n\n\tconst fallbackUserForUnscopedProject =\n\t\tscopedResources.length === 1 && usersByResourceId?.size === 1\n\t\t\t? usersByResourceId.values().next().value\n\t\t\t: undefined;\n\tconst userForProject = (project: ResourceDescriptor): string | undefined => {\n\t\tconst resourceId = resourceIdForProject(project);\n\t\tif (resourceId != null) {\n\t\t\tconst user = usersByResourceId?.get(resourceId);\n\t\t\tif (user != null) {\n\t\t\t\treturn user;\n\t\t\t}\n\t\t}\n\n\t\t// Some providers/tests return project descriptors without their parent resource id. When we have only\n\t\t// one scoped resource, re-use that sole resolved user rather than silently dropping every project.\n\t\treturn fallbackUserForUnscopedProject;\n\t};\n\t// Deliberately NOT falling back the way `userForProject` does. That fallback exists so a project with no\n\t// parent resource id still gets scoped rather than dropped, and the handle is what the drop is judged on;\n\t// the id is optional everywhere it is read, so a miss degrades to the handle instead of widening the read.\n\tconst userIdForProject = (project: ResourceDescriptor): string | undefined => {\n\t\tconst resourceId = resourceIdForProject(project);\n\t\treturn resourceId != null ? userIdsByResourceId?.get(resourceId) : undefined;\n\t};\n\n\tconst retryProjectKeys = new Set<string>();\n\tconst completedProjectKeys = new Set(priorCompletedProjects);\n\tfor (const retryProject of priorRetryProjects) {\n\t\tcompletedProjectKeys.delete(retryProject);\n\t}\n\tconst scopedProjectsWithUsers =\n\t\tusersByResourceId != null\n\t\t\t? matchedProjects.filter(project => {\n\t\t\t\t\tif (userForProject(project) != null) return true;\n\n\t\t\t\t\tconst key = projectKey(project);\n\t\t\t\t\tretryProjectKeys.add(key);\n\t\t\t\t\tcompletedProjectKeys.delete(key);\n\t\t\t\t\treturn false;\n\t\t\t\t})\n\t\t\t: matchedProjects;\n\n\t// Select the untouched window plus any earlier windows/projects that explicitly need retrying. Keys\n\t// include the parent resource so two trackers can reuse the same project id without collapsing.\n\tconst projectsByRetryKey = new Map(scopedProjectsWithUsers.map(project => [projectKey(project), project]));\n\tconst scopedProjectsByKey = new Map<string, ResourceDescriptor>();\n\tconst addScopedProject = (project: ResourceDescriptor): void => {\n\t\tconst key = projectKey(project);\n\t\tif (completedProjectKeys.has(key)) return;\n\n\t\tscopedProjectsByKey.set(key, project);\n\t};\n\tfor (const retryProject of priorRetryProjects) {\n\t\tconst project = projectsByRetryKey.get(retryProject);\n\t\tif (project != null) {\n\t\t\taddScopedProject(project);\n\t\t} else if (projectDiscoveryFailed || projectDiscoveryTruncated) {\n\t\t\tretryProjectKeys.add(retryProject);\n\t\t}\n\t}\n\tconst windowPages = new Set<number>(priorRetryPages);\n\tif (normalWindowPage != null) {\n\t\twindowPages.add(normalWindowPage);\n\t}\n\tif (!paginated && (projectDiscoveryFailed || projectDiscoveryTruncated)) {\n\t\twindowPages.add(1);\n\t}\n\tif (paginated) {\n\t\tfor (const windowPage of windowPages) {\n\t\t\tconst windowStart = (windowPage - 1) * projectsPerPage;\n\t\t\tfor (const project of scopedProjectsWithUsers.slice(windowStart, windowStart + projectsPerPage)) {\n\t\t\t\taddScopedProject(project);\n\t\t\t}\n\t\t}\n\t} else if (compositeCursor == null || priorRetryPages.length > 0) {\n\t\t// An unpaged retry-only cursor should re-read only the explicitly failed projects already added\n\t\t// above. Discovery retries still rescan the aggregate set so newly recovered projects are included.\n\t\tfor (const project of scopedProjectsWithUsers) {\n\t\t\taddScopedProject(project);\n\t\t}\n\t}\n\tconst scopedProjects = [...scopedProjectsByKey.values()];\n\tconst furthestWindowPage = windowPages.size > 0 ? Math.max(...windowPages) : normalWindowPage;\n\tconst normalWindowEnd =\n\t\tfurthestWindowPage != null ? furthestWindowPage * projectsPerPage : scopedProjectsWithUsers.length;\n\tconst nextPage =\n\t\tpaginated && furthestWindowPage != null && scopedProjectsWithUsers.length > normalWindowEnd\n\t\t\t? furthestWindowPage + 1\n\t\t\t: undefined;\n\tconst retryWindowPages = (): number[] => {\n\t\tif (!projectDiscoveryFailed && !projectDiscoveryTruncated && retryProjectKeys.size === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\tconst pages = new Set(windowPages);\n\t\tif (pages.size === 0) {\n\t\t\t// A terminal retry has no untouched next window to identify its origin. Keep a manual window\n\t\t\t// marker so a later partial discovery can reconcile against the projects already emitted.\n\t\t\tpages.add(paginated ? Math.max(1, page - 1) : 1);\n\t\t}\n\t\treturn [...pages];\n\t};\n\tif (scopedProjects.length === 0) {\n\t\t// The discovered projects didn't intersect the requested filter/window, or every matching resource\n\t\t// failed user resolution. If discovery or account lookup was partial, the empty result is not a\n\t\t// proven-empty account — carry `fetchFailed` so the caller knows issues may be missing.\n\t\treturn emptyPage(projectDiscoveryFailed || accountLookupFailed, projectDiscoveryTruncated, {\n\t\t\tnextPage: nextPage,\n\t\t\tpages: retryWindowPages(),\n\t\t\tprojects: [...retryProjectKeys],\n\t\t\tcompletedProjects: [...completedProjectKeys],\n\t\t});\n\t}\n\n\t// A read spanning several projects is a merge, so it can only honor a key a normalized issue carries. Bound here\n\t// rather than next to the key's validation above, because the project count is only known now — while still\n\t// before any project read, so a refusal costs no request. Refused rather than served as concatenated\n\t// per-project runs, which would look ordered without being so.\n\t//\n\t// Counted over every project the READ covers, not the `scopedProjects` window this page happens to hold: the\n\t// window is `itemsPerPage` projects wide, so counting it would refuse a 20-project page and then serve the\n\t// final 1-project page of the same paginated call — the same key answered two ways, with the earlier pages'\n\t// issues missing from a read that reported success.\n\t//\n\t// Projects are not the only thing a tracker read merges: a user-scoped read asking for several relationships\n\t// issues one provider query PER relationship and unions them within each project (Jira runs one JQL drain per\n\t// filter and folds them into one map, in filter order), so a single-project page of two filters is just as\n\t// concatenated as a two-project one. Counting only projects published those runs under the requested key.\n\t// Not gated on the provider: the two trackers that don't fan out (Linear, Trello) declare `Assignee` as their\n\t// only supported filter, so a multi-filter read is already refused above and can never reach this.\n\tconst mergesFilters = options.includeAllAssignees !== true && (options.filters?.length ?? 0) > 1;\n\tconst ordering = toIssueOrdering(sort, scopedProjectsWithUsers.length > 1 || mergesFilters);\n\tif (ordering.unmergeable != null) {\n\t\twarnings.push(\n\t\t\tunmergeableIssueSortWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\tordering.unmergeable,\n\t\t\t\tscopedProjectsWithUsers.length > 1 ? 'projects' : 'filters',\n\t\t\t),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\tconst perProject = await mapBounded(scopedProjects, providerFanOutConcurrency, async project => ({\n\t\tproject: project,\n\t\t...(await runCaptured(options.providerId, domain, options.connectionId, () =>\n\t\t\tintegration.getIssuesForProjectWithTruncationResult(\n\t\t\t\tproject,\n\t\t\t\t{\n\t\t\t\t\tuser: userForProject(project),\n\t\t\t\t\tuserId: userIdForProject(project),\n\t\t\t\t\tfilters: options.filters,\n\t\t\t\t\tsort: sort,\n\t\t\t\t},\n\t\t\t\toptions.connectionId,\n\t\t\t),\n\t\t)),\n\t}));\n\n\t// Partial project discovery means some projects' issues are missing from this page; propagate it so the\n\t// page reports fetchFailed even when every discovered project's own read succeeded.\n\tlet fetchFailed = projectDiscoveryFailed || accountLookupFailed;\n\tlet truncationRecovery: 'narrow-scope' | 'none' | undefined = projectDiscoveryTruncated ? 'none' : undefined;\n\tlet drainMetadata: CollectionMetadata | undefined;\n\tfor (const { project, value: result, warning } of perProject) {\n\t\tconst key = projectKey(project);\n\t\tconst retryProject = result == null;\n\t\tif (warning != null) {\n\t\t\t// Deduped: the per-project warning is built from the provider error alone and names no project, so a\n\t\t\t// single failing token repeats verbatim once per project in the window.\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t\tfetchFailed = true;\n\t\t\ttruncationRecovery = 'none';\n\t\t}\n\t\t// A thrown/unsupported read (e.g. Linear not-implemented) surfaces as a warning with no value;\n\t\t// mark the aggregate as fetchFailed so an empty result isn't mistaken for \"no issues\".\n\t\tif (result == null) {\n\t\t\tfetchFailed = true;\n\t\t}\n\t\tif (result != null) {\n\t\t\titems.push(...result.values);\n\t\t\tif (result.truncated) {\n\t\t\t\tconst recovery = result.recovery;\n\t\t\t\ttruncationRecovery = truncationRecovery == null || truncationRecovery === recovery ? recovery : 'none';\n\t\t\t}\n\t\t\tif (result.metadata != null) {\n\t\t\t\tdrainMetadata = mergeCollectionMetadata(drainMetadata, result.metadata);\n\t\t\t}\n\t\t}\n\t\tif (retryProject) {\n\t\t\tretryProjectKeys.add(key);\n\t\t\tcompletedProjectKeys.delete(key);\n\t\t} else {\n\t\t\t// A usable partial/truncated project is emitted once and remains structurally incomplete.\n\t\t\t// Re-running the same project cursor would normally return the same prefix and duplicate it\n\t\t\t// across facade pages; only a project that returned no value is safe to retry automatically.\n\t\t\tcompletedProjectKeys.add(key);\n\t\t}\n\t}\n\n\t// Each provider query arrived ordered by the tracker; the union of them — across projects, and across the\n\t// per-relationship queries a multi-filter read fans out into — is not, and the union is what this read\n\t// publishes. A no-op when nothing merged, since the tracker already ordered that single run.\n\tconst orderedItems = ordering.order(items);\n\n\tconst projectReadReportedTruncation = truncationRecovery != null;\n\tconst drainAssessment = assessCollectionMetadata(options.providerId, domain, options.connectionId, drainMetadata);\n\tfor (const warning of drainAssessment.warnings) {\n\t\t// A project result already supplies the recovery semantics. Replace metadata's unstructured fallback with\n\t\t// the structured warning below, while retaining specific failures and omissions.\n\t\tif (projectReadReportedTruncation && !drainAssessment.fetchFailed && warning.omission == null) continue;\n\n\t\tappendDedupedWarning(warnings, warning);\n\t}\n\tfetchFailed = fetchFailed || drainAssessment.fetchFailed;\n\tif (drainAssessment.truncated) {\n\t\ttruncationRecovery = 'none';\n\t}\n\n\tconst projectTruncated = truncationRecovery != null;\n\tconst scopeTooLarge = !fetchFailed && truncationRecovery === 'narrow-scope';\n\tif (scopeTooLarge || (!fetchFailed && projectTruncated && !warnings.some(warning => warning.omission != null))) {\n\t\tappendDedupedWarning(\n\t\t\twarnings,\n\t\t\tincompleteReadWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\tscopeTooLarge\n\t\t\t\t\t? 'Some projects hold more issues than one read can return; narrow the scope to read the rest.'\n\t\t\t\t\t: 'Some issues were omitted; the provider returned an incomplete result.',\n\t\t\t\tscopeTooLarge ? 'scope-too-large' : 'exhausted',\n\t\t\t),\n\t\t);\n\t}\n\t// A metadata omission from an earlier project asserts the read succeeded; a later one may since have failed.\n\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\n\tconst retryPages = retryWindowPages();\n\tconst cursor = toIssueTrackerPageCursor({\n\t\tcurrentPage: page + 1,\n\t\tunpaged: !paginated,\n\t\tnextPage: nextPage,\n\t\tretryPages: retryPages,\n\t\tretryProjects: [...retryProjectKeys],\n\t\tcompletedProjects:\n\t\t\t(trackCompletedProjects ||\n\t\t\t\tprojectDiscoveryFailed ||\n\t\t\t\tprojectDiscoveryTruncated ||\n\t\t\t\tretryProjectKeys.size > 0) &&\n\t\t\t(retryPages.length > 0 || nextPage != null)\n\t\t\t\t? [...completedProjectKeys]\n\t\t\t\t: [],\n\t});\n\treturn {\n\t\titems: orderedItems,\n\t\twarnings: warnings,\n\t\tpage: { currentPage: page, itemsPerPage: orderedItems.length, truncated: projectTruncated || undefined },\n\t\t// Failed-project retries alone are manual. Only an untouched project window is automatic progress.\n\t\thasMore: nextPage != null && cursor != null,\n\t\tcursor: cursor,\n\t\tfetchFailed: fetchFailed || undefined,\n\t};\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"issueTracker.js","names":[],"sources":["../../../../../plus/integrations/src/reads/issueTracker.ts"],"sourcesContent":["import type { CollectionMetadata } from '@gitkraken/provider-apis';\nimport type { IssueShape, IssueSorting, IssueStateFilter } from '@gitlens/git/models/issue.js';\nimport type { ResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport { mapBounded } from '@gitlens/utils/promise.js';\nimport { assessCollectionMetadata, mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { providerFanOutConcurrency } from '../constants.js';\nimport { isIssuesIntegration } from '../models/issuesIntegration.js';\nimport { IssueFilter, providersMetadata } from '../providers/models.js';\nimport { mergeCollectionMetadata, parsePageCursor } from '../providers/utils/providerPaging.js';\nimport type { ProviderPagedResult, ProviderWarning } from '../results.js';\nimport { appendDedupedWarning, reconcileOmissionsWithFailure } from '../results.js';\nimport { isIssuesHostIntegrationId } from '../utils/integration.utils.js';\nimport type { ProviderReadContext } from './context.js';\nimport { parseIssueTrackerPageCursor, toIssueTrackerPageCursor } from './cursors.js';\nimport { runCaptured } from './drains.js';\nimport { projectKey, resourceIdForProject, resourceLabel, resourceMatchesOrg } from './hierarchy.utils.js';\nimport { resolveIssueSort, toIssueOrdering } from './ordering.js';\nimport {\n\tincompleteReadWarning,\n\tissueTrackerOnlySurfaceWarning,\n\totherWarning,\n\tunmergeableIssueSortWarning,\n\tunsupportedIssueSortWarning,\n} from './warnings.js';\n\nconst issueStateFilters: readonly IssueStateFilter[] = ['open', 'closed', 'all'];\n\nexport async function listIssueTrackerIssuesPage(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\torg?: string;\n\t\tproject?: string;\n\t\tfilters?: IssueFilter[];\n\t\t/** Broadens the read to every assignee. Scopes to user-assigned issues when omitted. */\n\t\tincludeAllAssignees?: boolean;\n\t\t/**\n\t\t * How to order the issues, as `field:direction`. Omitted orders most-recently-updated-first where the\n\t\t * tracker can express it.\n\t\t *\n\t\t * Validated against `getSupportedFilters().issues`' sibling `issueSorts` — a tracker reports there, not\n\t\t * under the account-wide table, because resource -> project IS its only issue surface. A key it can't\n\t\t * express refuses the read rather than serving a differently-ordered list.\n\t\t *\n\t\t * One page can span SEVERAL projects, whose issues are merged here, so a key no normalized issue carries\n\t\t * (`priority`, `dueDate`, `resolved`) is refused for a multi-project page even though the tracker orders by\n\t\t * it perfectly well within one project.\n\t\t *\n\t\t * Unlike the git-host reads this one is safe to change mid-pagination: its cursor windows PROJECTS and\n\t\t * drains each to exhaustion, so which projects a round covers doesn't depend on how their issues are\n\t\t * ordered. Only the order within a page changes.\n\t\t */\n\t\tsort?: IssueSorting;\n\t\t/**\n\t\t * Which issue states to read. Omitted reads open issues only.\n\t\t *\n\t\t * Applied server-side by every project read, so it narrows each project's drain rather than the merged page.\n\t\t * Only a tracker declaring `ProviderMetadata.supportsIssueStates` can express anything but `'open'`; asking\n\t\t * another for `'closed'`/`'all'` refuses the read rather than serving its open issues.\n\t\t */\n\t\tstate?: IssueStateFilter;\n\t\tforceSync?: boolean;\n\t\tpage?: number;\n\t\tcursor?: string;\n\t\titemsPerPage?: number;\n\t\tconnectionId?: string;\n\t\t/**\n\t\t * Explicit self-managed tracker host. Used to select the instance when the requested connection has no\n\t\t * configured domain; it must come from the trusted authentication configuration, never from repository\n\t\t * or remote data. Ignored for the cloud trackers.\n\t\t */\n\t\tdomain?: string;\n\t},\n): Promise<ProviderPagedResult<IssueShape>> {\n\t// Pagination is opt-in: only window the projects when the caller actually asked to page. A caller that\n\t// passes none of page/cursor/itemsPerPage keeps the \"aggregate every matched project\" contract, so an\n\t// existing consumer that doesn't inspect `hasMore` never silently loses projects past a default window.\n\tconst compositeCursor = parseIssueTrackerPageCursor(options.cursor);\n\tconst paginated =\n\t\tcompositeCursor?.unpaged !== true &&\n\t\t(options.page != null || options.cursor != null || options.itemsPerPage != null);\n\t// A composite cursor can carry retries from earlier project windows alongside the next untouched window.\n\t// `currentPage` remains the caller-facing position; `nextPage` is the window this round should advance.\n\tconst page = Math.max(\n\t\t1,\n\t\tMath.trunc(compositeCursor?.currentPage ?? parsePageCursor(options.cursor) ?? options.page ?? 1),\n\t);\n\tconst projectsPerPage = Math.max(1, Math.trunc(options.itemsPerPage ?? 20));\n\n\tconst items: IssueShape[] = [];\n\tconst warnings: ProviderWarning[] = [];\n\tconst emptyPage = (\n\t\tfetchFailed?: boolean,\n\t\ttruncated?: boolean,\n\t\tretry?: {\n\t\t\tnextPage?: number;\n\t\t\tpages?: readonly number[];\n\t\t\tprojects?: readonly string[];\n\t\t\tcompletedProjects?: readonly string[];\n\t\t},\n\t): ProviderPagedResult<IssueShape> => {\n\t\tconst cursor =\n\t\t\tretry != null\n\t\t\t\t? toIssueTrackerPageCursor({\n\t\t\t\t\t\tcurrentPage: page + 1,\n\t\t\t\t\t\tunpaged: !paginated,\n\t\t\t\t\t\tnextPage: retry.nextPage,\n\t\t\t\t\t\tretryPages: retry.pages ?? [],\n\t\t\t\t\t\tretryProjects: retry.projects ?? [],\n\t\t\t\t\t\tcompletedProjects: retry.completedProjects ?? [],\n\t\t\t\t\t\tstate: options.state,\n\t\t\t\t\t})\n\t\t\t\t: undefined;\n\t\treturn {\n\t\t\titems: items,\n\t\t\twarnings: warnings,\n\t\t\tpage: { currentPage: page, itemsPerPage: items.length, truncated: truncated || undefined },\n\t\t\t// Retry-only state is deliberately manual: advertising it as forward progress would make a\n\t\t\t// persistent provider failure spin forever in a normal `while (hasMore)` consumer. An untouched\n\t\t\t// project window remains normal forward progress even when this window failed completely.\n\t\t\thasMore: retry?.nextPage != null && cursor != null,\n\t\t\tcursor: cursor,\n\t\t\tfetchFailed: fetchFailed || undefined,\n\t\t};\n\t};\n\tconst normalWindowPage = paginated\n\t\t? (compositeCursor?.nextPage ?? (compositeCursor == null ? page : undefined))\n\t\t: undefined;\n\tconst priorRetryPages = compositeCursor?.retryPages ?? [];\n\tconst priorRetryProjects = compositeCursor?.retryProjects ?? [];\n\tconst priorCompletedProjects = compositeCursor?.completedProjects ?? [];\n\tconst trackCompletedProjects =\n\t\tpriorCompletedProjects.length > 0 || priorRetryPages.length > 0 || compositeCursor?.completedProjects != null;\n\tconst retryPagesForDiscoveryFailure = (): number[] => [\n\t\t...priorRetryPages,\n\t\t...(normalWindowPage != null ? [normalWindowPage] : compositeCursor == null ? [1] : []),\n\t];\n\tconst discoveryFailureRetry = (): {\n\t\tpages: readonly number[];\n\t\tprojects: readonly string[];\n\t\tcompletedProjects: readonly string[];\n\t} => ({\n\t\tpages: retryPagesForDiscoveryFailure(),\n\t\tprojects: priorRetryProjects,\n\t\tcompletedProjects: priorCompletedProjects,\n\t});\n\n\tif (!isIssuesHostIntegrationId(options.providerId)) {\n\t\twarnings.push(\n\t\t\tissueTrackerOnlySurfaceWarning(options.providerId, options.connectionId, 'Issue-tracker project reads'),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\t// A supplied connectionId or domain that no longer resolves is a broken target, not an empty account.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\twarnings.push(...early.warnings);\n\t\treturn emptyPage(early.fetchFailed);\n\t}\n\tif (!isIssuesIntegration(integration)) {\n\t\twarnings.push(\n\t\t\tissueTrackerOnlySurfaceWarning(options.providerId, options.connectionId, 'Issue-tracker project reads'),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\n\t// Before any upstream request: the discovery fan-outs below (resources, projects, per-resource accounts) are\n\t// three round trips, and an order this tracker can't express refuses the read whatever they return. Placed after\n\t// `domain` only because every warning carries it.\n\tconst resolvedSort = resolveIssueSort(providersMetadata[options.providerId]?.supportedIssueSorts, options.sort);\n\tif (resolvedSort.rejection != null) {\n\t\twarnings.push(\n\t\t\tunsupportedIssueSortWarning(options.providerId, domain, options.connectionId, resolvedSort.rejection),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\tconst sort = resolvedSort.sort;\n\n\t// Refused before any request for the same reason as the sort: a state the tracker can't express would otherwise\n\t// be dropped by its project reads, and their open issues published as the requested state. A value outside the\n\t// vocabulary is refused too, since `toProviderIssueStates` would read it as omitted, which means open.\n\tif (\n\t\toptions.state != null &&\n\t\t(!issueStateFilters.includes(options.state) ||\n\t\t\t(options.state !== 'open' && !providersMetadata[options.providerId]?.supportsIssueStates))\n\t) {\n\t\twarnings.push(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\tissueStateFilters.includes(options.state)\n\t\t\t\t\t? `Issue state '${options.state}' is not supported by '${options.providerId}'.`\n\t\t\t\t\t: `Unknown issue state; expected one of ${issueStateFilters.join(', ')}.`,\n\t\t\t),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\t// A cursor resumed under another state than it was minted for skips what it already covered, whose other-state\n\t// issues were never read. A narrowed read only mints composite cursors, which record their state, so any other\n\t// cursor came from an open read.\n\tconst cursorState =\n\t\tcompositeCursor != null ? (compositeCursor.state ?? 'open') : options.cursor != null ? 'open' : undefined;\n\tif (cursorState != null && cursorState !== (options.state ?? 'open')) {\n\t\twarnings.push(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t'The cursor belongs to a read of another issue state; start again without it.',\n\t\t\t),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\tawait ctx.forceRefreshIfRequested(integration, options.forceSync, options.connectionId);\n\n\tconst { value: resources, warning: resourcesWarning } = await runCaptured(\n\t\toptions.providerId,\n\t\tdomain,\n\t\toptions.connectionId,\n\t\t() => integration.getResourcesForUserResult(options.connectionId),\n\t);\n\tif (resourcesWarning != null) {\n\t\twarnings.push(resourcesWarning);\n\t}\n\tif (resources == null || resources.length === 0) {\n\t\tconst fetchFailed = resourcesWarning != null && resources == null;\n\t\treturn emptyPage(fetchFailed, false, fetchFailed ? discoveryFailureRetry() : undefined);\n\t}\n\n\tconst scopedResources =\n\t\toptions.org != null ? resources.filter(r => resourceMatchesOrg(r, options.org!)) : resources;\n\tif (scopedResources.length === 0) {\n\t\treturn emptyPage();\n\t}\n\n\tconst { value: projectsResult, warning: projectsWarning } = await runCaptured(\n\t\toptions.providerId,\n\t\tdomain,\n\t\toptions.connectionId,\n\t\t() => integration.getProjectsForResourcesWithMetadataResult(scopedResources, options.connectionId),\n\t);\n\tif (projectsWarning != null) {\n\t\twarnings.push(projectsWarning);\n\t}\n\t// Partial project discovery: continue with the resources that succeeded, but surface per-resource\n\t// failures as warnings and remember to mark the page fetchFailed so the caller knows some issues may be\n\t// missing. `projectDiscoveryFailed`/`projectDiscoveryTruncated` are OR-ed into the page's\n\t// fetchFailed/truncated at every return below (a truncated-but-not-failed discovery, e.g. a paging\n\t// backstop, still means the project set is incomplete).\n\tconst projectDiscoveryAssessment = mergeAssessmentInto(\n\t\twarnings,\n\t\toptions.providerId,\n\t\tdomain,\n\t\toptions.connectionId,\n\t\tprojectsResult?.metadata,\n\t);\n\tconst projectDiscoveryFailed = projectDiscoveryAssessment.fetchFailed;\n\tconst projectDiscoveryTruncated = projectDiscoveryAssessment.truncated;\n\tconst projects = projectsResult?.values;\n\tif (projects == null || projects.length === 0) {\n\t\tconst fetchFailed = (projectsWarning != null && projectsResult == null) || projectDiscoveryFailed;\n\t\treturn emptyPage(\n\t\t\tfetchFailed,\n\t\t\tprojectDiscoveryTruncated,\n\t\t\tfetchFailed || projectDiscoveryTruncated ? discoveryFailureRetry() : undefined,\n\t\t);\n\t}\n\n\tconst matchedProjects =\n\t\toptions.project != null ? projects.filter(p => resourceMatchesOrg(p, options.project!)) : projects;\n\n\t// Validate the requested filters against what this provider supports (e.g. Linear/Trello support only\n\t// Assignee). An unsupported filter must not silently degrade — Linear/Trello ignore the requested type\n\t// and apply Assignee regardless — so warn + fetchFailed instead of returning a differently-scoped set.\n\tif (options.filters?.length) {\n\t\tconst supported = providersMetadata[options.providerId]?.supportedIssueFilters;\n\t\tconst allSupported = supported != null && options.filters.every(f => supported.includes(f));\n\t\tif (!allSupported) {\n\t\t\twarnings.push(\n\t\t\t\totherWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t`One or more requested issue filters are not supported by '${options.providerId}'.`,\n\t\t\t\t),\n\t\t\t);\n\t\t\treturn emptyPage(true);\n\t\t}\n\t}\n\n\t// `includeAllAssignees` drops the user scope, but a user-relative filter (Author/Mention) is meaningless\n\t// without a user. Passing both to the provider degrades silently: Jira, seeing no user, falls through to\n\t// an unscoped project fetch and returns EVERY issue instead of the requested author's/mentions. Reject\n\t// the incompatible combination up front rather than publishing a differently-scoped set as the result.\n\tif (options.includeAllAssignees === true && options.filters?.some(f => f !== IssueFilter.Assignee)) {\n\t\twarnings.push(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`\\`includeAllAssignees\\` cannot be combined with an author/mention filter for '${options.providerId}' (those filters require a user scope).`,\n\t\t\t),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\t// Scope to the current user's assigned issues unless the caller broadens to all assignees. Resolve the\n\t// handle from each resource's own account (multi-account safe), capturing any error so its kind\n\t// (e.g. auth) is preserved rather than collapsed to a generic warning.\n\tlet usersByResourceId: Map<string, string> | undefined;\n\t// The same accounts keyed by the same resource, holding the provider's stable id rather than the handle. A\n\t// separate map rather than a widened value because the two are resolved and consumed independently: a\n\t// provider may carry an account with no id, and `scopedProjectsWithUsers` gates on the HANDLE alone, so\n\t// folding them together would drop a resource whose id happened to be missing.\n\tlet userIdsByResourceId: Map<string, string> | undefined;\n\tlet accountLookupFailed = false;\n\tif (options.includeAllAssignees !== true) {\n\t\tusersByResourceId = new Map<string, string>();\n\t\tuserIdsByResourceId = new Map<string, string>();\n\t\tconst accounts = await mapBounded(scopedResources, providerFanOutConcurrency, async resource => ({\n\t\t\tresource: resource,\n\t\t\t...(await runCaptured(options.providerId, domain, options.connectionId, () =>\n\t\t\t\tintegration.getAccountForResourceResult(resource, options.connectionId),\n\t\t\t)),\n\t\t}));\n\n\t\tfor (const { resource, value: account, warning: accountWarning } of accounts) {\n\t\t\tconst user = account?.username ?? account?.name ?? undefined;\n\t\t\tif (user != null) {\n\t\t\t\tconst resourceKey = resourceIdForProject(resource) ?? resource.key;\n\t\t\t\tusersByResourceId.set(resourceKey, user);\n\t\t\t\t// Empty-checked, not just null-checked: an account shape declares `id` as a plain string, and a\n\t\t\t\t// provider with nothing to put there sets `''` rather than omitting it — which would otherwise\n\t\t\t\t// reach a user-field query as an empty identity and match nothing.\n\t\t\t\tif (account?.id != null && account.id !== '') {\n\t\t\t\t\tuserIdsByResourceId.set(resourceKey, account.id);\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Deduped: a captured `accountWarning` carries only the provider error, with no resource in its\n\t\t\t// message, so one expired token across N resources would otherwise repeat verbatim N times. The\n\t\t\t// synthesized fallback names the resource, so those stay distinct on their own.\n\t\t\tappendDedupedWarning(\n\t\t\t\twarnings,\n\t\t\t\taccountWarning ??\n\t\t\t\t\totherWarning(\n\t\t\t\t\t\toptions.providerId,\n\t\t\t\t\t\tdomain,\n\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t\t`Could not resolve the current user for '${resourceLabel(resource)}'; skipping that resource to avoid returning issues assigned to others.`,\n\t\t\t\t\t),\n\t\t\t);\n\t\t\taccountLookupFailed = true;\n\t\t}\n\t}\n\n\tconst fallbackUserForUnscopedProject =\n\t\tscopedResources.length === 1 && usersByResourceId?.size === 1\n\t\t\t? usersByResourceId.values().next().value\n\t\t\t: undefined;\n\tconst userForProject = (project: ResourceDescriptor): string | undefined => {\n\t\tconst resourceId = resourceIdForProject(project);\n\t\tif (resourceId != null) {\n\t\t\tconst user = usersByResourceId?.get(resourceId);\n\t\t\tif (user != null) {\n\t\t\t\treturn user;\n\t\t\t}\n\t\t}\n\n\t\t// Some providers/tests return project descriptors without their parent resource id. When we have only\n\t\t// one scoped resource, re-use that sole resolved user rather than silently dropping every project.\n\t\treturn fallbackUserForUnscopedProject;\n\t};\n\t// Deliberately NOT falling back the way `userForProject` does. That fallback exists so a project with no\n\t// parent resource id still gets scoped rather than dropped, and the handle is what the drop is judged on;\n\t// the id is optional everywhere it is read, so a miss degrades to the handle instead of widening the read.\n\tconst userIdForProject = (project: ResourceDescriptor): string | undefined => {\n\t\tconst resourceId = resourceIdForProject(project);\n\t\treturn resourceId != null ? userIdsByResourceId?.get(resourceId) : undefined;\n\t};\n\n\tconst retryProjectKeys = new Set<string>();\n\tconst completedProjectKeys = new Set(priorCompletedProjects);\n\tfor (const retryProject of priorRetryProjects) {\n\t\tcompletedProjectKeys.delete(retryProject);\n\t}\n\tconst scopedProjectsWithUsers =\n\t\tusersByResourceId != null\n\t\t\t? matchedProjects.filter(project => {\n\t\t\t\t\tif (userForProject(project) != null) return true;\n\n\t\t\t\t\tconst key = projectKey(project);\n\t\t\t\t\tretryProjectKeys.add(key);\n\t\t\t\t\tcompletedProjectKeys.delete(key);\n\t\t\t\t\treturn false;\n\t\t\t\t})\n\t\t\t: matchedProjects;\n\n\t// Select the untouched window plus any earlier windows/projects that explicitly need retrying. Keys\n\t// include the parent resource so two trackers can reuse the same project id without collapsing.\n\tconst projectsByRetryKey = new Map(scopedProjectsWithUsers.map(project => [projectKey(project), project]));\n\tconst scopedProjectsByKey = new Map<string, ResourceDescriptor>();\n\tconst addScopedProject = (project: ResourceDescriptor): void => {\n\t\tconst key = projectKey(project);\n\t\tif (completedProjectKeys.has(key)) return;\n\n\t\tscopedProjectsByKey.set(key, project);\n\t};\n\tfor (const retryProject of priorRetryProjects) {\n\t\tconst project = projectsByRetryKey.get(retryProject);\n\t\tif (project != null) {\n\t\t\taddScopedProject(project);\n\t\t} else if (projectDiscoveryFailed || projectDiscoveryTruncated) {\n\t\t\tretryProjectKeys.add(retryProject);\n\t\t}\n\t}\n\tconst windowPages = new Set<number>(priorRetryPages);\n\tif (normalWindowPage != null) {\n\t\twindowPages.add(normalWindowPage);\n\t}\n\tif (!paginated && (projectDiscoveryFailed || projectDiscoveryTruncated)) {\n\t\twindowPages.add(1);\n\t}\n\tif (paginated) {\n\t\tfor (const windowPage of windowPages) {\n\t\t\tconst windowStart = (windowPage - 1) * projectsPerPage;\n\t\t\tfor (const project of scopedProjectsWithUsers.slice(windowStart, windowStart + projectsPerPage)) {\n\t\t\t\taddScopedProject(project);\n\t\t\t}\n\t\t}\n\t} else if (compositeCursor == null || priorRetryPages.length > 0) {\n\t\t// An unpaged retry-only cursor should re-read only the explicitly failed projects already added\n\t\t// above. Discovery retries still rescan the aggregate set so newly recovered projects are included.\n\t\tfor (const project of scopedProjectsWithUsers) {\n\t\t\taddScopedProject(project);\n\t\t}\n\t}\n\tconst scopedProjects = [...scopedProjectsByKey.values()];\n\tconst furthestWindowPage = windowPages.size > 0 ? Math.max(...windowPages) : normalWindowPage;\n\tconst normalWindowEnd =\n\t\tfurthestWindowPage != null ? furthestWindowPage * projectsPerPage : scopedProjectsWithUsers.length;\n\tconst nextPage =\n\t\tpaginated && furthestWindowPage != null && scopedProjectsWithUsers.length > normalWindowEnd\n\t\t\t? furthestWindowPage + 1\n\t\t\t: undefined;\n\tconst retryWindowPages = (): number[] => {\n\t\tif (!projectDiscoveryFailed && !projectDiscoveryTruncated && retryProjectKeys.size === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\tconst pages = new Set(windowPages);\n\t\tif (pages.size === 0) {\n\t\t\t// A terminal retry has no untouched next window to identify its origin. Keep a manual window\n\t\t\t// marker so a later partial discovery can reconcile against the projects already emitted.\n\t\t\tpages.add(paginated ? Math.max(1, page - 1) : 1);\n\t\t}\n\t\treturn [...pages];\n\t};\n\tif (scopedProjects.length === 0) {\n\t\t// The discovered projects didn't intersect the requested filter/window, or every matching resource\n\t\t// failed user resolution. If discovery or account lookup was partial, the empty result is not a\n\t\t// proven-empty account — carry `fetchFailed` so the caller knows issues may be missing.\n\t\treturn emptyPage(projectDiscoveryFailed || accountLookupFailed, projectDiscoveryTruncated, {\n\t\t\tnextPage: nextPage,\n\t\t\tpages: retryWindowPages(),\n\t\t\tprojects: [...retryProjectKeys],\n\t\t\tcompletedProjects: [...completedProjectKeys],\n\t\t});\n\t}\n\n\t// A read spanning several projects is a merge, so it can only honor a key a normalized issue carries. Bound here\n\t// rather than next to the key's validation above, because the project count is only known now — while still\n\t// before any project read, so a refusal costs no request. Refused rather than served as concatenated\n\t// per-project runs, which would look ordered without being so.\n\t//\n\t// Counted over every project the READ covers, not the `scopedProjects` window this page happens to hold: the\n\t// window is `itemsPerPage` projects wide, so counting it would refuse a 20-project page and then serve the\n\t// final 1-project page of the same paginated call — the same key answered two ways, with the earlier pages'\n\t// issues missing from a read that reported success.\n\t//\n\t// Projects are not the only thing a tracker read merges: a user-scoped read asking for several relationships\n\t// issues one provider query PER relationship and unions them within each project (Jira runs one JQL drain per\n\t// filter and folds them into one map, in filter order), so a single-project page of two filters is just as\n\t// concatenated as a two-project one. Counting only projects published those runs under the requested key.\n\t// Not gated on the provider: the two trackers that don't fan out (Linear, Trello) declare `Assignee` as their\n\t// only supported filter, so a multi-filter read is already refused above and can never reach this.\n\tconst mergesFilters = options.includeAllAssignees !== true && (options.filters?.length ?? 0) > 1;\n\tconst ordering = toIssueOrdering(sort, scopedProjectsWithUsers.length > 1 || mergesFilters);\n\tif (ordering.unmergeable != null) {\n\t\twarnings.push(\n\t\t\tunmergeableIssueSortWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\tordering.unmergeable,\n\t\t\t\tscopedProjectsWithUsers.length > 1 ? 'projects' : 'filters',\n\t\t\t),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\tconst perProject = await mapBounded(scopedProjects, providerFanOutConcurrency, async project => ({\n\t\tproject: project,\n\t\t...(await runCaptured(options.providerId, domain, options.connectionId, () =>\n\t\t\tintegration.getIssuesForProjectWithTruncationResult(\n\t\t\t\tproject,\n\t\t\t\t{\n\t\t\t\t\tuser: userForProject(project),\n\t\t\t\t\tuserId: userIdForProject(project),\n\t\t\t\t\tfilters: options.filters,\n\t\t\t\t\tsort: sort,\n\t\t\t\t\tstate: options.state,\n\t\t\t\t},\n\t\t\t\toptions.connectionId,\n\t\t\t),\n\t\t)),\n\t}));\n\n\t// Partial project discovery means some projects' issues are missing from this page; propagate it so the\n\t// page reports fetchFailed even when every discovered project's own read succeeded.\n\tlet fetchFailed = projectDiscoveryFailed || accountLookupFailed;\n\tlet truncationRecovery: 'narrow-scope' | 'none' | undefined = projectDiscoveryTruncated ? 'none' : undefined;\n\tlet drainMetadata: CollectionMetadata | undefined;\n\tfor (const { project, value: result, warning } of perProject) {\n\t\tconst key = projectKey(project);\n\t\tconst retryProject = result == null;\n\t\tif (warning != null) {\n\t\t\t// Deduped: the per-project warning is built from the provider error alone and names no project, so a\n\t\t\t// single failing token repeats verbatim once per project in the window.\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t\tfetchFailed = true;\n\t\t\ttruncationRecovery = 'none';\n\t\t}\n\t\t// A thrown/unsupported read (e.g. Linear not-implemented) surfaces as a warning with no value;\n\t\t// mark the aggregate as fetchFailed so an empty result isn't mistaken for \"no issues\".\n\t\tif (result == null) {\n\t\t\tfetchFailed = true;\n\t\t}\n\t\tif (result != null) {\n\t\t\titems.push(...result.values);\n\t\t\tif (result.truncated) {\n\t\t\t\tconst recovery = result.recovery;\n\t\t\t\ttruncationRecovery = truncationRecovery == null || truncationRecovery === recovery ? recovery : 'none';\n\t\t\t}\n\t\t\tif (result.metadata != null) {\n\t\t\t\tdrainMetadata = mergeCollectionMetadata(drainMetadata, result.metadata);\n\t\t\t}\n\t\t}\n\t\tif (retryProject) {\n\t\t\tretryProjectKeys.add(key);\n\t\t\tcompletedProjectKeys.delete(key);\n\t\t} else {\n\t\t\t// A usable partial/truncated project is emitted once and remains structurally incomplete.\n\t\t\t// Re-running the same project cursor would normally return the same prefix and duplicate it\n\t\t\t// across facade pages; only a project that returned no value is safe to retry automatically.\n\t\t\tcompletedProjectKeys.add(key);\n\t\t}\n\t}\n\n\t// Each provider query arrived ordered by the tracker; the union of them — across projects, and across the\n\t// per-relationship queries a multi-filter read fans out into — is not, and the union is what this read\n\t// publishes. A no-op when nothing merged, since the tracker already ordered that single run.\n\tconst orderedItems = ordering.order(items);\n\n\tconst projectReadReportedTruncation = truncationRecovery != null;\n\tconst drainAssessment = assessCollectionMetadata(options.providerId, domain, options.connectionId, drainMetadata);\n\tfor (const warning of drainAssessment.warnings) {\n\t\t// A project result already supplies the recovery semantics. Replace metadata's unstructured fallback with\n\t\t// the structured warning below, while retaining specific failures and omissions.\n\t\tif (projectReadReportedTruncation && !drainAssessment.fetchFailed && warning.omission == null) continue;\n\n\t\tappendDedupedWarning(warnings, warning);\n\t}\n\tfetchFailed = fetchFailed || drainAssessment.fetchFailed;\n\tif (drainAssessment.truncated) {\n\t\ttruncationRecovery = 'none';\n\t}\n\n\tconst projectTruncated = truncationRecovery != null;\n\tconst scopeTooLarge = !fetchFailed && truncationRecovery === 'narrow-scope';\n\tif (scopeTooLarge || (!fetchFailed && projectTruncated && !warnings.some(warning => warning.omission != null))) {\n\t\tappendDedupedWarning(\n\t\t\twarnings,\n\t\t\tincompleteReadWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\tscopeTooLarge\n\t\t\t\t\t? 'Some projects hold more issues than one read can return; narrow the scope to read the rest.'\n\t\t\t\t\t: 'Some issues were omitted; the provider returned an incomplete result.',\n\t\t\t\tscopeTooLarge ? 'scope-too-large' : 'exhausted',\n\t\t\t),\n\t\t);\n\t}\n\t// A metadata omission from an earlier project asserts the read succeeded; a later one may since have failed.\n\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\n\tconst retryPages = retryWindowPages();\n\tconst cursor = toIssueTrackerPageCursor({\n\t\tcurrentPage: page + 1,\n\t\tunpaged: !paginated,\n\t\tnextPage: nextPage,\n\t\tretryPages: retryPages,\n\t\tretryProjects: [...retryProjectKeys],\n\t\tcompletedProjects:\n\t\t\t(trackCompletedProjects ||\n\t\t\t\tprojectDiscoveryFailed ||\n\t\t\t\tprojectDiscoveryTruncated ||\n\t\t\t\tretryProjectKeys.size > 0) &&\n\t\t\t(retryPages.length > 0 || nextPage != null)\n\t\t\t\t? [...completedProjectKeys]\n\t\t\t\t: [],\n\t\tstate: options.state,\n\t});\n\treturn {\n\t\titems: orderedItems,\n\t\twarnings: warnings,\n\t\tpage: { currentPage: page, itemsPerPage: orderedItems.length, truncated: projectTruncated || undefined },\n\t\t// Failed-project retries alone are manual. Only an untouched project window is automatic progress.\n\t\thasMore: nextPage != null && cursor != null,\n\t\tcursor: cursor,\n\t\tfetchFailed: fetchFailed || undefined,\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -68,6 +68,17 @@ export declare function truncationWarning(id: IntegrationIds, domain: string | u
|
|
|
68
68
|
* fetched" — are ones this warning must never make by accident.
|
|
69
69
|
*/
|
|
70
70
|
cause: IncompleteReadCause): ProviderWarning;
|
|
71
|
+
/**
|
|
72
|
+
* The part of a drain that paging could not reach, when that drain ALSO stopped at its page budget with a usable
|
|
73
|
+
* cursor — one facet of a composite read reaching the provider's ceiling while a sibling can still page. Reported
|
|
74
|
+
* beside the `page-budget` {@link truncationWarning}, never in place of it: raising the budget returns more of the
|
|
75
|
+
* rest, and nothing returns this part, so each warning speaks only for its own.
|
|
76
|
+
*
|
|
77
|
+
* The message names no mechanism. The fact behind it is `paging.truncated`, which a provider cap sets but so does
|
|
78
|
+
* `hasMore` with no usable cursor (GitHub's `structuralIncompleteness`) and any per-repository truncation on the
|
|
79
|
+
* repo-scoped path, so "capped by the provider" would name a cause we cannot confirm.
|
|
80
|
+
*/
|
|
81
|
+
export declare function cappedBesideBudgetWarning(id: IntegrationIds, domain: string | undefined, connectionId: string | undefined, readKind: TruncatedReadKind): ProviderWarning;
|
|
71
82
|
/** Warning for an account-wide issue read whose requested filters the provider can't express server-side. */
|
|
72
83
|
export declare function unsupportedAccountWideIssueFiltersWarning(id: IntegrationIds, domain: string | undefined, connectionId: string | undefined, filters: IssueFilter[]): ProviderWarning;
|
|
73
84
|
/** Warning for an account-wide PR read whose requested relationship union cannot be expressed exactly. */
|
|
@@ -89,6 +89,19 @@ function incompleteReadWarning(id, domain, connectionId, message, cause) {
|
|
|
89
89
|
function truncationWarning(id, domain, connectionId, readKind, cause) {
|
|
90
90
|
return incompleteReadWarning(id, domain, connectionId, truncationMessage(id, readKind, cause), cause);
|
|
91
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* The part of a drain that paging could not reach, when that drain ALSO stopped at its page budget with a usable
|
|
94
|
+
* cursor — one facet of a composite read reaching the provider's ceiling while a sibling can still page. Reported
|
|
95
|
+
* beside the `page-budget` {@link truncationWarning}, never in place of it: raising the budget returns more of the
|
|
96
|
+
* rest, and nothing returns this part, so each warning speaks only for its own.
|
|
97
|
+
*
|
|
98
|
+
* The message names no mechanism. The fact behind it is `paging.truncated`, which a provider cap sets but so does
|
|
99
|
+
* `hasMore` with no usable cursor (GitHub's `structuralIncompleteness`) and any per-repository truncation on the
|
|
100
|
+
* repo-scoped path, so "capped by the provider" would name a cause we cannot confirm.
|
|
101
|
+
*/
|
|
102
|
+
function cappedBesideBudgetWarning(id, domain, connectionId, readKind) {
|
|
103
|
+
return incompleteReadWarning(id, domain, connectionId, `${readKind} read for '${id}' could not reach part of its results by paging; raising the page budget will not return those.`, "exhausted");
|
|
104
|
+
}
|
|
92
105
|
function truncationMessage(id, readKind, cause) {
|
|
93
106
|
switch (cause) {
|
|
94
107
|
case "interrupted": return `${readKind} read for '${id}' did not complete; some results are missing and the read reported a failure.`;
|
|
@@ -303,6 +316,6 @@ function issuesUnsupportedWarning(id, domain, connectionId) {
|
|
|
303
316
|
return otherWarning(id, domain, connectionId, `Issues are not supported by '${id}'; use a dedicated issue integration (e.g. Jira) instead.`);
|
|
304
317
|
}
|
|
305
318
|
//#endregion
|
|
306
|
-
export { gitHostOnlySurfaceWarning, incompleteReadWarning, issueSearchCapResultWarning, issueTrackerOnlySurfaceWarning, issuesUnsupportedWarning, noConnectionWarning, otherWarning, pullRequestSearchCapResultWarning, truncationWarning, unmergeableIssueSortWarning, unsupportedAccountWideIssueFiltersWarning, unsupportedAccountWidePullRequestFiltersWarning, unsupportedFiltersWarning, unsupportedIssueSearchCriteriaWarning, unsupportedIssueSortWarning, unsupportedPullRequestSearchCriteriaWarning, unusableSearchScopeMessage };
|
|
319
|
+
export { cappedBesideBudgetWarning, gitHostOnlySurfaceWarning, incompleteReadWarning, issueSearchCapResultWarning, issueTrackerOnlySurfaceWarning, issuesUnsupportedWarning, noConnectionWarning, otherWarning, pullRequestSearchCapResultWarning, truncationWarning, unmergeableIssueSortWarning, unsupportedAccountWideIssueFiltersWarning, unsupportedAccountWidePullRequestFiltersWarning, unsupportedFiltersWarning, unsupportedIssueSearchCriteriaWarning, unsupportedIssueSortWarning, unsupportedPullRequestSearchCriteriaWarning, unusableSearchScopeMessage };
|
|
307
320
|
|
|
308
321
|
//# sourceMappingURL=warnings.js.map
|