@gitkraken/core-gitlens 0.9.3 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -1
- package/dist/git/models/issue.d.ts +25 -2
- package/dist/git/models/issue.js +3 -1
- package/dist/git/models/issue.js.map +1 -1
- package/dist/git/models/pullRequest.d.ts +55 -5
- package/dist/git/models/pullRequest.js +5 -1
- package/dist/git/models/pullRequest.js.map +1 -1
- package/dist/git/utils/issue.utils.d.ts +4 -0
- package/dist/git/utils/issue.utils.js +4 -0
- package/dist/git/utils/issue.utils.js.map +1 -1
- package/dist/git/utils/pullRequest.utils.d.ts +4 -0
- package/dist/git/utils/pullRequest.utils.js +8 -0
- package/dist/git/utils/pullRequest.utils.js.map +1 -1
- package/dist/plus/git-github/api/github.d.ts +64 -4
- package/dist/plus/git-github/api/github.js +232 -15
- package/dist/plus/git-github/api/github.js.map +1 -1
- package/dist/plus/git-github/models.d.ts +48 -6
- package/dist/plus/git-github/models.js +11 -10
- package/dist/plus/git-github/models.js.map +1 -1
- package/dist/plus/integrations/fieldPresence.d.ts +75 -0
- package/dist/plus/integrations/fieldPresence.js +381 -0
- package/dist/plus/integrations/fieldPresence.js.map +1 -0
- package/dist/plus/integrations/index.d.ts +6 -4
- package/dist/plus/integrations/index.js +2 -1
- package/dist/plus/integrations/index.js.map +1 -1
- package/dist/plus/integrations/integrationService.d.ts +13 -4
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/manager.d.ts +62 -7
- package/dist/plus/integrations/models/gitHostIntegration.d.ts +103 -9
- package/dist/plus/integrations/models/gitHostIntegration.js +116 -5
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
- package/dist/plus/integrations/models/integration.d.ts +91 -4
- package/dist/plus/integrations/models/integration.js +56 -7
- package/dist/plus/integrations/models/integration.js.map +1 -1
- package/dist/plus/integrations/models/issueReads.d.ts +14 -0
- package/dist/plus/integrations/models/issuesIntegration.d.ts +44 -3
- package/dist/plus/integrations/models/issuesIntegration.js +51 -1
- package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
- package/dist/plus/integrations/providers/azure/azure.js +3 -3
- package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
- package/dist/plus/integrations/providers/azure/models.d.ts +36 -4
- package/dist/plus/integrations/providers/azure/models.js +118 -8
- package/dist/plus/integrations/providers/azure/models.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOps.d.ts +102 -5
- package/dist/plus/integrations/providers/azureDevOps.js +264 -45
- package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOpsServer.d.ts +30 -6
- package/dist/plus/integrations/providers/azureDevOpsServer.js +82 -21
- package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket/bitbucket.d.ts +21 -1
- package/dist/plus/integrations/providers/bitbucket/bitbucket.js +74 -10
- package/dist/plus/integrations/providers/bitbucket/bitbucket.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket/models.d.ts +45 -8
- package/dist/plus/integrations/providers/bitbucket/models.js +32 -9
- package/dist/plus/integrations/providers/bitbucket/models.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket-server/models.d.ts +2 -6
- package/dist/plus/integrations/providers/bitbucket-server/models.js +1 -3
- package/dist/plus/integrations/providers/bitbucket-server/models.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.d.ts +3 -1
- package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.js +4 -1
- package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket-server.d.ts +2 -2
- package/dist/plus/integrations/providers/bitbucket-server.js +7 -5
- package/dist/plus/integrations/providers/bitbucket-server.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket.d.ts +22 -2
- package/dist/plus/integrations/providers/bitbucket.js +29 -6
- package/dist/plus/integrations/providers/bitbucket.js.map +1 -1
- package/dist/plus/integrations/providers/github.d.ts +40 -3
- package/dist/plus/integrations/providers/github.js +133 -11
- package/dist/plus/integrations/providers/github.js.map +1 -1
- package/dist/plus/integrations/providers/gitlab/gitlab.d.ts +34 -1
- package/dist/plus/integrations/providers/gitlab/gitlab.js +118 -4
- package/dist/plus/integrations/providers/gitlab/gitlab.js.map +1 -1
- package/dist/plus/integrations/providers/gitlab/models.d.ts +70 -5
- package/dist/plus/integrations/providers/gitlab/models.js +106 -13
- package/dist/plus/integrations/providers/gitlab/models.js.map +1 -1
- package/dist/plus/integrations/providers/gitlab.d.ts +31 -2
- package/dist/plus/integrations/providers/gitlab.js +48 -8
- package/dist/plus/integrations/providers/gitlab.js.map +1 -1
- package/dist/plus/integrations/providers/jira-server.d.ts +3 -2
- package/dist/plus/integrations/providers/jira-server.js +18 -9
- package/dist/plus/integrations/providers/jira-server.js.map +1 -1
- package/dist/plus/integrations/providers/jira.d.ts +26 -2
- package/dist/plus/integrations/providers/jira.js +120 -9
- package/dist/plus/integrations/providers/jira.js.map +1 -1
- package/dist/plus/integrations/providers/jiraIssueByKey.d.ts +79 -0
- package/dist/plus/integrations/providers/jiraIssueByKey.js +58 -7
- package/dist/plus/integrations/providers/jiraIssueByKey.js.map +1 -1
- package/dist/plus/integrations/providers/linear.d.ts +19 -1
- package/dist/plus/integrations/providers/linear.js +105 -7
- package/dist/plus/integrations/providers/linear.js.map +1 -1
- package/dist/plus/integrations/providers/linearIssuesEtag.d.ts +42 -0
- package/dist/plus/integrations/providers/linearIssuesEtag.js +94 -0
- package/dist/plus/integrations/providers/linearIssuesEtag.js.map +1 -0
- package/dist/plus/integrations/providers/models.d.ts +70 -8
- package/dist/plus/integrations/providers/models.js +59 -21
- package/dist/plus/integrations/providers/models.js.map +1 -1
- package/dist/plus/integrations/providers/providerErrors.js +8 -1
- package/dist/plus/integrations/providers/providerErrors.js.map +1 -1
- package/dist/plus/integrations/providers/providersApi.d.ts +70 -4
- package/dist/plus/integrations/providers/providersApi.js +148 -4
- package/dist/plus/integrations/providers/providersApi.js.map +1 -1
- package/dist/plus/integrations/providers/pullRequestReviews.d.ts +13 -0
- package/dist/plus/integrations/providers/pullRequestReviews.js +30 -6
- package/dist/plus/integrations/providers/pullRequestReviews.js.map +1 -1
- package/dist/plus/integrations/providers/trello.js +11 -7
- package/dist/plus/integrations/providers/trello.js.map +1 -1
- package/dist/plus/integrations/providers/utils/providerPaging.d.ts +11 -0
- package/dist/plus/integrations/providers/utils/providerPaging.js +47 -1
- package/dist/plus/integrations/providers/utils/providerPaging.js.map +1 -1
- package/dist/plus/integrations/providers/utils.d.ts +1 -1
- package/dist/plus/integrations/reads/batchEtags.d.ts +69 -0
- package/dist/plus/integrations/reads/batchEtags.js +114 -0
- package/dist/plus/integrations/reads/batchEtags.js.map +1 -0
- package/dist/plus/integrations/reads/counts.js +2 -2
- package/dist/plus/integrations/reads/counts.js.map +1 -1
- package/dist/plus/integrations/reads/currentAccount.js +2 -2
- package/dist/plus/integrations/reads/currentAccount.js.map +1 -1
- package/dist/plus/integrations/reads/cursors.d.ts +8 -0
- package/dist/plus/integrations/reads/cursors.js +5 -3
- package/dist/plus/integrations/reads/cursors.js.map +1 -1
- package/dist/plus/integrations/reads/drains.d.ts +13 -4
- package/dist/plus/integrations/reads/drains.js +29 -5
- package/dist/plus/integrations/reads/drains.js.map +1 -1
- package/dist/plus/integrations/reads/etag.d.ts +26 -0
- package/dist/plus/integrations/reads/etag.js +108 -0
- package/dist/plus/integrations/reads/etag.js.map +1 -0
- package/dist/plus/integrations/reads/filters.d.ts +8 -0
- package/dist/plus/integrations/reads/filters.js +2 -1
- package/dist/plus/integrations/reads/filters.js.map +1 -1
- package/dist/plus/integrations/reads/hierarchy.js +3 -3
- package/dist/plus/integrations/reads/hierarchy.js.map +1 -1
- package/dist/plus/integrations/reads/issueBatch.d.ts +47 -16
- package/dist/plus/integrations/reads/issueBatch.js +55 -87
- package/dist/plus/integrations/reads/issueBatch.js.map +1 -1
- package/dist/plus/integrations/reads/issueTracker.d.ts +10 -2
- package/dist/plus/integrations/reads/issueTracker.js +23 -6
- package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
- package/dist/plus/integrations/reads/issues.d.ts +1 -1
- package/dist/plus/integrations/reads/issues.js +4 -4
- package/dist/plus/integrations/reads/issues.js.map +1 -1
- package/dist/plus/integrations/reads/pullRequestBatch.d.ts +44 -6
- package/dist/plus/integrations/reads/pullRequestBatch.js +31 -34
- package/dist/plus/integrations/reads/pullRequestBatch.js.map +1 -1
- package/dist/plus/integrations/reads/pullRequestBranches.d.ts +5 -2
- package/dist/plus/integrations/reads/pullRequestBranches.js +4 -3
- package/dist/plus/integrations/reads/pullRequestBranches.js.map +1 -1
- package/dist/plus/integrations/reads/pullRequests.js +8 -4
- package/dist/plus/integrations/reads/pullRequests.js.map +1 -1
- package/dist/plus/integrations/reads/sweeps.js +8 -4
- package/dist/plus/integrations/reads/sweeps.js.map +1 -1
- package/dist/plus/integrations/reads/warnings.d.ts +23 -8
- package/dist/plus/integrations/reads/warnings.js +43 -19
- package/dist/plus/integrations/reads/warnings.js.map +1 -1
- package/dist/plus/integrations/results.d.ts +12 -4
- package/dist/plus/integrations/results.js +2 -0
- package/dist/plus/integrations/results.js.map +1 -1
- package/docs/integrations.md +389 -23
- package/docs/kepler-read-api-parity.md +16 -2
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drains.js","names":[],"sources":["../../../../../plus/integrations/src/reads/drains.ts"],"sourcesContent":["import type { PullRequestStateFilter } from '@gitlens/git/models/pullRequest.js';\nimport { mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { supportedOrderedCloudIntegrationIds } from '../constants.js';\nimport type { ProviderSweepTarget, PullRequestSweepOptions } from '../manager.js';\nimport type { GitHostIntegration } from '../models/gitHostIntegration.js';\nimport type { IntegrationResult } from '../models/integration.js';\nimport type { PullRequestFilter } from '../providerFilters.js';\nimport type { ProviderPullRequest, ProviderReposInput, ProviderRepository } from '../providers/models.js';\nimport { getProviderPullRequestIdentity } from '../providers/models.js';\nimport type { ProviderWarning } from '../results.js';\nimport { appendDedupedWarning, reconcileOmissionsWithFailure, toProviderWarning } from '../results.js';\nimport { isIssuesHostIntegrationId } from '../utils/integration.utils.js';\nimport { cappedBesideBudgetWarning, noConnectionWarning, truncationWarning } from './warnings.js';\n\n/**\n * The all-pages reads: a provider read run repeatedly until it runs out of pages, as opposed to the single-page\n * reads the facade's paged surface exposes. `sweepPullRequests` and `broadenIssues` are built on these.\n *\n * A drain's job is to keep going where a single page would stop, so what distinguishes it is its failure\n * bookkeeping rather than its loop: it must return the prefix it did read plus enough signal for the caller to\n * know the result is not authoritative — `truncated` when pages remain unread (a page backstop, or a provider\n * claiming another page while handing back no usable cursor), `fetchFailed` when a page or scope failed\n * outright. Discarding the prefix on a late failure, or returning it as if complete, are the two mistakes these\n * exist to prevent.\n */\n\n/**\n * Runs a result-returning read and captures failure as a neutral {@link ProviderWarning} rather than\n * letting it throw or silently vanish. Handles both a returned `{ error }` (the read cores' contract)\n * and a hard throw; a soft warning (`{ value, error }`) yields the value *and* a warning.\n */\nexport async function runCaptured<T>(\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tfn: () => Promise<IntegrationResult<T>>,\n\toptions?: { warnOnMissingSession?: boolean },\n): Promise<{ value?: T; warning?: ProviderWarning }> {\n\ttry {\n\t\tconst result = await fn();\n\t\tif (result == null) {\n\t\t\t// The read core returns undefined only when it couldn't resolve a session. For a per-connection\n\t\t\t// or explicit-domain read that means the requested target is gone or its authentication is invalid,\n\t\t\t// which must not be reported as an empty account. The untargeted primary path legitimately yields\n\t\t\t// nothing when the provider isn't connected, so leave it as an empty result.\n\t\t\treturn connectionId != null || options?.warnOnMissingSession\n\t\t\t\t? { warning: noConnectionWarning(id, domain, connectionId) }\n\t\t\t\t: {};\n\t\t}\n\t\tif (result.error != null) {\n\t\t\treturn { value: result.value, warning: toProviderWarning(id, domain, connectionId, result.error) };\n\t\t}\n\t\treturn { value: result.value };\n\t} catch (ex) {\n\t\treturn { warning: toProviderWarning(id, domain, connectionId, ex) };\n\t}\n}\n\n/**\n * Drains every page of the user's pull requests for one git-host integration, threading the opaque\n * next-cursor the provider returns (so it works for both page- and cursor-based hosts). Stops at\n * `maxPages` (marking `truncated`) or on a hard read failure (marking `fetchFailed`), keeping the\n * pages fetched so far. A soft warning (`{ value, error }`) is recorded but the drain continues.\n */\nexport async function drainPullRequests(\n\tintegration: GitHostIntegration,\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\trepos: ProviderReposInput,\n\tstate: PullRequestStateFilter[] | undefined,\n\tfilters: PullRequestFilter[] | undefined,\n\t/**\n\t * Breadth opt-ins that only the account-wide read can express. Grouped rather than passed positionally\n\t * because they are same-typed neighbors a transposition would silently swap, and picked from the sweep\n\t * options rather than restated so a third opt-in is declared once.\n\t */\n\taccountWideOptions: Pick<PullRequestSweepOptions, 'includeReviewRequested' | 'includeReviews'> | undefined,\n\tconnectionId: string | undefined,\n\tmaxPages: number,\n\tattributeUnavailableProvider: boolean,\n): Promise<{\n\titems: ProviderPullRequest[];\n\twarnings: ProviderWarning[];\n\tfetchFailed: boolean;\n\ttruncated: boolean;\n\tfailedProvider: boolean;\n}> {\n\tconst items: ProviderPullRequest[] = [];\n\tconst itemIndexByIdentity = new Map<string, number>();\n\tconst warnings: ProviderWarning[] = [];\n\tlet cursor: string | undefined;\n\tlet page = 0;\n\t// SDK metadata failures across pages mean the collection is incomplete even when no page threw; carry\n\t// this through the terminal returns instead of resetting it to false at the last page.\n\tlet fetchFailed = false;\n\tlet truncated = false;\n\t// A page the provider capped or couldn't vouch for, and which no other warning already explains — decides\n\t// whether this drain raises one of its own for that part.\n\tlet unexplainedTruncation = false;\n\n\t// With no repos this is an account-wide \"my PRs\" sweep. The repo-scoped core rejects an empty `repos`\n\t// input, so read the provider-native account-wide core instead.\n\tconst accountWide = repos.length === 0;\n\t// The breadth opt-ins are only expressible on the account-wide core, so gate them here — the single place\n\t// that already knows which core it is about to call.\n\tconst includeReviewRequested = accountWide && (accountWideOptions?.includeReviewRequested ?? false);\n\tconst includeReviews = accountWide && (accountWideOptions?.includeReviews ?? false);\n\t/** Every cursor already followed, so a provider that cycles them can't keep the drain walking in circles. */\n\tconst seenCursors = new Set<string>();\n\n\tfor (;;) {\n\t\tpage++;\n\t\t// Snapshot the mutable loop cursor so the read closure doesn't capture a later-reassigned value.\n\t\tconst pageCursor = cursor;\n\t\tconst { value, warning } = await runCaptured(id, domain, connectionId, () =>\n\t\t\taccountWide\n\t\t\t\t? integration.getMyPullRequestsForUserResult(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tstate: state,\n\t\t\t\t\t\t\tcursor: pageCursor,\n\t\t\t\t\t\t\tincludeReviewRequested: includeReviewRequested,\n\t\t\t\t\t\t\tfilters: filters,\n\t\t\t\t\t\t\t// Lite by default; the full projection (reviews) only when a caller opts in.\n\t\t\t\t\t\t\tsummary: !includeReviews,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tconnectionId,\n\t\t\t\t\t)\n\t\t\t\t: integration.getMyPullRequestsForReposResult(\n\t\t\t\t\t\trepos,\n\t\t\t\t\t\t{ state: state, filters: filters, cursor: pageCursor },\n\t\t\t\t\t\tconnectionId,\n\t\t\t\t\t),\n\t\t);\n\t\tif (warning != null) {\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t}\n\t\tif (value == null) {\n\t\t\t// An implicit sweep may silently skip a provider that has no session before it yields data.\n\t\t\t// Once a page has been returned, however, losing that session leaves an unread tail and must\n\t\t\t// be attributed even when the provider wasn't explicitly requested.\n\t\t\tconst sessionLostAfterProgress = warning == null && page > 1;\n\t\t\tconst unavailable = warning == null && (attributeUnavailableProvider || sessionLostAfterProgress);\n\t\t\tif (unavailable) {\n\t\t\t\tappendDedupedWarning(warnings, noConnectionWarning(id, domain, connectionId));\n\t\t\t}\n\t\t\t// `warning` set → a hard read failure (incomplete items); otherwise not connected / no session.\n\t\t\tconst failed = fetchFailed || warning != null || unavailable;\n\t\t\t// A cap seen on an earlier page still left results out, so it is still worth saying — but as part\n\t\t\t// of a read that failed, never as an omission. (`failed` is always true here when anything was\n\t\t\t// latched: reaching this exit past page 1 means a later page was lost.)\n\t\t\tif (unexplainedTruncation) {\n\t\t\t\tappendDedupedWarning(\n\t\t\t\t\twarnings,\n\t\t\t\t\ttruncationWarning(id, domain, connectionId, 'Pull request', failed ? 'interrupted' : 'exhausted'),\n\t\t\t\t);\n\t\t\t}\n\t\t\t// An earlier page may already have emitted an omission before this one died; it asserts the read\n\t\t\t// succeeded, which is no longer true.\n\t\t\treconcileOmissionsWithFailure(warnings, failed);\n\t\t\treturn {\n\t\t\t\titems: items,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: failed,\n\t\t\t\ttruncated: truncated || sessionLostAfterProgress,\n\t\t\t\t// Only a top-level first-page rejection means the provider itself failed. A later-page or\n\t\t\t\t// per-scope failure still yielded a usable provider slice and stays represented separately.\n\t\t\t\tfailedProvider: page === 1 && (warning != null || unavailable),\n\t\t\t};\n\t\t}\n\n\t\t// Composite account-wide queries can surface the same PR on different relationship/state pages\n\t\t// (for example authored + review-requested, or closed + merged). Keep the first stable position but\n\t\t// replace its value with the latest representation so a later, richer merged state wins. Repository\n\t\t// identity is what keys a row (see `getProviderPullRequestIdentity`), falling back to the canonical\n\t\t// URL; a row with neither stays unkeyed and is retained so unrelated incomplete rows are never\n\t\t// collapsed.\n\t\tfor (const pullRequest of value.values) {\n\t\t\tconst identity = getProviderPullRequestIdentity(pullRequest);\n\t\t\tif (identity == null) {\n\t\t\t\titems.push(pullRequest);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst existingIndex = itemIndexByIdentity.get(identity);\n\t\t\tif (existingIndex == null) {\n\t\t\t\titemIndexByIdentity.set(identity, items.length);\n\t\t\t\titems.push(pullRequest);\n\t\t\t} else {\n\t\t\t\titems[existingIndex] = pullRequest;\n\t\t\t}\n\t\t}\n\n\t\t// Assess this page's SDK metadata: append scope-aware warnings (deduped across pages), and remember\n\t\t// whether a structured failure or incompleteness occurred.\n\t\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, value.metadata);\n\t\tfetchFailed = fetchFailed || assessment.fetchFailed;\n\t\tconst pageTruncated =\n\t\t\t(value as { truncated?: boolean }).truncated === true ||\n\t\t\tvalue.paging?.truncated === true ||\n\t\t\tassessment.truncated;\n\t\ttruncated = truncated || pageTruncated;\n\t\t// Whether this drain owes its OWN warning for a capped page — `mergeAssessmentInto` has already appended\n\t\t// one when the fact came from SDK metadata, and repeating it would be noise.\n\t\tunexplainedTruncation = unexplainedTruncation || (pageTruncated && !assessment.truncated);\n\n\t\tif (!(value.paging?.more ?? false)) {\n\t\t\t// A read that can't confirm completeness (single-page provider reads with no `hasNextPage`)\n\t\t\t// sets `paging.truncated`; propagate it (and any top-level `truncated` and SDK incompleteness)\n\t\t\t// so the sweep doesn't claim an all-pages result.\n\t\t\tif (unexplainedTruncation) {\n\t\t\t\tappendDedupedWarning(\n\t\t\t\t\twarnings,\n\t\t\t\t\ttruncationWarning(\n\t\t\t\t\t\tid,\n\t\t\t\t\t\tdomain,\n\t\t\t\t\t\tconnectionId,\n\t\t\t\t\t\t'Pull request',\n\t\t\t\t\t\tfetchFailed ? 'interrupted' : 'exhausted',\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn {\n\t\t\t\titems: items,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: fetchFailed,\n\t\t\t\ttruncated: truncated,\n\t\t\t\tfailedProvider: false,\n\t\t\t};\n\t\t}\n\n\t\t// Resolve the continuation BEFORE deciding why the drain stops. `page-budget` claims the missing items\n\t\t// are reachable, which is only true with a usable cursor in hand — and a provider can report another\n\t\t// page while handing back none (Bitbucket Server does, when it omits `nextPageStart`). Checking the\n\t\t// budget first would label that unreachable tail as merely unfetched, so raising `maxPages` would\n\t\t// return the identical set.\n\t\tconst nextCursor = value.paging?.cursor;\n\t\t// A cursor already used isn't a continuation either — following it refetches a page we have, and a\n\t\t// provider that cycles (A→B→A) would otherwise burn the whole budget and then be reported as merely\n\t\t// out of budget. Tracked as a SET rather than compared one-back, matching the SDK's own `followCursors`:\n\t\t// `drainToRequestedPage` and `collectProviderPagedResult` compare only the previous cursor, which a\n\t\t// cycle slips past. That is tolerable there and not here, because only this drain reports `page-budget`.\n\t\tconst continuable = nextCursor != null && nextCursor !== '{}' && !seenCursors.has(nextCursor);\n\t\tif (!continuable || page >= maxPages) {\n\t\t\t// A usable cursor in hand means `page-budget`, even when an earlier page was capped: a composite\n\t\t\t// read caps its facets independently, so one facet reaching the provider's ceiling leaves its\n\t\t\t// siblings' cursors live, and raising the budget returns more of them. What no budget returns is the\n\t\t\t// capped part, so that is reported BESIDE the budget stop rather than in place of it — unless an SDK\n\t\t\t// omission already says so. Neither warning then misstates the part the other describes.\n\t\t\tconst budgetStop = !fetchFailed && continuable;\n\t\t\tappendDedupedWarning(\n\t\t\t\twarnings,\n\t\t\t\ttruncationWarning(\n\t\t\t\t\tid,\n\t\t\t\t\tdomain,\n\t\t\t\t\tconnectionId,\n\t\t\t\t\t'Pull request',\n\t\t\t\t\tfetchFailed ? 'interrupted' : budgetStop ? 'page-budget' : 'exhausted',\n\t\t\t\t),\n\t\t\t);\n\t\t\tif (budgetStop && unexplainedTruncation) {\n\t\t\t\tappendDedupedWarning(warnings, cappedBesideBudgetWarning(id, domain, connectionId, 'Pull request'));\n\t\t\t}\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn {\n\t\t\t\titems: items,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: fetchFailed,\n\t\t\t\ttruncated: true,\n\t\t\t\tfailedProvider: false,\n\t\t\t};\n\t\t}\n\n\t\tseenCursors.add(nextCursor);\n\t\tcursor = nextCursor;\n\t}\n}\n\n/**\n * Drains every page of repositories under an org for one git-host integration, threading the opaque\n * next-cursor the provider returns. Stops at `maxPages` (marking `truncated`) or on a hard read failure\n * (marking `fetchFailed`), keeping the pages fetched so far.\n */\nexport async function drainRepositories(\n\tintegration: GitHostIntegration,\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\torg: string,\n\tproject: string | undefined,\n\tconnectionId: string | undefined,\n\tmaxPages: number,\n): Promise<{\n\trepos: ProviderRepository[];\n\twarnings: ProviderWarning[];\n\tfetchFailed: boolean;\n\ttruncated: boolean;\n}> {\n\tconst repos: ProviderRepository[] = [];\n\tconst warnings: ProviderWarning[] = [];\n\tlet fetchFailed = false;\n\tlet truncated = false;\n\tlet cursor: string | undefined;\n\tlet page = 0;\n\n\tfor (;;) {\n\t\tpage++;\n\t\tconst pageCursor = cursor;\n\t\tconst { value, warning } = await runCaptured(\n\t\t\tid,\n\t\t\tdomain,\n\t\t\tconnectionId,\n\t\t\t() =>\n\t\t\t\tintegration.getRepositoriesForOrgResult(org, {\n\t\t\t\t\tproject: project,\n\t\t\t\t\tcursor: pageCursor,\n\t\t\t\t\tconnectionId: connectionId,\n\t\t\t\t}),\n\t\t\t{ warnOnMissingSession: true },\n\t\t);\n\t\tif (warning != null) {\n\t\t\t// Deduped, like every other accumulation here: a soft warning (`{ value, error }`) repeats verbatim on\n\t\t\t// each page that hits the same condition, and the array's contract is that no two entries are equal.\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t}\n\t\tif (value == null) {\n\t\t\tconst interruptedAfterProgress = page > 1;\n\t\t\tif (interruptedAfterProgress && warning == null) {\n\t\t\t\t// Not a backstop: the read was cut short mid-drain, which is why it also sets `fetchFailed`\n\t\t\t\t// below. A retry may complete it, so this must not claim the succeeded-but-capped omission.\n\t\t\t\tappendDedupedWarning(\n\t\t\t\t\twarnings,\n\t\t\t\t\ttruncationWarning(id, domain, connectionId, 'Repository', 'interrupted'),\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst failed = fetchFailed || warning != null || interruptedAfterProgress;\n\t\t\t// An SDK omission from an earlier page asserts the read succeeded; this one didn't.\n\t\t\treconcileOmissionsWithFailure(warnings, failed);\n\t\t\treturn {\n\t\t\t\trepos: repos,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: failed,\n\t\t\t\ttruncated: truncated || interruptedAfterProgress,\n\t\t\t};\n\t\t}\n\n\t\trepos.push(...value.values);\n\t\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, value.metadata);\n\t\tfetchFailed = fetchFailed || assessment.fetchFailed;\n\t\ttruncated = truncated || value.truncated === true || value.paging?.truncated === true || assessment.truncated;\n\t\tif (!(value.paging?.more ?? false)) {\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn {\n\t\t\t\trepos: repos,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: fetchFailed,\n\t\t\t\ttruncated: truncated,\n\t\t\t};\n\t\t}\n\n\t\tif (page >= maxPages) {\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn { repos: repos, warnings: warnings, fetchFailed: fetchFailed, truncated: true };\n\t\t}\n\n\t\tconst nextCursor = value.paging?.cursor;\n\t\tif (nextCursor == null || nextCursor === '{}') {\n\t\t\t// Provider says there is more but didn't return a usable cursor; stop rather than refetch the same page.\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn { repos: repos, warnings: warnings, fetchFailed: fetchFailed, truncated: true };\n\t\t}\n\n\t\tcursor = nextCursor;\n\t}\n}\n\nexport function resolvePullRequestSweepTargets(options: PullRequestSweepOptions | undefined): {\n\ttargets: readonly ProviderSweepTarget[];\n\tattributeUnavailableProviders: boolean;\n} {\n\tif (options?.targets != null) {\n\t\tif (options.providerIds != null || options.connectionId != null) {\n\t\t\tthrow new TypeError(\"Pull request sweep 'targets' cannot be combined with 'providerIds' or 'connectionId'\");\n\t\t}\n\n\t\tconst seenProviderIds = new Set<IntegrationIds>();\n\t\tfor (const target of options.targets) {\n\t\t\tif (seenProviderIds.has(target.providerId)) {\n\t\t\t\tthrow new TypeError(\n\t\t\t\t\t`Pull request sweep targets must contain at most one target per provider; duplicate '${target.providerId}'`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tseenProviderIds.add(target.providerId);\n\t\t}\n\n\t\treturn { targets: options.targets, attributeUnavailableProviders: true };\n\t}\n\n\tconst providerIds =\n\t\toptions?.providerIds ?? supportedOrderedCloudIntegrationIds.filter(id => !isIssuesHostIntegrationId(id));\n\tconst connectionId = providerIds.length === 1 ? options?.connectionId : undefined;\n\treturn {\n\t\ttargets: providerIds.map(providerId => ({ providerId: providerId, connectionId: connectionId })),\n\t\tattributeUnavailableProviders: options?.providerIds != null,\n\t};\n}\n\nexport async function getCurrentAccountIdentity(\n\tintegration: GitHostIntegration,\n\tconnectionId: string | undefined,\n): Promise<{ id: string; username?: string } | undefined> {\n\ttry {\n\t\tconst account = await integration.getCurrentAccount({ connectionId: connectionId });\n\t\treturn account != null ? { id: account.id, username: account.username } : undefined;\n\t} catch {\n\t\t// Authorship is optional enrichment; don't turn a successful PR read into a failure if identity lookup fails.\n\t\treturn undefined;\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"drains.js","names":[],"sources":["../../../../../plus/integrations/src/reads/drains.ts"],"sourcesContent":["import type { PullRequest, PullRequestStateFilter } from '@gitlens/git/models/pullRequest.js';\nimport { defaultPullRequestSort } from '@gitlens/git/models/pullRequest.js';\nimport { mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { supportedOrderedCloudIntegrationIds } from '../constants.js';\nimport type { ProviderSweepTarget, PullRequestSweepOptions } from '../manager.js';\nimport type { GitHostIntegration } from '../models/gitHostIntegration.js';\nimport type { IntegrationResult } from '../models/integration.js';\nimport type { PullRequestFilter } from '../providerFilters.js';\nimport type { ProviderPullRequest, ProviderReposInput, ProviderRepository } from '../providers/models.js';\nimport { getProviderPullRequestIdentity } from '../providers/models.js';\nimport type { ProviderWarning } from '../results.js';\nimport { appendDedupedWarning, reconcileOmissionsWithFailure, toProviderWarning } from '../results.js';\nimport { isIssuesHostIntegrationId } from '../utils/integration.utils.js';\nimport {\n\tcappedBesideBudgetWarning,\n\tnoConnectionWarning,\n\tpullRequestSearchCapResultWarning,\n\ttruncationWarning,\n} from './warnings.js';\n\n/**\n * The all-pages reads: a provider read run repeatedly until it runs out of pages, as opposed to the single-page\n * reads the facade's paged surface exposes. `sweepPullRequests` and `broadenIssues` are built on these.\n *\n * A drain's job is to keep going where a single page would stop, so what distinguishes it is its failure\n * bookkeeping rather than its loop: it must return the prefix it did read plus enough signal for the caller to\n * know the result is not authoritative — `truncated` when pages remain unread (a page backstop, or a provider\n * claiming another page while handing back no usable cursor), `fetchFailed` when a page or scope failed\n * outright. Discarding the prefix on a late failure, or returning it as if complete, are the two mistakes these\n * exist to prevent.\n */\n\n/**\n * Runs a result-returning read and captures failure as a neutral {@link ProviderWarning} rather than\n * letting it throw or silently vanish. Handles both a returned `{ error }` (the read cores' contract)\n * and a hard throw; a soft warning (`{ value, error }`) yields the value *and* a warning.\n */\nexport async function runCaptured<T>(\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tfn: () => Promise<IntegrationResult<T>>,\n\toptions?: { warnOnMissingSession?: boolean },\n): Promise<{ value?: T; warning?: ProviderWarning }> {\n\ttry {\n\t\tconst result = await fn();\n\t\tif (result == null) {\n\t\t\t// The read core returns undefined only when it couldn't resolve a session. For a per-connection\n\t\t\t// or explicit-domain read that means the requested target is gone or its authentication is invalid,\n\t\t\t// which must not be reported as an empty account. The untargeted primary path legitimately yields\n\t\t\t// nothing when the provider isn't connected, so leave it as an empty result.\n\t\t\treturn connectionId != null || options?.warnOnMissingSession\n\t\t\t\t? { warning: noConnectionWarning(id, domain, connectionId) }\n\t\t\t\t: {};\n\t\t}\n\t\tif (result.error != null) {\n\t\t\treturn { value: result.value, warning: toProviderWarning(id, domain, connectionId, result.error) };\n\t\t}\n\t\treturn { value: result.value };\n\t} catch (ex) {\n\t\treturn { warning: toProviderWarning(id, domain, connectionId, ex) };\n\t}\n}\n\n/**\n * Drains every page of the user's pull requests for one git-host integration, threading the opaque\n * next-cursor the provider returns (so it works for both page- and cursor-based hosts). Stops at\n * `maxPages` (marking `truncated`) or on a hard read failure (marking `fetchFailed`), keeping the\n * pages fetched so far. A soft warning (`{ value, error }`) is recorded but the drain continues.\n */\nexport async function drainPullRequests(\n\tintegration: GitHostIntegration,\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\trepos: ProviderReposInput,\n\tstate: PullRequestStateFilter[] | undefined,\n\tfilters: PullRequestFilter[] | undefined,\n\t/**\n\t * Breadth opt-ins that only the account-wide read can express. Grouped rather than passed positionally\n\t * because they are same-typed neighbors a transposition would silently swap, and picked from the sweep\n\t * options rather than restated so a third opt-in is declared once.\n\t */\n\taccountWideOptions: Pick<PullRequestSweepOptions, 'includeReviewRequested' | 'includeReviews'> | undefined,\n\tconnectionId: string | undefined,\n\tmaxPages: number,\n\tattributeUnavailableProvider: boolean,\n): Promise<{\n\titems: (ProviderPullRequest | PullRequest)[];\n\twarnings: ProviderWarning[];\n\tfetchFailed: boolean;\n\ttruncated: boolean;\n\tfailedProvider: boolean;\n}> {\n\tconst items: (ProviderPullRequest | PullRequest)[] = [];\n\tconst itemIndexByIdentity = new Map<string, number>();\n\tconst warnings: ProviderWarning[] = [];\n\tlet cursor: string | undefined;\n\tlet page = 0;\n\t// SDK metadata failures across pages mean the collection is incomplete even when no page threw; carry\n\t// this through the terminal returns instead of resetting it to false at the last page.\n\tlet fetchFailed = false;\n\tlet truncated = false;\n\t// A page the provider capped or couldn't vouch for, and which no other warning already explains — decides\n\t// whether this drain raises one of its own for that part.\n\tlet unexplainedTruncation = false;\n\t// The largest match count reported on a truncated page, from which the drain derives the provider's cap.\n\tlet truncatedTotalCount: number | undefined;\n\tconst capWarning = (): ProviderWarning | undefined =>\n\t\tpullRequestSearchCapResultWarning(id, domain, connectionId, truncatedTotalCount, defaultPullRequestSort);\n\tconst appendTruncationWarnings = (cause: 'interrupted' | 'exhausted'): void => {\n\t\tconst cap = capWarning();\n\t\tif (cap != null) {\n\t\t\tappendDedupedWarning(warnings, cap);\n\t\t}\n\t\t// A cap explains the missing tail of a read that succeeded; a failed read still owes its own warning.\n\t\tif (unexplainedTruncation && (cap == null || cause === 'interrupted')) {\n\t\t\tappendDedupedWarning(warnings, truncationWarning(id, domain, connectionId, 'Pull request', cause));\n\t\t}\n\t};\n\n\t// With no repos this is an account-wide \"my PRs\" sweep. The repo-scoped core rejects an empty `repos`\n\t// input, so read the provider-native account-wide core instead.\n\tconst accountWide = repos.length === 0;\n\t// The breadth opt-ins are only expressible on the account-wide core, so gate them here — the single place\n\t// that already knows which core it is about to call.\n\tconst includeReviewRequested = accountWide && (accountWideOptions?.includeReviewRequested ?? false);\n\tconst includeReviews = accountWide && (accountWideOptions?.includeReviews ?? false);\n\t/** Every cursor already followed, so a provider that cycles them can't keep the drain walking in circles. */\n\tconst seenCursors = new Set<string>();\n\n\tfor (;;) {\n\t\tpage++;\n\t\t// Snapshot the mutable loop cursor so the read closure doesn't capture a later-reassigned value.\n\t\tconst pageCursor = cursor;\n\t\tconst { value, warning } = await runCaptured(id, domain, connectionId, () =>\n\t\t\taccountWide\n\t\t\t\t? integration.getMyPullRequestsForUserResult(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tstate: state,\n\t\t\t\t\t\t\tcursor: pageCursor,\n\t\t\t\t\t\t\tincludeReviewRequested: includeReviewRequested,\n\t\t\t\t\t\t\tfilters: filters,\n\t\t\t\t\t\t\t// Lite by default; the full projection (reviews) only when a caller opts in.\n\t\t\t\t\t\t\tsummary: !includeReviews,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tconnectionId,\n\t\t\t\t\t)\n\t\t\t\t: integration.getMyPullRequestsForReposResult(\n\t\t\t\t\t\trepos,\n\t\t\t\t\t\t{ state: state, filters: filters, cursor: pageCursor },\n\t\t\t\t\t\tconnectionId,\n\t\t\t\t\t),\n\t\t);\n\t\tif (warning != null) {\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t}\n\t\tif (value == null) {\n\t\t\t// An implicit sweep may silently skip a provider that has no session before it yields data.\n\t\t\t// Once a page has been returned, however, losing that session leaves an unread tail and must\n\t\t\t// be attributed even when the provider wasn't explicitly requested.\n\t\t\tconst sessionLostAfterProgress = warning == null && page > 1;\n\t\t\tconst unavailable = warning == null && (attributeUnavailableProvider || sessionLostAfterProgress);\n\t\t\tif (unavailable) {\n\t\t\t\tappendDedupedWarning(warnings, noConnectionWarning(id, domain, connectionId));\n\t\t\t}\n\t\t\t// `warning` set → a hard read failure (incomplete items); otherwise not connected / no session.\n\t\t\tconst failed = fetchFailed || warning != null || unavailable;\n\t\t\t// A cap seen on an earlier page still left results out, so it is still worth saying — but as part\n\t\t\t// of a read that failed, never as an omission. (`failed` is always true here when anything was\n\t\t\t// latched: reaching this exit past page 1 means a later page was lost.)\n\t\t\tappendTruncationWarnings(failed ? 'interrupted' : 'exhausted');\n\t\t\t// An earlier page may already have emitted an omission before this one died; it asserts the read\n\t\t\t// succeeded, which is no longer true.\n\t\t\treconcileOmissionsWithFailure(warnings, failed);\n\t\t\treturn {\n\t\t\t\titems: items,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: failed,\n\t\t\t\ttruncated: truncated || sessionLostAfterProgress,\n\t\t\t\t// Only a top-level first-page rejection means the provider itself failed. A later-page or\n\t\t\t\t// per-scope failure still yielded a usable provider slice and stays represented separately.\n\t\t\t\tfailedProvider: page === 1 && (warning != null || unavailable),\n\t\t\t};\n\t\t}\n\n\t\t// Composite account-wide queries can surface the same PR on different relationship/state pages\n\t\t// (for example authored + review-requested, or closed + merged). Keep the first stable position but\n\t\t// replace its value with the latest representation so a later, richer merged state wins. Repository\n\t\t// identity is what keys a row (see `getProviderPullRequestIdentity`), falling back to the canonical\n\t\t// URL; a row with neither stays unkeyed and is retained so unrelated incomplete rows are never\n\t\t// collapsed.\n\t\tfor (const pullRequest of value.values) {\n\t\t\tconst identity = getProviderPullRequestIdentity(pullRequest);\n\t\t\tif (identity == null) {\n\t\t\t\titems.push(pullRequest);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst existingIndex = itemIndexByIdentity.get(identity);\n\t\t\tif (existingIndex == null) {\n\t\t\t\titemIndexByIdentity.set(identity, items.length);\n\t\t\t\titems.push(pullRequest);\n\t\t\t} else {\n\t\t\t\titems[existingIndex] = pullRequest;\n\t\t\t}\n\t\t}\n\n\t\t// Assess this page's SDK metadata: append scope-aware warnings (deduped across pages), and remember\n\t\t// whether a structured failure or incompleteness occurred.\n\t\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, value.metadata);\n\t\tfetchFailed = fetchFailed || assessment.fetchFailed;\n\t\tconst pageTruncated =\n\t\t\t(value as { truncated?: boolean }).truncated === true ||\n\t\t\tvalue.paging?.truncated === true ||\n\t\t\tassessment.truncated;\n\t\ttruncated = truncated || pageTruncated;\n\t\t// Whether this drain owes its OWN warning for a capped page — `mergeAssessmentInto` has already appended\n\t\t// one when the fact came from SDK metadata, and repeating it would be noise.\n\t\tunexplainedTruncation = unexplainedTruncation || (pageTruncated && !assessment.truncated);\n\t\t// Only the account-wide read reports a search match count here; the repo-scoped one reports the item\n\t\t// total, which no cap is derived from. Latched even when SDK metadata explains the page: a composite page\n\t\t// reports a failed or cursorless sibling facet there, which says nothing about the cap another reached.\n\t\tif (accountWide && pageTruncated && value.paging?.totalCount != null) {\n\t\t\ttruncatedTotalCount = Math.max(truncatedTotalCount ?? 0, value.paging.totalCount);\n\t\t}\n\n\t\tif (!(value.paging?.more ?? false)) {\n\t\t\t// A read that can't confirm completeness (single-page provider reads with no `hasNextPage`)\n\t\t\t// sets `paging.truncated`; propagate it (and any top-level `truncated` and SDK incompleteness)\n\t\t\t// so the sweep doesn't claim an all-pages result.\n\t\t\tappendTruncationWarnings(fetchFailed ? 'interrupted' : 'exhausted');\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn {\n\t\t\t\titems: items,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: fetchFailed,\n\t\t\t\ttruncated: truncated,\n\t\t\t\tfailedProvider: false,\n\t\t\t};\n\t\t}\n\n\t\t// Resolve the continuation BEFORE deciding why the drain stops. `page-budget` claims the missing items\n\t\t// are reachable, which is only true with a usable cursor in hand — and a provider can report another\n\t\t// page while handing back none (Bitbucket Server does, when it omits `nextPageStart`). Checking the\n\t\t// budget first would label that unreachable tail as merely unfetched, so raising `maxPages` would\n\t\t// return the identical set.\n\t\tconst nextCursor = value.paging?.cursor;\n\t\t// A cursor already used isn't a continuation either — following it refetches a page we have, and a\n\t\t// provider that cycles (A→B→A) would otherwise burn the whole budget and then be reported as merely\n\t\t// out of budget. Tracked as a SET rather than compared one-back, matching the SDK's own `followCursors`:\n\t\t// `drainToRequestedPage` and `collectProviderPagedResult` compare only the previous cursor, which a\n\t\t// cycle slips past. That is tolerable there and not here, because only this drain reports `page-budget`.\n\t\tconst continuable = nextCursor != null && nextCursor !== '{}' && !seenCursors.has(nextCursor);\n\t\tif (!continuable || page >= maxPages) {\n\t\t\t// A usable cursor in hand means `page-budget`, even when an earlier page was capped: a composite\n\t\t\t// read caps its facets independently, so one facet reaching the provider's ceiling leaves its\n\t\t\t// siblings' cursors live, and raising the budget returns more of them. What no budget returns is the\n\t\t\t// capped part, so that is reported BESIDE the budget stop rather than in place of it — unless an SDK\n\t\t\t// omission already says so. Neither warning then misstates the part the other describes.\n\t\t\tconst budgetStop = !fetchFailed && continuable;\n\t\t\tappendDedupedWarning(\n\t\t\t\twarnings,\n\t\t\t\ttruncationWarning(\n\t\t\t\t\tid,\n\t\t\t\t\tdomain,\n\t\t\t\t\tconnectionId,\n\t\t\t\t\t'Pull request',\n\t\t\t\t\tfetchFailed ? 'interrupted' : budgetStop ? 'page-budget' : 'exhausted',\n\t\t\t\t),\n\t\t\t);\n\t\t\tconst cap = capWarning();\n\t\t\tif (cap != null) {\n\t\t\t\tappendDedupedWarning(warnings, cap);\n\t\t\t} else if (budgetStop && unexplainedTruncation) {\n\t\t\t\tappendDedupedWarning(warnings, cappedBesideBudgetWarning(id, domain, connectionId, 'Pull request'));\n\t\t\t}\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn {\n\t\t\t\titems: items,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: fetchFailed,\n\t\t\t\ttruncated: true,\n\t\t\t\tfailedProvider: false,\n\t\t\t};\n\t\t}\n\n\t\tseenCursors.add(nextCursor);\n\t\tcursor = nextCursor;\n\t}\n}\n\n/**\n * Drains every page of repositories under an org for one git-host integration, threading the opaque\n * next-cursor the provider returns. Stops at `maxPages` (marking `truncated`) or on a hard read failure\n * (marking `fetchFailed`), keeping the pages fetched so far.\n */\nexport async function drainRepositories(\n\tintegration: GitHostIntegration,\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\torg: string,\n\tproject: string | undefined,\n\tconnectionId: string | undefined,\n\tmaxPages: number,\n): Promise<{\n\trepos: ProviderRepository[];\n\twarnings: ProviderWarning[];\n\tfetchFailed: boolean;\n\ttruncated: boolean;\n}> {\n\tconst repos: ProviderRepository[] = [];\n\tconst warnings: ProviderWarning[] = [];\n\tlet fetchFailed = false;\n\tlet truncated = false;\n\tlet cursor: string | undefined;\n\tlet page = 0;\n\n\tfor (;;) {\n\t\tpage++;\n\t\tconst pageCursor = cursor;\n\t\tconst { value, warning } = await runCaptured(\n\t\t\tid,\n\t\t\tdomain,\n\t\t\tconnectionId,\n\t\t\t() =>\n\t\t\t\tintegration.getRepositoriesForOrgResult(org, {\n\t\t\t\t\tproject: project,\n\t\t\t\t\tcursor: pageCursor,\n\t\t\t\t\tconnectionId: connectionId,\n\t\t\t\t}),\n\t\t\t{ warnOnMissingSession: true },\n\t\t);\n\t\tif (warning != null) {\n\t\t\t// Deduped, like every other accumulation here: a soft warning (`{ value, error }`) repeats verbatim on\n\t\t\t// each page that hits the same condition, and the array's contract is that no two entries are equal.\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t}\n\t\tif (value == null) {\n\t\t\tconst interruptedAfterProgress = page > 1;\n\t\t\tif (interruptedAfterProgress && warning == null) {\n\t\t\t\t// Not a backstop: the read was cut short mid-drain, which is why it also sets `fetchFailed`\n\t\t\t\t// below. A retry may complete it, so this must not claim the succeeded-but-capped omission.\n\t\t\t\tappendDedupedWarning(\n\t\t\t\t\twarnings,\n\t\t\t\t\ttruncationWarning(id, domain, connectionId, 'Repository', 'interrupted'),\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst failed = fetchFailed || warning != null || interruptedAfterProgress;\n\t\t\t// An SDK omission from an earlier page asserts the read succeeded; this one didn't.\n\t\t\treconcileOmissionsWithFailure(warnings, failed);\n\t\t\treturn {\n\t\t\t\trepos: repos,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: failed,\n\t\t\t\ttruncated: truncated || interruptedAfterProgress,\n\t\t\t};\n\t\t}\n\n\t\trepos.push(...value.values);\n\t\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, value.metadata);\n\t\tfetchFailed = fetchFailed || assessment.fetchFailed;\n\t\ttruncated = truncated || value.truncated === true || value.paging?.truncated === true || assessment.truncated;\n\t\tif (!(value.paging?.more ?? false)) {\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn {\n\t\t\t\trepos: repos,\n\t\t\t\twarnings: warnings,\n\t\t\t\tfetchFailed: fetchFailed,\n\t\t\t\ttruncated: truncated,\n\t\t\t};\n\t\t}\n\n\t\tif (page >= maxPages) {\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn { repos: repos, warnings: warnings, fetchFailed: fetchFailed, truncated: true };\n\t\t}\n\n\t\tconst nextCursor = value.paging?.cursor;\n\t\tif (nextCursor == null || nextCursor === '{}') {\n\t\t\t// Provider says there is more but didn't return a usable cursor; stop rather than refetch the same page.\n\t\t\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\t\t\treturn { repos: repos, warnings: warnings, fetchFailed: fetchFailed, truncated: true };\n\t\t}\n\n\t\tcursor = nextCursor;\n\t}\n}\n\nexport function resolvePullRequestSweepTargets(options: PullRequestSweepOptions | undefined): {\n\ttargets: readonly ProviderSweepTarget[];\n\tattributeUnavailableProviders: boolean;\n} {\n\tif (options?.targets != null) {\n\t\tif (options.providerIds != null || options.connectionId != null) {\n\t\t\tthrow new TypeError(\"Pull request sweep 'targets' cannot be combined with 'providerIds' or 'connectionId'\");\n\t\t}\n\n\t\tconst seenProviderIds = new Set<IntegrationIds>();\n\t\tfor (const target of options.targets) {\n\t\t\tif (seenProviderIds.has(target.providerId)) {\n\t\t\t\tthrow new TypeError(\n\t\t\t\t\t`Pull request sweep targets must contain at most one target per provider; duplicate '${target.providerId}'`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tseenProviderIds.add(target.providerId);\n\t\t}\n\n\t\treturn { targets: options.targets, attributeUnavailableProviders: true };\n\t}\n\n\tconst providerIds =\n\t\toptions?.providerIds ?? supportedOrderedCloudIntegrationIds.filter(id => !isIssuesHostIntegrationId(id));\n\tconst connectionId = providerIds.length === 1 ? options?.connectionId : undefined;\n\treturn {\n\t\ttargets: providerIds.map(providerId => ({ providerId: providerId, connectionId: connectionId })),\n\t\tattributeUnavailableProviders: options?.providerIds != null,\n\t};\n}\n\nexport async function getCurrentAccountIdentity(\n\tintegration: GitHostIntegration,\n\tconnectionId: string | undefined,\n): Promise<{ id: string; username?: string } | undefined> {\n\ttry {\n\t\tconst account = await integration.getCurrentAccount({ connectionId: connectionId });\n\t\treturn account != null ? { id: account.id, username: account.username } : undefined;\n\t} catch {\n\t\t// Authorship is optional enrichment; don't turn a successful PR read into a failure if identity lookup fails.\n\t\treturn undefined;\n\t}\n}\n\n/**\n * The current account to map each raw row against, in order (see {@link GitHostIntegration.getPullRequestViewers}).\n * Rows with no author can't be matched, so a read of only those resolves nothing; a failed lookup leaves every row's\n * authorship unknown rather than failing the read.\n */\nexport async function getPullRequestViewers(\n\tintegration: GitHostIntegration,\n\tconnectionId: string | undefined,\n\tpullRequests: readonly (ProviderPullRequest | PullRequest)[],\n): Promise<({ id: string; username?: string } | undefined)[]> {\n\tif (!pullRequests.some(pr => pr.author != null)) return pullRequests.map(() => undefined);\n\n\ttry {\n\t\treturn await integration.getPullRequestViewers(pullRequests, connectionId);\n\t} catch {\n\t\treturn pullRequests.map(() => undefined);\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { IssueShape } from "../../../git/models/issue.js";
|
|
2
|
+
import { PullRequestShape } from "../../../git/models/pullRequest.js";
|
|
3
|
+
import { IssueEtagFields, IssueEtagInclude, PullRequestEtagFields, PullRequestEtagInclude } from "../models/integration.js";
|
|
4
|
+
//#region ../plus/integrations/src/reads/etag.d.ts
|
|
5
|
+
export declare function pullRequestEtagFieldsFromShape(pr: PullRequestShape): PullRequestEtagFields;
|
|
6
|
+
export declare function issueEtagFieldsFromShape(issue: IssueShape): IssueEtagFields;
|
|
7
|
+
/**
|
|
8
|
+
* The distinct `includes` in canonical order, whatever order and repetition the caller gave. Values it doesn't know
|
|
9
|
+
* are dropped, so a caller validates them first (see {@link findInvalidPullRequestEtagInclude}).
|
|
10
|
+
*/
|
|
11
|
+
export declare function normalizePullRequestEtagIncludes(includes: readonly PullRequestEtagInclude[]): readonly PullRequestEtagInclude[];
|
|
12
|
+
/** The first of `includes` that isn't a {@link PullRequestEtagInclude}, if any. */
|
|
13
|
+
export declare function findInvalidPullRequestEtagInclude(includes: readonly string[]): string | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Each of `includes` adds one input — mergeability, review decision or check rollup — which a host may change
|
|
16
|
+
* without moving `updatedDate`. An input that isn't listed is left out, so its churn never forces a full read.
|
|
17
|
+
*/
|
|
18
|
+
export declare function pullRequestEtag(fields: PullRequestEtagFields, includes: readonly PullRequestEtagInclude[]): string;
|
|
19
|
+
/** The issue twin of {@link normalizePullRequestEtagIncludes}; validate with {@link findInvalidIssueEtagInclude}. */
|
|
20
|
+
export declare function normalizeIssueEtagIncludes(includes: readonly IssueEtagInclude[]): readonly IssueEtagInclude[];
|
|
21
|
+
/** The first of `includes` that isn't an {@link IssueEtagInclude}, if any. */
|
|
22
|
+
export declare function findInvalidIssueEtagInclude(includes: readonly string[]): string | undefined;
|
|
23
|
+
/** `'reactions'` adds the thumbs-up count, which a host changes without moving `updatedDate`. */
|
|
24
|
+
export declare function issueEtag(fields: IssueEtagFields, includes: readonly IssueEtagInclude[]): string;
|
|
25
|
+
//#endregion
|
|
26
|
+
//# sourceMappingURL=etag.d.ts.map
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import "../constants.js";
|
|
2
|
+
import { getIssueFieldPresence } from "../fieldPresence.js";
|
|
3
|
+
import { issueEtagIncludes, pullRequestEtagIncludes, pullRequestRevision } from "../models/integration.js";
|
|
4
|
+
//#region ../plus/integrations/src/reads/etag.ts
|
|
5
|
+
/**
|
|
6
|
+
* The etags the batch reads hand back (`getPullRequestsBatch`, `getIssuesBatch`): an opaque stamp of an item's
|
|
7
|
+
* change state, which a caller sends back so a cheap check can answer "unchanged" without a full read.
|
|
8
|
+
*
|
|
9
|
+
* Computed by core, never the provider's HTTP ETag, and from the fields alone — not the item's id, since the
|
|
10
|
+
* caller's key already identifies the target and ids cross vocabularies between reads. Not hashed: the stamp IS
|
|
11
|
+
* the serialized fields, so two different change states can never collide into a false `unchanged` — except for an
|
|
12
|
+
* Azure DevOps pull request's `revision`, a 64-bit hash of the fields Azure changes without a timestamp, where a
|
|
13
|
+
* collision (about 2^-64) is the only way a change can hide. The prefix names the scheme (`pr1`, `is1`) and every
|
|
14
|
+
* include it covers ({@link PullRequestEtagInclude}, {@link IssueEtagInclude}), in canonical order
|
|
15
|
+
* (`pr1+mergeable+checks:`, `is1+reactions:`), so two different sets can never collide even when their values do (a
|
|
16
|
+
* `null` mergeability and a `null` rollup). A stamp from another scheme or another set just compares unequal and
|
|
17
|
+
* costs a full read.
|
|
18
|
+
*/
|
|
19
|
+
/** The hosts that keep no update time on a pull request, whose etag adds a {@link pullRequestRevision}. */
|
|
20
|
+
const revisionProviderIds = /* @__PURE__ */ new Set(["azureDevOps", "azure-devops-server"]);
|
|
21
|
+
function pullRequestEtagFieldsFromShape(pr) {
|
|
22
|
+
const fields = {
|
|
23
|
+
state: pr.state,
|
|
24
|
+
isDraft: pr.isDraft,
|
|
25
|
+
updatedDate: pr.updatedDate,
|
|
26
|
+
headSha: pr.refs?.head.sha,
|
|
27
|
+
mergeableState: pr.mergeableState,
|
|
28
|
+
reviewDecision: pr.reviewDecision,
|
|
29
|
+
statusCheckRollupState: "statusCheckRollupState" in pr ? pr.statusCheckRollupState : void 0
|
|
30
|
+
};
|
|
31
|
+
if (pr.provider != null && revisionProviderIds.has(pr.provider.id)) fields.revision = pullRequestRevision(pr);
|
|
32
|
+
return fields;
|
|
33
|
+
}
|
|
34
|
+
function issueEtagFieldsFromShape(issue) {
|
|
35
|
+
const fields = {
|
|
36
|
+
state: issue.state,
|
|
37
|
+
updatedDate: issue.updatedDate
|
|
38
|
+
};
|
|
39
|
+
if (issue.thumbsUpCount != null && issue.provider != null && getIssueFieldPresence(issue)?.reactions === "fetched") fields.thumbsUpCount = issue.thumbsUpCount;
|
|
40
|
+
return fields;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The distinct `includes` in canonical order, whatever order and repetition the caller gave. Values it doesn't know
|
|
44
|
+
* are dropped, so a caller validates them first (see {@link findInvalidPullRequestEtagInclude}).
|
|
45
|
+
*/
|
|
46
|
+
function normalizePullRequestEtagIncludes(includes) {
|
|
47
|
+
return pullRequestEtagIncludes.filter((include) => includes.includes(include));
|
|
48
|
+
}
|
|
49
|
+
/** The first of `includes` that isn't a {@link PullRequestEtagInclude}, if any. */
|
|
50
|
+
function findInvalidPullRequestEtagInclude(includes) {
|
|
51
|
+
return includes.find((include) => !pullRequestEtagIncludes.includes(include));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Each of `includes` adds one input — mergeability, review decision or check rollup — which a host may change
|
|
55
|
+
* without moving `updatedDate`. An input that isn't listed is left out, so its churn never forces a full read.
|
|
56
|
+
*/
|
|
57
|
+
function pullRequestEtag(fields, includes) {
|
|
58
|
+
const covered = normalizePullRequestEtagIncludes(includes);
|
|
59
|
+
const state = [
|
|
60
|
+
fields.state,
|
|
61
|
+
fields.isDraft ?? null,
|
|
62
|
+
timeOf(fields.updatedDate),
|
|
63
|
+
fields.headSha ?? null
|
|
64
|
+
];
|
|
65
|
+
if (fields.revision != null) state.push(fields.revision);
|
|
66
|
+
if (covered.length === 0) return `pr1:${JSON.stringify(state)}`;
|
|
67
|
+
for (const include of covered) switch (include) {
|
|
68
|
+
case "mergeable":
|
|
69
|
+
state.push(fields.mergeableState ?? null);
|
|
70
|
+
break;
|
|
71
|
+
case "reviewDecision":
|
|
72
|
+
state.push(fields.reviewDecision ?? null);
|
|
73
|
+
break;
|
|
74
|
+
case "checks": state.push(fields.statusCheckRollupState ?? null);
|
|
75
|
+
}
|
|
76
|
+
return `pr1+${covered.join("+")}:${JSON.stringify(state)}`;
|
|
77
|
+
}
|
|
78
|
+
/** The issue twin of {@link normalizePullRequestEtagIncludes}; validate with {@link findInvalidIssueEtagInclude}. */
|
|
79
|
+
function normalizeIssueEtagIncludes(includes) {
|
|
80
|
+
return issueEtagIncludes.filter((include) => includes.includes(include));
|
|
81
|
+
}
|
|
82
|
+
/** The first of `includes` that isn't an {@link IssueEtagInclude}, if any. */
|
|
83
|
+
function findInvalidIssueEtagInclude(includes) {
|
|
84
|
+
return includes.find((include) => !issueEtagIncludes.includes(include));
|
|
85
|
+
}
|
|
86
|
+
/** `'reactions'` adds the thumbs-up count, which a host changes without moving `updatedDate`. */
|
|
87
|
+
function issueEtag(fields, includes) {
|
|
88
|
+
const covered = normalizeIssueEtagIncludes(includes);
|
|
89
|
+
const state = [fields.state, timeOf(fields.updatedDate)];
|
|
90
|
+
if (covered.length === 0) return `is1:${JSON.stringify(state)}`;
|
|
91
|
+
for (const include of covered) switch (include) {
|
|
92
|
+
case "reactions": state.push(fields.thumbsUpCount ?? null);
|
|
93
|
+
}
|
|
94
|
+
return `is1+${covered.join("+")}:${JSON.stringify(state)}`;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Never throws: an etag is computed for every fully read row, so a mapper that hands over a missing or unparseable
|
|
98
|
+
* date must not fail the whole read. Such a row's etag just won't match a cheap check's, which always has a date,
|
|
99
|
+
* so the cost is a full read, never a false `unchanged`.
|
|
100
|
+
*/
|
|
101
|
+
function timeOf(date) {
|
|
102
|
+
const time = new Date(date).getTime();
|
|
103
|
+
return Number.isNaN(time) ? null : time;
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
export { findInvalidIssueEtagInclude, findInvalidPullRequestEtagInclude, issueEtag, issueEtagFieldsFromShape, normalizeIssueEtagIncludes, normalizePullRequestEtagIncludes, pullRequestEtag, pullRequestEtagFieldsFromShape };
|
|
107
|
+
|
|
108
|
+
//# sourceMappingURL=etag.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"etag.js","names":[],"sources":["../../../../../plus/integrations/src/reads/etag.ts"],"sourcesContent":["import type { IssueShape } from '@gitlens/git/models/issue.js';\nimport type { PullRequest, PullRequestShape } from '@gitlens/git/models/pullRequest.js';\nimport { GitCloudHostIntegrationId, GitSelfManagedHostIntegrationId } from '../constants.js';\nimport { getIssueFieldPresence } from '../fieldPresence.js';\nimport type {\n\tIssueEtagFields,\n\tIssueEtagInclude,\n\tPullRequestEtagFields,\n\tPullRequestEtagInclude,\n} from '../models/integration.js';\nimport { issueEtagIncludes, pullRequestEtagIncludes, pullRequestRevision } from '../models/integration.js';\n\n/**\n * The etags the batch reads hand back (`getPullRequestsBatch`, `getIssuesBatch`): an opaque stamp of an item's\n * change state, which a caller sends back so a cheap check can answer \"unchanged\" without a full read.\n *\n * Computed by core, never the provider's HTTP ETag, and from the fields alone — not the item's id, since the\n * caller's key already identifies the target and ids cross vocabularies between reads. Not hashed: the stamp IS\n * the serialized fields, so two different change states can never collide into a false `unchanged` — except for an\n * Azure DevOps pull request's `revision`, a 64-bit hash of the fields Azure changes without a timestamp, where a\n * collision (about 2^-64) is the only way a change can hide. The prefix names the scheme (`pr1`, `is1`) and every\n * include it covers ({@link PullRequestEtagInclude}, {@link IssueEtagInclude}), in canonical order\n * (`pr1+mergeable+checks:`, `is1+reactions:`), so two different sets can never collide even when their values do (a\n * `null` mergeability and a `null` rollup). A stamp from another scheme or another set just compares unequal and\n * costs a full read.\n */\n\n/** The hosts that keep no update time on a pull request, whose etag adds a {@link pullRequestRevision}. */\nconst revisionProviderIds: ReadonlySet<string> = new Set([\n\tGitCloudHostIntegrationId.AzureDevOps,\n\tGitSelfManagedHostIntegrationId.AzureDevOpsServer,\n]);\n\nexport function pullRequestEtagFieldsFromShape(pr: PullRequestShape): PullRequestEtagFields {\n\tconst fields: PullRequestEtagFields = {\n\t\tstate: pr.state,\n\t\tisDraft: pr.isDraft,\n\t\tupdatedDate: pr.updatedDate,\n\t\theadSha: pr.refs?.head.sha,\n\t\tmergeableState: pr.mergeableState,\n\t\treviewDecision: pr.reviewDecision,\n\t\t// On the class, not the shape: every provider's mapper builds a `PullRequest`. Read by name rather than by\n\t\t// `instanceof`, which a second copy of the model module fails, silently dropping the rollup from every full\n\t\t// row's etag so that no cheap check ever matches it again.\n\t\tstatusCheckRollupState: 'statusCheckRollupState' in pr ? (pr as PullRequest).statusCheckRollupState : undefined,\n\t};\n\t// Read defensively, as `timeOf` reads the date: a row without a provider must not fail the read.\n\tif (pr.provider != null && revisionProviderIds.has(pr.provider.id)) {\n\t\tfields.revision = pullRequestRevision(pr);\n\t}\n\treturn fields;\n}\n\nexport function issueEtagFieldsFromShape(issue: IssueShape): IssueEtagFields {\n\tconst fields: IssueEtagFields = { state: issue.state, updatedDate: issue.updatedDate };\n\t// Only where the row's read really fetched reactions: elsewhere a count is a placeholder or a tracker's votes,\n\t// which no cheap check reads, so taking it would fail every check of that row under `'reactions'`.\n\tif (\n\t\tissue.thumbsUpCount != null &&\n\t\tissue.provider != null &&\n\t\tgetIssueFieldPresence(issue)?.reactions === 'fetched'\n\t) {\n\t\tfields.thumbsUpCount = issue.thumbsUpCount;\n\t}\n\treturn fields;\n}\n\n/**\n * The distinct `includes` in canonical order, whatever order and repetition the caller gave. Values it doesn't know\n * are dropped, so a caller validates them first (see {@link findInvalidPullRequestEtagInclude}).\n */\nexport function normalizePullRequestEtagIncludes(\n\tincludes: readonly PullRequestEtagInclude[],\n): readonly PullRequestEtagInclude[] {\n\treturn pullRequestEtagIncludes.filter(include => includes.includes(include));\n}\n\n/** The first of `includes` that isn't a {@link PullRequestEtagInclude}, if any. */\nexport function findInvalidPullRequestEtagInclude(includes: readonly string[]): string | undefined {\n\treturn includes.find(include => !(pullRequestEtagIncludes as readonly string[]).includes(include));\n}\n\n/**\n * Each of `includes` adds one input — mergeability, review decision or check rollup — which a host may change\n * without moving `updatedDate`. An input that isn't listed is left out, so its churn never forces a full read.\n */\nexport function pullRequestEtag(fields: PullRequestEtagFields, includes: readonly PullRequestEtagInclude[]): string {\n\tconst covered = normalizePullRequestEtagIncludes(includes);\n\tconst state: unknown[] = [fields.state, fields.isDraft ?? null, timeOf(fields.updatedDate), fields.headSha ?? null];\n\t// Only where it is set, so every other host's etag stays exactly what it was.\n\tif (fields.revision != null) {\n\t\tstate.push(fields.revision);\n\t}\n\n\tif (covered.length === 0) return `pr1:${JSON.stringify(state)}`;\n\n\tfor (const include of covered) {\n\t\tswitch (include) {\n\t\t\tcase 'mergeable':\n\t\t\t\tstate.push(fields.mergeableState ?? null);\n\t\t\t\tbreak;\n\t\t\tcase 'reviewDecision':\n\t\t\t\tstate.push(fields.reviewDecision ?? null);\n\t\t\t\tbreak;\n\t\t\tcase 'checks':\n\t\t\t\tstate.push(fields.statusCheckRollupState ?? null);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn `pr1+${covered.join('+')}:${JSON.stringify(state)}`;\n}\n\n/** The issue twin of {@link normalizePullRequestEtagIncludes}; validate with {@link findInvalidIssueEtagInclude}. */\nexport function normalizeIssueEtagIncludes(includes: readonly IssueEtagInclude[]): readonly IssueEtagInclude[] {\n\treturn issueEtagIncludes.filter(include => includes.includes(include));\n}\n\n/** The first of `includes` that isn't an {@link IssueEtagInclude}, if any. */\nexport function findInvalidIssueEtagInclude(includes: readonly string[]): string | undefined {\n\treturn includes.find(include => !(issueEtagIncludes as readonly string[]).includes(include));\n}\n\n/** `'reactions'` adds the thumbs-up count, which a host changes without moving `updatedDate`. */\nexport function issueEtag(fields: IssueEtagFields, includes: readonly IssueEtagInclude[]): string {\n\tconst covered = normalizeIssueEtagIncludes(includes);\n\tconst state: unknown[] = [fields.state, timeOf(fields.updatedDate)];\n\tif (covered.length === 0) return `is1:${JSON.stringify(state)}`;\n\n\tfor (const include of covered) {\n\t\tswitch (include) {\n\t\t\tcase 'reactions':\n\t\t\t\tstate.push(fields.thumbsUpCount ?? null);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn `is1+${covered.join('+')}:${JSON.stringify(state)}`;\n}\n\n/**\n * Never throws: an etag is computed for every fully read row, so a mapper that hands over a missing or unparseable\n * date must not fail the whole read. Such a row's etag just won't match a cheap check's, which always has a date,\n * so the cost is a full read, never a false `unchanged`.\n */\nfunction timeOf(date: Date): number | null {\n\tconst time = new Date(date).getTime();\n\treturn Number.isNaN(time) ? null : time;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA4BA,MAAM,sCAA2C,IAAI,IAAI,CAAA,eAAA,qBAGzD,CAAC;AAED,SAAgB,+BAA+B,IAA6C;CAC3F,MAAM,SAAgC;EACrC,OAAO,GAAG;EACV,SAAS,GAAG;EACZ,aAAa,GAAG;EAChB,SAAS,GAAG,MAAM,KAAK;EACvB,gBAAgB,GAAG;EACnB,gBAAgB,GAAG;EAInB,wBAAwB,4BAA4B,KAAM,GAAmB,yBAAyB,KAAA;CACvG;CAEA,IAAI,GAAG,YAAY,QAAQ,oBAAoB,IAAI,GAAG,SAAS,EAAE,GAChE,OAAO,WAAW,oBAAoB,EAAE;CAEzC,OAAO;AACR;AAEA,SAAgB,yBAAyB,OAAoC;CAC5E,MAAM,SAA0B;EAAE,OAAO,MAAM;EAAO,aAAa,MAAM;CAAY;CAGrF,IACC,MAAM,iBAAiB,QACvB,MAAM,YAAY,QAClB,sBAAsB,KAAK,CAAC,EAAE,cAAc,WAE5C,OAAO,gBAAgB,MAAM;CAE9B,OAAO;AACR;;;;;AAMA,SAAgB,iCACf,UACoC;CACpC,OAAO,wBAAwB,QAAO,YAAW,SAAS,SAAS,OAAO,CAAC;AAC5E;;AAGA,SAAgB,kCAAkC,UAAiD;CAClG,OAAO,SAAS,MAAK,YAAW,CAAE,wBAA8C,SAAS,OAAO,CAAC;AAClG;;;;;AAMA,SAAgB,gBAAgB,QAA+B,UAAqD;CACnH,MAAM,UAAU,iCAAiC,QAAQ;CACzD,MAAM,QAAmB;EAAC,OAAO;EAAO,OAAO,WAAW;EAAM,OAAO,OAAO,WAAW;EAAG,OAAO,WAAW;CAAI;CAElH,IAAI,OAAO,YAAY,MACtB,MAAM,KAAK,OAAO,QAAQ;CAG3B,IAAI,QAAQ,WAAW,GAAG,OAAO,OAAO,KAAK,UAAU,KAAK;CAE5D,KAAK,MAAM,WAAW,SACrB,QAAQ,SAAR;EACC,KAAK;GACJ,MAAM,KAAK,OAAO,kBAAkB,IAAI;GACxC;EACD,KAAK;GACJ,MAAM,KAAK,OAAO,kBAAkB,IAAI;GACxC;EACD,KAAK,UACJ,MAAM,KAAK,OAAO,0BAA0B,IAAI;CAElD;CAGD,OAAO,OAAO,QAAQ,KAAK,GAAG,EAAE,GAAG,KAAK,UAAU,KAAK;AACxD;;AAGA,SAAgB,2BAA2B,UAAoE;CAC9G,OAAO,kBAAkB,QAAO,YAAW,SAAS,SAAS,OAAO,CAAC;AACtE;;AAGA,SAAgB,4BAA4B,UAAiD;CAC5F,OAAO,SAAS,MAAK,YAAW,CAAE,kBAAwC,SAAS,OAAO,CAAC;AAC5F;;AAGA,SAAgB,UAAU,QAAyB,UAA+C;CACjG,MAAM,UAAU,2BAA2B,QAAQ;CACnD,MAAM,QAAmB,CAAC,OAAO,OAAO,OAAO,OAAO,WAAW,CAAC;CAClE,IAAI,QAAQ,WAAW,GAAG,OAAO,OAAO,KAAK,UAAU,KAAK;CAE5D,KAAK,MAAM,WAAW,SACrB,QAAQ,SAAR;EACC,KAAK,aACJ,MAAM,KAAK,OAAO,iBAAiB,IAAI;CAEzC;CAGD,OAAO,OAAO,QAAQ,KAAK,GAAG,EAAE,GAAG,KAAK,UAAU,KAAK;AACxD;;;;;;AAOA,SAAS,OAAO,MAA2B;CAC1C,MAAM,OAAO,IAAI,KAAK,IAAI,CAAC,CAAC,QAAQ;CACpC,OAAO,OAAO,MAAM,IAAI,IAAI,OAAO;AACpC"}
|
|
@@ -220,6 +220,14 @@ export type SupportedFilters = {
|
|
|
220
220
|
* `issueSorts`.
|
|
221
221
|
*/
|
|
222
222
|
issueSortsAccountWide: IssueSorting[];
|
|
223
|
+
/**
|
|
224
|
+
* Whether an issue tracker's reads can be asked for a `state` other than open (`closed`, `all`). False means
|
|
225
|
+
* only open issues are readable, and the read refuses the other two rather than serving the open list.
|
|
226
|
+
*
|
|
227
|
+
* Tracker-only, like `issues`: the git hosts' state support belongs to the filtered search, under
|
|
228
|
+
* `issueSearch.states`.
|
|
229
|
+
*/
|
|
230
|
+
issueStates: boolean;
|
|
223
231
|
};
|
|
224
232
|
/**
|
|
225
233
|
* The filters `listPullRequestsPage`/`listIssuesPage` (and the sweeps) accept for a provider, so a caller can
|
|
@@ -357,7 +357,8 @@ function getSupportedFilters(providerId) {
|
|
|
357
357
|
sorts: [...issueSearch?.sorts ?? []]
|
|
358
358
|
},
|
|
359
359
|
issueSorts: [...metadata?.supportedIssueSorts ?? []],
|
|
360
|
-
issueSortsAccountWide: [...metadata?.supportedAccountWideIssueSorts ?? []]
|
|
360
|
+
issueSortsAccountWide: [...metadata?.supportedAccountWideIssueSorts ?? []],
|
|
361
|
+
issueStates: metadata?.supportsIssueStates === true
|
|
361
362
|
};
|
|
362
363
|
}
|
|
363
364
|
//#endregion
|
|
@@ -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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"}
|
|
@@ -5,7 +5,7 @@ import { mergeAssessmentInto } from "../collectionMetadata.js";
|
|
|
5
5
|
import { isGitHostIntegration, isIssuesHostIntegrationId, warnOnMissingSessionForDomain } from "../utils/integration.utils.js";
|
|
6
6
|
import { toProviderRepositoryShape } from "../providers/models.js";
|
|
7
7
|
import { pageToCursor, refusedPage, resolveContinuation, resolveCurrentPage, toProviderPageInfo } from "./paging.js";
|
|
8
|
-
import { gitHostOnlySurfaceWarning,
|
|
8
|
+
import { gitHostOnlySurfaceWarning, truncationWarning, unsupportedWarning } from "./warnings.js";
|
|
9
9
|
import { runCaptured } from "./drains.js";
|
|
10
10
|
import { isIssuesIntegration } from "../models/issuesIntegration.js";
|
|
11
11
|
import { assertHierarchyReadTarget, mergeHierarchyResults, orgForProject, resourceMatchesOrg, resourceToOrg, withProviderContext } from "./hierarchy.utils.js";
|
|
@@ -66,7 +66,7 @@ async function listOrgs(ctx, options) {
|
|
|
66
66
|
}
|
|
67
67
|
} else if (!integration.supportsOrganizationDiscovery) {
|
|
68
68
|
fetchFailed = true;
|
|
69
|
-
warnings.push(
|
|
69
|
+
warnings.push(unsupportedWarning(id, domain, connectionId, `Organization discovery is not supported by '${id}'.`));
|
|
70
70
|
} else {
|
|
71
71
|
const { value, warning } = await runCaptured(id, domain, connectionId, () => integration.getOrganizationsForUserResult(connectionId), { warnOnMissingSession });
|
|
72
72
|
if (value != null) {
|
|
@@ -168,7 +168,7 @@ async function listRepos(ctx, options) {
|
|
|
168
168
|
if (!isGitHostIntegration(integration)) return refusedPage(page, [gitHostOnlySurfaceWarning(options.providerId, void 0, options.connectionId, "repository discovery")], true);
|
|
169
169
|
const domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);
|
|
170
170
|
const accountWide = options.org == null;
|
|
171
|
-
if (!(accountWide ? integration.supportsUserRepositoryDiscovery : integration.supportsRepositoryDiscovery)) return refusedPage(page, [
|
|
171
|
+
if (!(accountWide ? integration.supportsUserRepositoryDiscovery : integration.supportsRepositoryDiscovery)) return refusedPage(page, [unsupportedWarning(options.providerId, domain, options.connectionId, accountWide ? `Account-wide repository discovery is not supported by '${options.providerId}'; list repositories per org instead.` : `Repository discovery is not supported by '${options.providerId}'.`)], true);
|
|
172
172
|
const org = options.org;
|
|
173
173
|
const warnOnMissingSession = warnOnMissingSessionForDomain(options.providerId, options.domain);
|
|
174
174
|
const readPage = (cursor) => runCaptured(options.providerId, domain, options.connectionId, () => org == null ? integration.getRepositoriesForUserResult({
|