@gitkraken/core-gitlens 0.9.4 → 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 +22 -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 +7 -3
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/manager.d.ts +56 -6
- 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/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 +40 -3
- package/dist/plus/integrations/providers/bitbucket/models.js +30 -7
- 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/issueSorts.d.ts +1 -0
- package/dist/plus/integrations/providers/jira-server.d.ts +1 -0
- package/dist/plus/integrations/providers/jira-server.js +9 -6
- package/dist/plus/integrations/providers/jira-server.js.map +1 -1
- package/dist/plus/integrations/providers/jira.d.ts +20 -1
- package/dist/plus/integrations/providers/jira.js +101 -6
- 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 +97 -5
- 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 +51 -7
- package/dist/plus/integrations/providers/models.js +45 -16
- 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 +62 -3
- package/dist/plus/integrations/providers/providersApi.js +145 -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 +10 -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/drains.d.ts +13 -3
- 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/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 +1 -1
- package/dist/plus/integrations/reads/issueTracker.js +4 -4
- 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 +385 -23
- package/docs/kepler-read-api-parity.md +16 -2
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"issueBatch.js","names":[],"sources":["../../../../../plus/integrations/src/reads/issueBatch.ts"],"sourcesContent":["import type { IssueShape } from '@gitlens/git/models/issue.js';\nimport { mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds, IssuesHostIntegrationIds } from '../constants.js';\nimport { IssuesCloudHostIntegrationId } from '../constants.js';\nimport type { BatchSlot } from '../models/integration.js';\nimport { isIssuesIntegration } from '../models/issuesIntegration.js';\nimport { githubGraphQLInt32Max, isAzureProviderId, isGitHubProviderId } from '../providers/providerErrors.js';\nimport type { ProviderResult, ProviderWarning } from '../results.js';\nimport { appendDedupedWarning, toProviderWarning } from '../results.js';\nimport { areDomainsOnSameHost, hostFromDomain } from '../utils/domain.utils.js';\nimport {\n\tisGitHostIntegration,\n\tisIssuesHostIntegrationId,\n\tisIssuesSelfManagedHostIntegrationId,\n} from '../utils/integration.utils.js';\nimport type { ProviderReadContext } from './context.js';\nimport { runCaptured } from './drains.js';\nimport {\n\tgitHostOnlySurfaceWarning,\n\tissuesUnsupportedWarning,\n\tissueTrackerOnlySurfaceWarning,\n\tnoConnectionWarning,\n\totherWarning,\n} from './warnings.js';\n\n/**\n * The BATCH issue read: resolve N issues BY IDENTITY in one call — `(owner, repo, number)` coordinates on a git\n * host, `(resourceId, ABC-123)` identifiers on an issue tracker.\n *\n * A sibling of the issue searches rather than a mode of one, because it answers a different question. A search\n * asks \"what matches\"; this asks \"does this exact issue exist\", which is what a caller correlating a branch name\n * to the issue it references is actually asking. Three consequences follow, and they are the reason this exists:\n *\n * - It resolves by EXACT IDENTIFIER, so relevance never enters into it.\n * - No result ceiling applies, so there is no partial window to reason about and no omission to report.\n * - An absent slot is a PROVEN ABSENCE, not \"not found within a page budget\". That is the property that matters\n * most: a caller can CACHE a miss. Emulating this with a paged list cannot prove absence without walking the\n * whole scope, so a miss stays unproven and the walk repeats on every pass, forever.\n *\n * Modeled on `countIssues` for its shape — caller-owned `key` echoed back, per-target isolation, as few requests\n * as the provider allows — because both take a set of independent questions and answer them together.\n *\n * ONE integration call per invocation, whatever the target count, as in `getPullRequestsBatch`: the integration\n * fans its targets out (chunked for GitHub/GHE, one request per target with bounded concurrency on GitLab and\n * Azure DevOps and on the trackers) and settles each independently, so failing targets spend at most one strike\n * of the integration's failure budget.\n */\n\n/** One issue to resolve, echoed back under the caller's own `key`. Which form a call takes depends on its provider. */\nexport type IssueBatchTarget =\n\t/**\n\t * A git host's issue, by repository coordinate. GitHub/GHE, GitLab (and self-managed) and Azure DevOps (and\n\t * Server); Bitbucket and Bitbucket Data Center have no issues and refuse.\n\t */\n\t| {\n\t\t\tkey: string;\n\t\t\t/** GitHub owner, GitLab namespace path (may be nested), or Azure DevOps organization (or collection). */\n\t\t\towner: string;\n\t\t\t/**\n\t\t\t * Repository name. Required, but ignored on Azure DevOps, whose work items belong to the project rather\n\t\t\t * than to a repository.\n\t\t\t */\n\t\t\trepo: string;\n\t\t\t/** Issue number, GitLab issue iid, or Azure DevOps work item id. */\n\t\t\tnumber: number;\n\t\t\t/** Azure DevOps project. Required there, ignored elsewhere. */\n\t\t\tproject?: string;\n\t }\n\t/**\n\t * An issue tracker's issue, by its own identifier (e.g. `ABC-123`) within a resource. Jira (Cloud and Data\n\t * Center) and Linear. For Jira Data Center `resourceId` is the host — the instance's single resource, as\n\t * `listOrgs` reports it — and `resourceUrl` is not needed, since the browser link is built from the\n\t * connection's own base URL; Jira Cloud requires it.\n\t */\n\t| { key: string; resourceId: string; resourceUrl?: string; identifier: string };\n\ntype CoordinateTarget = Extract<IssueBatchTarget, { owner: string }>;\ntype TrackerTarget = Extract<IssueBatchTarget, { resourceId: string }>;\n\n/** The answer for one {@link IssueBatchTarget}. */\nexport interface IssueBatchResult {\n\tkey: string;\n\t/**\n\t * The resolved issue, or `undefined` when it PROVABLY does not exist (or is not visible to this connection).\n\t *\n\t * Absent is an answer here, unlike every paged read on this facade: a target that FAILED — its own request\n\t * outright, or just its own alias within an otherwise-answering GitHub chunk (e.g. an org enforcing SAML SSO\n\t * the token isn't authorized for) — is not returned at all and sets `fetchFailed`, so a caller can tell\n\t * \"proven absent\" from \"unknown\" and cache the first without ever caching the second.\n\t */\n\tissue?: IssueShape;\n}\n\nconst surface = 'Batch issue resolution';\n\nfunction refused(warning: ProviderWarning): ProviderResult<IssueBatchResult> {\n\treturn { items: [], warnings: [warning], fetchFailed: true };\n}\n\nexport async function getIssuesBatch(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\ttargets: readonly IssueBatchTarget[];\n\t\tconnectionId?: string;\n\t\t/**\n\t\t * Explicit self-managed host domain. Used only when the requested connection has no configured domain;\n\t\t * it must come from the trusted authentication configuration, not repository or remote data.\n\t\t */\n\t\tdomain?: string;\n\t},\n): Promise<ProviderResult<IssueBatchResult>> {\n\t// Nothing was asked for, so nothing is missing: an empty success, not a refusal.\n\tif (options.targets.length === 0) return { items: [], warnings: [] };\n\n\tconst duplicateKey = findDuplicateKey(options.targets);\n\tif (duplicateKey != null) {\n\t\t// Refuses the whole call rather than deduping: `key` exists so the caller can match results without\n\t\t// positional bookkeeping, and two results under one key make that ambiguous for EVERY target, not just\n\t\t// the repeated one.\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`Duplicate issue batch target key '${duplicateKey}'; keys identify results, so each must be unique.`,\n\t\t\t),\n\t\t);\n\t}\n\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn getIssuesBatchForTracker(ctx, options.providerId, options.targets, options.connectionId, options.domain);\n\t}\n\n\tconst targets = options.targets;\n\tif (!targets.every(isCoordinateTarget)) {\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`${surface} for '${options.providerId}' takes repository coordinates { key, owner, repo, number, project? }, not tracker identifiers.`,\n\t\t\t),\n\t\t);\n\t}\n\n\t// The validated value must be the one sent to the provider, so every string field is trimmed once here.\n\tconst trimmedTargets = targets.map(trimCoordinateFields);\n\n\t// Deterministic caller bugs, refused like a duplicate key rather than sent upstream, where a blank owner or repo\n\t// would come back as a missing repository — a proven absence the caller would cache.\n\tconst invalid = findInvalidCoordinateTarget(options.providerId, trimmedTargets);\n\tif (invalid != null) {\n\t\treturn refused(otherWarning(options.providerId, undefined, options.connectionId, invalid));\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\treturn unresolvedIntegration(ctx, options.providerId, options.connectionId, options.domain);\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refused(gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, surface));\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\n\tif (!integration.supportsIssues) {\n\t\treturn refused(issuesUnsupportedWarning(options.providerId, domain, options.connectionId));\n\t}\n\n\tconst warnings: ProviderWarning[] = [];\n\tlet fetchFailed = false;\n\n\tconst { value: slots, warning } = await runCaptured(\n\t\toptions.providerId,\n\t\tdomain,\n\t\toptions.connectionId,\n\t\t() =>\n\t\t\tintegration.getIssuesBatchResult(\n\t\t\t\ttrimmedTargets.map(t => ({ owner: t.owner, repo: t.repo, number: t.number, project: t.project })),\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t),\n\t\t// A missing session must surface as a connection warning, including on the primary path, which\n\t\t// otherwise reads it as \"not connected\" and says nothing — and this read would then call it unsupported.\n\t\t{ warnOnMissingSession: true },\n\t);\n\tif (warning != null) {\n\t\tappendDedupedWarning(warnings, warning);\n\t}\n\n\tif (slots == null) {\n\t\t// A provider that doesn't implement the batch hook answers `undefined` with no error. Either way its\n\t\t// targets are DROPPED rather than reported as absent — the difference between \"unknown\" and \"proven\n\t\t// absent\" is the read's whole value, and a failure must never be cached as an answer.\n\t\tif (warning == null) {\n\t\t\tappendDedupedWarning(\n\t\t\t\twarnings,\n\t\t\t\totherWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t`${surface} is not supported by '${options.providerId}'; resolve issues individually instead.`,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t\treturn { items: [], warnings: warnings, fetchFailed: true };\n\t}\n\n\tconst items: IssueBatchResult[] = [];\n\tfor (let i = 0; i < trimmedTargets.length; i++) {\n\t\tconst slot = slots[i];\n\t\tif (slot.status === 'rejected') {\n\t\t\t// Dropped, never reported absent, like a whole-call failure: only THIS target failed.\n\t\t\tappendBatchSlotWarning(warnings, options.providerId, domain, options.connectionId, slot);\n\t\t\tfetchFailed = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst issue = slot.value;\n\t\titems.push({ key: trimmedTargets[i].key, ...(issue != null ? { issue: issue } : {}) });\n\t}\n\n\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed || undefined };\n}\n\n/**\n * The tracker form. ONE integration call per invocation, whatever the target count: the integration makes one\n * single-issue request per target, with bounded concurrency, and settles each independently — see\n * `IssuesIntegration.getIssuesByResourceIdBatchResult`.\n *\n * `resourceId` is trusted and the read does no resource discovery. It is handed to the provider's by-resource-id\n * read as-is, never wrapped into a synthesized descriptor: Linear and Trello answer `undefined` for a descriptor\n * that fails `isIssueResourceDescriptor`, which would be published as a proven absence.\n *\n * `domain` selects the host of a self-managed tracker (Jira Data Center), as it does for\n * `listIssueTrackerIssuesPage`, and is ignored for the cloud trackers, which have a single canonical host. Unlike\n * the paged reads, this one never falls back to the primary connection for a self-managed tracker: it requires a\n * `domain` or a `connectionId` with a configured host and refuses otherwise. Two self-hosted instances routinely\n * issue the same project and issue keys, and `issue: undefined` is a proven absence a caller may cache — an answer\n * from whichever host happens to be primary would be cached under a key that names a different instance (#5872).\n */\nasync function getIssuesBatchForTracker(\n\tctx: ProviderReadContext,\n\tproviderId: IssuesHostIntegrationIds,\n\ttargets: readonly IssueBatchTarget[],\n\tconnectionId: string | undefined,\n\tdomain: string | undefined,\n): Promise<ProviderResult<IssueBatchResult>> {\n\tif (!targets.every(isTrackerTarget)) {\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tundefined,\n\t\t\t\tconnectionId,\n\t\t\t\t`${surface} for '${providerId}' takes tracker identifiers { key, resourceId, resourceUrl?, identifier }, not repository coordinates.`,\n\t\t\t),\n\t\t);\n\t}\n\n\t// The validated value must be the one sent to the provider, so every string field is trimmed once here.\n\tconst trimmedTargets = targets.map(trimTrackerTarget);\n\n\t// Deterministic caller bugs, refused like a duplicate key rather than sent upstream to fail one by one.\n\tconst invalid = findInvalidTrackerTarget(providerId, trimmedTargets);\n\tif (invalid != null) {\n\t\treturn refused(otherWarning(providerId, undefined, connectionId, invalid));\n\t}\n\n\t// A `domain` only selects a host when it parses to one, and a `connectionId` only when it names a configured\n\t// connection that has one; anything else would resolve the primary host instead, which is the fallback this\n\t// read refuses.\n\tif (\n\t\tisIssuesSelfManagedHostIntegrationId(providerId) &&\n\t\t(domain != null\n\t\t\t? hostFromDomain(domain) == null\n\t\t\t: connectionId == null ||\n\t\t\t\t!ctx.getConfigured(providerId).some(c => c.id === connectionId && hostFromDomain(c.domain) != null))\n\t) {\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tundefined,\n\t\t\t\tconnectionId,\n\t\t\t\t`${surface} requires a domain or a configured connection id for '${providerId}': every configured host can hold a different issue under the same key, so the read does not answer from the primary connection.`,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(providerId, connectionId, domain);\n\tif (integration == null) return unresolvedIntegration(ctx, providerId, connectionId, domain);\n\tif (!isIssuesIntegration(integration)) {\n\t\treturn refused(issueTrackerOnlySurfaceWarning(providerId, connectionId, surface));\n\t}\n\n\tconst unsupported = (): ProviderWarning =>\n\t\totherWarning(\n\t\t\tproviderId,\n\t\t\tundefined,\n\t\t\tconnectionId,\n\t\t\t`${surface} is not supported by '${providerId}'; its single-issue read cannot prove an absence, so a miss would not be safe to cache.`,\n\t\t);\n\tif (!integration.supportsIssueLookupByResourceId) return refused(unsupported());\n\n\t// A self-managed tracker's single resource is its host, and a caller keys (and caches) each answer by\n\t// `resourceId` while the read is addressed to the host `domain`/`connectionId` resolved. When they disagree,\n\t// host B's answer — including a cacheable absence — would be stored under host A's key. Refused whole, like\n\t// the caller bugs above: a target naming another host means the call selected a host its caller did not mean.\n\tif (isIssuesSelfManagedHostIntegrationId(providerId)) {\n\t\tconst mismatched = trimmedTargets.find(t => !areDomainsOnSameHost(t.resourceId, integration.domain));\n\t\tif (mismatched != null) {\n\t\t\treturn refused(\n\t\t\t\totherWarning(\n\t\t\t\t\tproviderId,\n\t\t\t\t\tundefined,\n\t\t\t\t\tconnectionId,\n\t\t\t\t\t`Issue batch target '${mismatched.key}' requires the resource id of '${providerId}' to name the host the read resolved to.`,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t}\n\n\tconst resolvedDomain = ctx.domainForRead(integration, providerId, connectionId, domain);\n\tconst warnings: ProviderWarning[] = [];\n\tlet fetchFailed = false;\n\n\tconst { value: slots, warning } = await runCaptured(\n\t\tproviderId,\n\t\tresolvedDomain,\n\t\tconnectionId,\n\t\t() =>\n\t\t\tintegration.getIssuesByResourceIdBatchResult(\n\t\t\t\ttrimmedTargets.map(t => ({\n\t\t\t\t\tresourceId: t.resourceId,\n\t\t\t\t\tidentifier: t.identifier,\n\t\t\t\t\tresourceUrl: t.resourceUrl,\n\t\t\t\t})),\n\t\t\t\tconnectionId,\n\t\t\t),\n\t\t// The read core answers `undefined` when it cannot resolve a session. That must surface as a connection\n\t\t// warning, including on the primary path, rather than as nothing — or worse, be read as absence.\n\t\t{ warnOnMissingSession: true },\n\t);\n\tif (warning != null) {\n\t\tappendDedupedWarning(warnings, warning);\n\t}\n\n\tif (slots == null) {\n\t\t// Dropped, never reported absent: \"unknown\" and \"proven absent\" must stay distinguishable.\n\t\tif (warning == null) {\n\t\t\tappendDedupedWarning(warnings, unsupported());\n\t\t}\n\t\treturn { items: [], warnings: warnings, fetchFailed: true };\n\t}\n\n\tconst items: IssueBatchResult[] = [];\n\tfor (let i = 0; i < trimmedTargets.length; i++) {\n\t\tconst slot = slots[i];\n\t\tif (slot.status === 'rejected') {\n\t\t\t// Dropped, never reported absent, like a whole-call failure.\n\t\t\tappendBatchSlotWarning(warnings, providerId, resolvedDomain, connectionId, slot);\n\t\t\tfetchFailed = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst issue = slot.value;\n\t\titems.push({ key: trimmedTargets[i].key, ...(issue != null ? { issue: issue } : {}) });\n\t}\n\n\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed || undefined };\n}\n\nfunction isCoordinateTarget(target: IssueBatchTarget): target is CoordinateTarget {\n\treturn 'owner' in target;\n}\n\nfunction isTrackerTarget(target: IssueBatchTarget): target is TrackerTarget {\n\treturn 'resourceId' in target;\n}\n\n/**\n * Trims `owner`, `repo` and `project` once, shared by every coordinate-form batch read (this one, the pull request\n * batch and the pull-requests-by-branch read): leading/trailing whitespace can't change which repository a\n * coordinate names, so it's stripped and accepted rather than refused, matching #5821's scope-name precedent.\n */\nexport function trimCoordinateFields<T extends { owner: string; repo: string; project?: string }>(target: T): T {\n\treturn { ...target, owner: target.owner.trim(), repo: target.repo.trim(), project: target.project?.trim() };\n}\n\n/** Trims `resourceId`, `resourceUrl` and `identifier` once; the validated value must be the one sent to the provider. */\nfunction trimTrackerTarget(target: TrackerTarget): TrackerTarget {\n\treturn {\n\t\t...target,\n\t\tresourceId: target.resourceId.trim(),\n\t\tresourceUrl: target.resourceUrl?.trim() || undefined,\n\t\tidentifier: target.identifier.trim(),\n\t};\n}\n\nfunction findInvalidCoordinateTarget(\n\tproviderId: IntegrationIds,\n\ttargets: readonly CoordinateTarget[],\n): string | undefined {\n\tconst isAzure = isAzureProviderId(providerId);\n\tconst isGitHub = isGitHubProviderId(providerId);\n\tfor (const target of targets) {\n\t\tif (!Number.isSafeInteger(target.number) || target.number <= 0) {\n\t\t\treturn `Issue batch target '${target.key}' has number ${target.number}; expected a positive integer.`;\n\t\t}\n\t\tif (isGitHub && target.number > githubGraphQLInt32Max) {\n\t\t\treturn `Issue batch target '${target.key}' has number ${target.number}; GitHub numbers are 32-bit and cannot exceed ${githubGraphQLInt32Max}.`;\n\t\t}\n\t\tif (target.owner.length === 0) {\n\t\t\treturn `Issue batch target '${target.key}' requires a non-empty owner.`;\n\t\t}\n\t\tif (isAzure) {\n\t\t\tif (!target.project) {\n\t\t\t\treturn `Issue batch target '${target.key}' requires a project for '${providerId}'.`;\n\t\t\t}\n\t\t} else if (target.repo.length === 0) {\n\t\t\treturn `Issue batch target '${target.key}' requires a non-empty repo.`;\n\t\t}\n\t}\n\treturn undefined;\n}\n\nfunction findInvalidTrackerTarget(\n\tproviderId: IssuesHostIntegrationIds,\n\ttargets: readonly TrackerTarget[],\n): string | undefined {\n\tfor (const target of targets) {\n\t\tif (target.resourceId.length === 0) {\n\t\t\treturn `Issue batch target '${target.key}' requires a resource id.`;\n\t\t}\n\t\tif (target.identifier.length === 0) {\n\t\t\treturn `Issue batch target '${target.key}' requires an issue identifier.`;\n\t\t}\n\t\tif (providerId === IssuesCloudHostIntegrationId.Jira && !target.resourceUrl) {\n\t\t\treturn `Issue batch target '${target.key}' requires the Jira resource URL so the result contains a browser link without resource discovery.`;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * The answer when no integration resolves for a batch read. Targets were asked about, so this is never a silent\n * empty success: a supplied connection or domain that no longer resolves gets its own warning, and the untargeted\n * primary path (e.g. a self-managed provider with no configured host) a connection warning, as `getCurrentAccount`\n * gives — either way `fetchFailed`, so no caller mistakes the dropped targets for an answer.\n */\nexport function unresolvedIntegration<T>(\n\tctx: ProviderReadContext,\n\tproviderId: IntegrationIds,\n\tconnectionId: string | undefined,\n\tdomain: string | undefined,\n): ProviderResult<T> {\n\tconst early = ctx.earlyReturnConnectionWarnings(providerId, connectionId, domain);\n\tif (early.warnings.length > 0) return { items: [], warnings: early.warnings, fetchFailed: true };\n\n\tconst resolvedDomain = ctx.resolveDomainForRead(providerId, connectionId, domain);\n\treturn { items: [], warnings: [noConnectionWarning(providerId, resolvedDomain, connectionId)], fetchFailed: true };\n}\n\n/**\n * Appends the warning for a batch target that could not be checked, shared by every batch read: for a refusal, the\n * scope failure it was recorded as (see `IntegrationBase.settleBatchRefusals`), published as an account-wide read\n * publishes one, scoped and with its cause; otherwise, the reason itself.\n */\nexport function appendBatchSlotWarning(\n\twarnings: ProviderWarning[],\n\tproviderId: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tslot: Extract<BatchSlot<unknown>, { status: 'rejected' }>,\n): void {\n\tif (slot.failure != null) {\n\t\tmergeAssessmentInto(warnings, providerId, domain, connectionId, {\n\t\t\tcompleteness: 'partial',\n\t\t\tfailures: [slot.failure],\n\t\t});\n\t\treturn;\n\t}\n\n\tappendDedupedWarning(warnings, toProviderWarning(providerId, domain, connectionId, slot.reason));\n}\n\nexport function findDuplicateKey(targets: readonly { key: string }[]): string | undefined {\n\tconst seen = new Set<string>();\n\tfor (const target of targets) {\n\t\tif (seen.has(target.key)) return target.key;\n\n\t\tseen.add(target.key);\n\t}\n\treturn undefined;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"issueBatch.js","names":[],"sources":["../../../../../plus/integrations/src/reads/issueBatch.ts"],"sourcesContent":["import type { IssueShape } from '@gitlens/git/models/issue.js';\nimport type { IntegrationIds, IssuesHostIntegrationIds } from '../constants.js';\nimport { IssuesCloudHostIntegrationId } from '../constants.js';\nimport type { IssueEtagInclude } from '../models/integration.js';\nimport { issueEtagIncludes } from '../models/integration.js';\nimport { isIssuesIntegration } from '../models/issuesIntegration.js';\nimport { githubGraphQLInt32Max, isAzureProviderId, isGitHubProviderId } from '../providers/providerErrors.js';\nimport type { ProviderResult, ProviderWarning } from '../results.js';\nimport { areDomainsOnSameHost, hostFromDomain } from '../utils/domain.utils.js';\nimport {\n\tisGitHostIntegration,\n\tisIssuesHostIntegrationId,\n\tisIssuesSelfManagedHostIntegrationId,\n} from '../utils/integration.utils.js';\nimport type { EtaggedBatchRow } from './batchEtags.js';\nimport { readEtaggedBatch } from './batchEtags.js';\nimport type { ProviderReadContext } from './context.js';\nimport { runCaptured } from './drains.js';\nimport {\n\tfindInvalidIssueEtagInclude,\n\tissueEtag,\n\tissueEtagFieldsFromShape,\n\tnormalizeIssueEtagIncludes,\n} from './etag.js';\nimport {\n\tgitHostOnlySurfaceWarning,\n\tissuesUnsupportedWarning,\n\tissueTrackerOnlySurfaceWarning,\n\tnoConnectionWarning,\n\totherWarning,\n\tunsupportedWarning,\n} from './warnings.js';\n\n/**\n * The BATCH issue read: resolve N issues BY IDENTITY in one call — `(owner, repo, number)` coordinates on a git\n * host, `(resourceId, ABC-123)` identifiers on an issue tracker.\n *\n * A sibling of the issue searches rather than a mode of one, because it answers a different question. A search\n * asks \"what matches\"; this asks \"does this exact issue exist\", which is what a caller correlating a branch name\n * to the issue it references is actually asking. Three consequences follow, and they are the reason this exists:\n *\n * - It resolves by EXACT IDENTIFIER, so relevance never enters into it.\n * - No result ceiling applies, so there is no partial window to reason about and no omission to report.\n * - An absent slot is a PROVEN ABSENCE, not \"not found within a page budget\". That is the property that matters\n * most: a caller can CACHE a miss. Emulating this with a paged list cannot prove absence without walking the\n * whole scope, so a miss stays unproven and the walk repeats on every pass, forever.\n *\n * Modeled on `countIssues` for its shape — caller-owned `key` echoed back, per-target isolation, as few requests\n * as the provider allows — because both take a set of independent questions and answer them together.\n *\n * ONE integration call per invocation when no target carries an `etag`, whatever the target count, as in\n * `getPullRequestsBatch`: the integration fans its targets out (chunked for GitHub/GHE, one request per target with\n * bounded concurrency on GitLab and Azure DevOps and on the trackers) and settles each independently, so failing\n * targets spend at most one strike of the integration's failure budget.\n *\n * With etags, up to THREE on a host with a cheap check (GitHub/GHE, GitLab and Azure DevOps; Jira Cloud and Linear\n * among the trackers): the cheap check of the targets that carry one, a full read of the rest started alongside it,\n * and a full read of the targets whose etag no longer matches — see `readEtaggedBatch`. Every other tracker still makes\n * one call and etags its rows.\n */\n\n/** One issue to resolve, echoed back under the caller's own `key`. Which form a call takes depends on its provider. */\nexport type IssueBatchTarget =\n\t/**\n\t * A git host's issue, by repository coordinate. GitHub/GHE, GitLab (and self-managed) and Azure DevOps (and\n\t * Server); Bitbucket and Bitbucket Data Center have no issues and refuse.\n\t */\n\t| {\n\t\t\tkey: string;\n\t\t\t/** GitHub owner, GitLab namespace path (may be nested), or Azure DevOps organization (or collection). */\n\t\t\towner: string;\n\t\t\t/**\n\t\t\t * Repository name. Required, but ignored on Azure DevOps, whose work items belong to the project rather\n\t\t\t * than to a repository.\n\t\t\t */\n\t\t\trepo: string;\n\t\t\t/** Issue number, GitLab issue iid, or Azure DevOps work item id. */\n\t\t\tnumber: number;\n\t\t\t/** Azure DevOps project. Required there, ignored elsewhere. */\n\t\t\tproject?: string;\n\t\t\t/** The {@link IssueBatchResult.etag} of the copy the caller holds; see {@link IssueBatchResult}. */\n\t\t\tetag?: string;\n\t }\n\t/**\n\t * An issue tracker's issue, by its own identifier (e.g. `ABC-123`) within a resource. Jira (Cloud and Data\n\t * Center) and Linear. For Jira Data Center `resourceId` is the host — the instance's single resource, as\n\t * `listOrgs` reports it — and `resourceUrl` is not needed, since the browser link is built from the\n\t * connection's own base URL; Jira Cloud requires it.\n\t */\n\t| {\n\t\t\tkey: string;\n\t\t\tresourceId: string;\n\t\t\tresourceUrl?: string;\n\t\t\tidentifier: string;\n\t\t\t/**\n\t\t\t * The {@link IssueBatchResult.etag} of the copy the caller holds. Checked cheaply on Jira Cloud and Linear;\n\t\t\t * every other tracker accepts it but reads the issue in full.\n\t\t\t */\n\t\t\tetag?: string;\n\t };\n\ntype CoordinateTarget = Extract<IssueBatchTarget, { owner: string }>;\ntype TrackerTarget = Extract<IssueBatchTarget, { resourceId: string }>;\n\n/**\n * The answer for one {@link IssueBatchTarget}, in one of four states:\n * - `{ key, issue, etag }` — read in full. Every fully read row carries an `etag`, whether or not the caller sent\n * one, so a caller can seed its etags from the reads it already makes.\n * - `{ key, unchanged: true, etag }` — the cheap check proved the caller's copy current, and nothing else was\n * fetched. Only for a target that sent an `etag`, on a host with a cheap check.\n * - `{ key }` — PROVEN ABSENT.\n * - no row, with `fetchFailed` and a warning — the read could not check. Never treat that as absent.\n */\nexport interface IssueBatchResult {\n\tkey: string;\n\t/**\n\t * The resolved issue, or `undefined` when it PROVABLY does not exist (or is not visible to this connection) —\n\t * or when it is `unchanged`.\n\t *\n\t * Absent is an answer here, unlike every paged read on this facade: a target that FAILED — its own request\n\t * outright, or just its own alias within an otherwise-answering GitHub chunk (e.g. an org enforcing SAML SSO\n\t * the token isn't authorized for) — is not returned at all and sets `fetchFailed`, so a caller can tell\n\t * \"proven absent\" from \"unknown\" and cache the first without ever caching the second.\n\t */\n\tissue?: IssueShape;\n\t/**\n\t * Opaque: compare for equality only, never parse. Computed by core from the issue's change state — its state and\n\t * update time, plus each input the call's `etagIncludes` listed — and NOT the provider's HTTP ETag. Send it back\n\t * as the target's `etag`. An etag from another scheme or another `etagIncludes` set simply compares unequal and\n\t * costs a full read, never a false `unchanged`. A host can add a reaction without moving the update time, so\n\t * unless `'reactions'` is listed, an `unchanged` copy's `thumbsUpCount` may be stale.\n\t */\n\tetag?: string;\n\t/** The caller's copy is current (its `etag` matched); `issue` is absent because nothing was read. */\n\tunchanged?: true;\n}\n\nconst surface = 'Batch issue resolution';\n\nfunction refused(warning: ProviderWarning): ProviderResult<IssueBatchResult> {\n\treturn { items: [], warnings: [warning], fetchFailed: true };\n}\n\nexport async function getIssuesBatch(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\ttargets: readonly IssueBatchTarget[];\n\t\tconnectionId?: string;\n\t\t/**\n\t\t * Explicit self-managed host domain. Used only when the requested connection has no configured domain;\n\t\t * it must come from the trusted authentication configuration, not repository or remote data.\n\t\t */\n\t\tdomain?: string;\n\t\t/**\n\t\t * Widens every etag to the listed inputs, each of which a host changes without moving the issue's update\n\t\t * time, and each of which costs its own fields in the cheap check. `'reactions'` is the thumbs-up count; it\n\t\t * widens nothing on a host whose rows carry no real count (Azure DevOps work items and the trackers). Order\n\t\t * and repeats don't matter. An unknown value refuses the whole call. Changes only which etag is computed; the\n\t\t * full read is the same. Calls that differ in this set never match each other's etags.\n\t\t */\n\t\tetagIncludes?: readonly IssueEtagInclude[];\n\t},\n): Promise<ProviderResult<IssueBatchResult>> {\n\t// Nothing was asked for, so nothing is missing: an empty success, not a refusal.\n\tif (options.targets.length === 0) return { items: [], warnings: [] };\n\n\tconst duplicateKey = findDuplicateKey(options.targets);\n\tif (duplicateKey != null) {\n\t\t// Refuses the whole call rather than deduping: `key` exists so the caller can match results without\n\t\t// positional bookkeeping, and two results under one key make that ambiguous for EVERY target, not just\n\t\t// the repeated one.\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`Duplicate issue batch target key '${duplicateKey}'; keys identify results, so each must be unique.`,\n\t\t\t),\n\t\t);\n\t}\n\n\t// Same as a bad target: refused whole before any request, naming the value and what's allowed.\n\tconst invalidInclude = findInvalidIssueEtagInclude(options.etagIncludes ?? []);\n\tif (invalidInclude != null) {\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`Unknown issue etag include '${invalidInclude}'; expected one of ${issueEtagIncludes.map(i => `'${i}'`).join(', ')}.`,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst etagIncludes = normalizeIssueEtagIncludes(options.etagIncludes ?? []);\n\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn getIssuesBatchForTracker(\n\t\t\tctx,\n\t\t\toptions.providerId,\n\t\t\toptions.targets,\n\t\t\toptions.connectionId,\n\t\t\toptions.domain,\n\t\t\tetagIncludes,\n\t\t);\n\t}\n\n\tconst targets = options.targets;\n\tif (!targets.every(isCoordinateTarget)) {\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`${surface} for '${options.providerId}' takes repository coordinates { key, owner, repo, number, project? }, not tracker identifiers.`,\n\t\t\t),\n\t\t);\n\t}\n\n\t// The validated value must be the one sent to the provider, so every string field is trimmed once here.\n\tconst trimmedTargets = targets.map(trimCoordinateFields);\n\n\t// Deterministic caller bugs, refused like a duplicate key rather than sent upstream, where a blank owner or repo\n\t// would come back as a missing repository — a proven absence the caller would cache.\n\tconst invalid = findInvalidCoordinateTarget(options.providerId, trimmedTargets);\n\tif (invalid != null) {\n\t\treturn refused(otherWarning(options.providerId, undefined, options.connectionId, invalid));\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\treturn unresolvedIntegration(ctx, options.providerId, options.connectionId, options.domain);\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refused(gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, surface));\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\n\tif (!integration.supportsIssues) {\n\t\treturn refused(issuesUnsupportedWarning(options.providerId, domain, options.connectionId));\n\t}\n\n\tconst toCoordinate = (t: CoordinateTarget) => ({\n\t\towner: t.owner,\n\t\trepo: t.repo,\n\t\tnumber: t.number,\n\t\tproject: t.project,\n\t});\n\tconst result = await readEtaggedBatch({\n\t\tproviderId: options.providerId,\n\t\tdomain: domain,\n\t\tconnectionId: options.connectionId,\n\t\ttargets: trimmedTargets,\n\t\tsupportsEtags: integration.supportsIssueEtags,\n\t\treadFull: batch =>\n\t\t\trunCaptured(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t() => integration.getIssuesBatchResult(batch.map(toCoordinate), undefined, options.connectionId),\n\t\t\t\t// A missing session must surface as a connection warning, including on the primary path, which\n\t\t\t\t// otherwise reads it as \"not connected\" and says nothing — and this read would then call it\n\t\t\t\t// unsupported.\n\t\t\t\t{ warnOnMissingSession: true },\n\t\t\t),\n\t\treadEtagFields: batch =>\n\t\t\trunCaptured(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t() =>\n\t\t\t\t\tintegration.getIssuesEtagFieldsResult(\n\t\t\t\t\t\tbatch.map(toCoordinate),\n\t\t\t\t\t\t{ etagIncludes: etagIncludes },\n\t\t\t\t\t\tundefined,\n\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t),\n\t\t\t\t{ warnOnMissingSession: true },\n\t\t\t),\n\t\titemEtag: issue => issueEtag(issueEtagFieldsFromShape(issue), etagIncludes),\n\t\tfieldsEtag: fields => issueEtag(fields, etagIncludes),\n\t\t// A provider that doesn't implement the batch hook answers `undefined` with no error. Either way its targets\n\t\t// are DROPPED rather than reported as absent — the difference between \"unknown\" and \"proven absent\" is the\n\t\t// read's whole value, and a failure must never be cached as an answer.\n\t\tunsupportedWarning: () =>\n\t\t\tunsupportedWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`${surface} is not supported by '${options.providerId}'; resolve issues individually instead.`,\n\t\t\t),\n\t});\n\n\treturn toIssueBatchResult(result);\n}\n\n/**\n * The tracker form. ONE integration call per invocation when no target carries an `etag`, whatever the target count:\n * the integration makes one single-issue request per target, with bounded concurrency, and settles each independently\n * — see `IssuesIntegration.getIssuesByResourceIdBatchResult`. With etags, a tracker with a cheap check (Jira Cloud's\n * bulk fetch, Linear's per-team query) takes the coordinate form's `readEtaggedBatch` flow; every other tracker\n * ignores a target's `etag` and reads and etags every found row in full.\n *\n * `resourceId` is trusted and the read does no resource discovery. It is handed to the provider's by-resource-id\n * read as-is, never wrapped into a synthesized descriptor: Linear and Trello answer `undefined` for a descriptor\n * that fails `isIssueResourceDescriptor`, which would be published as a proven absence.\n *\n * `domain` selects the host of a self-managed tracker (Jira Data Center), as it does for\n * `listIssueTrackerIssuesPage`, and is ignored for the cloud trackers, which have a single canonical host. Unlike\n * the paged reads, this one never falls back to the primary connection for a self-managed tracker: it requires a\n * `domain` or a `connectionId` with a configured host and refuses otherwise. Two self-hosted instances routinely\n * issue the same project and issue keys, and `issue: undefined` is a proven absence a caller may cache — an answer\n * from whichever host happens to be primary would be cached under a key that names a different instance (#5872).\n */\nasync function getIssuesBatchForTracker(\n\tctx: ProviderReadContext,\n\tproviderId: IssuesHostIntegrationIds,\n\ttargets: readonly IssueBatchTarget[],\n\tconnectionId: string | undefined,\n\tdomain: string | undefined,\n\tetagIncludes: readonly IssueEtagInclude[],\n): Promise<ProviderResult<IssueBatchResult>> {\n\tif (!targets.every(isTrackerTarget)) {\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tundefined,\n\t\t\t\tconnectionId,\n\t\t\t\t`${surface} for '${providerId}' takes tracker identifiers { key, resourceId, resourceUrl?, identifier }, not repository coordinates.`,\n\t\t\t),\n\t\t);\n\t}\n\n\t// The validated value must be the one sent to the provider, so every string field is trimmed once here.\n\tconst trimmedTargets = targets.map(trimTrackerTarget);\n\n\t// Deterministic caller bugs, refused like a duplicate key rather than sent upstream to fail one by one.\n\tconst invalid = findInvalidTrackerTarget(providerId, trimmedTargets);\n\tif (invalid != null) {\n\t\treturn refused(otherWarning(providerId, undefined, connectionId, invalid));\n\t}\n\n\t// A `domain` only selects a host when it parses to one, and a `connectionId` only when it names a configured\n\t// connection that has one; anything else would resolve the primary host instead, which is the fallback this\n\t// read refuses.\n\tif (\n\t\tisIssuesSelfManagedHostIntegrationId(providerId) &&\n\t\t(domain != null\n\t\t\t? hostFromDomain(domain) == null\n\t\t\t: connectionId == null ||\n\t\t\t\t!ctx.getConfigured(providerId).some(c => c.id === connectionId && hostFromDomain(c.domain) != null))\n\t) {\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tundefined,\n\t\t\t\tconnectionId,\n\t\t\t\t`${surface} requires a domain or a configured connection id for '${providerId}': every configured host can hold a different issue under the same key, so the read does not answer from the primary connection.`,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(providerId, connectionId, domain);\n\tif (integration == null) return unresolvedIntegration(ctx, providerId, connectionId, domain);\n\tif (!isIssuesIntegration(integration)) {\n\t\treturn refused(issueTrackerOnlySurfaceWarning(providerId, connectionId, surface));\n\t}\n\n\tconst unsupported = (): ProviderWarning =>\n\t\tunsupportedWarning(\n\t\t\tproviderId,\n\t\t\tundefined,\n\t\t\tconnectionId,\n\t\t\t`${surface} is not supported by '${providerId}'; its single-issue read cannot prove an absence, so a miss would not be safe to cache.`,\n\t\t);\n\tif (!integration.supportsIssueLookupByResourceId) return refused(unsupported());\n\n\t// A self-managed tracker's single resource is its host, and a caller keys (and caches) each answer by\n\t// `resourceId` while the read is addressed to the host `domain`/`connectionId` resolved. When they disagree,\n\t// host B's answer — including a cacheable absence — would be stored under host A's key. Refused whole, like\n\t// the caller bugs above: a target naming another host means the call selected a host its caller did not mean.\n\tif (isIssuesSelfManagedHostIntegrationId(providerId)) {\n\t\tconst mismatched = trimmedTargets.find(t => !areDomainsOnSameHost(t.resourceId, integration.domain));\n\t\tif (mismatched != null) {\n\t\t\treturn refused(\n\t\t\t\totherWarning(\n\t\t\t\t\tproviderId,\n\t\t\t\t\tundefined,\n\t\t\t\t\tconnectionId,\n\t\t\t\t\t`Issue batch target '${mismatched.key}' requires the resource id of '${providerId}' to name the host the read resolved to.`,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t}\n\n\tconst resolvedDomain = ctx.domainForRead(integration, providerId, connectionId, domain);\n\n\tconst toResourceTarget = (t: TrackerTarget) => ({\n\t\tresourceId: t.resourceId,\n\t\tidentifier: t.identifier,\n\t\tresourceUrl: t.resourceUrl,\n\t});\n\tconst result = await readEtaggedBatch({\n\t\tproviderId: providerId,\n\t\tdomain: resolvedDomain,\n\t\tconnectionId: connectionId,\n\t\ttargets: trimmedTargets,\n\t\tsupportsEtags: integration.supportsIssueEtagsByResourceId,\n\t\treadFull: batch =>\n\t\t\trunCaptured(\n\t\t\t\tproviderId,\n\t\t\t\tresolvedDomain,\n\t\t\t\tconnectionId,\n\t\t\t\t() => integration.getIssuesByResourceIdBatchResult(batch.map(toResourceTarget), connectionId),\n\t\t\t\t// The read core answers `undefined` when it cannot resolve a session. That must surface as a connection\n\t\t\t\t// warning, including on the primary path, rather than as nothing — or worse, be read as absence.\n\t\t\t\t{ warnOnMissingSession: true },\n\t\t\t),\n\t\treadEtagFields: batch =>\n\t\t\trunCaptured(\n\t\t\t\tproviderId,\n\t\t\t\tresolvedDomain,\n\t\t\t\tconnectionId,\n\t\t\t\t() =>\n\t\t\t\t\tintegration.getIssuesEtagFieldsByResourceIdBatchResult(\n\t\t\t\t\t\tbatch.map(toResourceTarget),\n\t\t\t\t\t\t{ etagIncludes: etagIncludes },\n\t\t\t\t\t\tconnectionId,\n\t\t\t\t\t),\n\t\t\t\t{ warnOnMissingSession: true },\n\t\t\t),\n\t\titemEtag: issue => issueEtag(issueEtagFieldsFromShape(issue), etagIncludes),\n\t\tfieldsEtag: fields => issueEtag(fields, etagIncludes),\n\t\tunsupportedWarning: unsupported,\n\t});\n\n\treturn toIssueBatchResult(result);\n}\n\n/** The etag flow's rows as this read's results, under the `issue` name the result type gives the item. */\nfunction toIssueBatchResult(result: ProviderResult<EtaggedBatchRow<IssueShape>>): ProviderResult<IssueBatchResult> {\n\treturn {\n\t\titems: result.items.map(row => ({\n\t\t\tkey: row.key,\n\t\t\t...(row.value != null ? { issue: row.value } : {}),\n\t\t\t...(row.unchanged ? { unchanged: true as const } : {}),\n\t\t\t...(row.etag != null ? { etag: row.etag } : {}),\n\t\t})),\n\t\twarnings: result.warnings,\n\t\tfetchFailed: result.fetchFailed,\n\t};\n}\n\nfunction isCoordinateTarget(target: IssueBatchTarget): target is CoordinateTarget {\n\treturn 'owner' in target;\n}\n\nfunction isTrackerTarget(target: IssueBatchTarget): target is TrackerTarget {\n\treturn 'resourceId' in target;\n}\n\n/**\n * Trims `owner`, `repo` and `project` once, shared by every coordinate-form batch read (this one, the pull request\n * batch and the pull-requests-by-branch read): leading/trailing whitespace can't change which repository a\n * coordinate names, so it's stripped and accepted rather than refused, matching #5821's scope-name precedent.\n */\nexport function trimCoordinateFields<T extends { owner: string; repo: string; project?: string }>(target: T): T {\n\treturn { ...target, owner: target.owner.trim(), repo: target.repo.trim(), project: target.project?.trim() };\n}\n\n/** Trims `resourceId`, `resourceUrl` and `identifier` once; the validated value must be the one sent to the provider. */\nfunction trimTrackerTarget(target: TrackerTarget): TrackerTarget {\n\treturn {\n\t\t...target,\n\t\tresourceId: target.resourceId.trim(),\n\t\tresourceUrl: target.resourceUrl?.trim() || undefined,\n\t\tidentifier: target.identifier.trim(),\n\t};\n}\n\nfunction findInvalidCoordinateTarget(\n\tproviderId: IntegrationIds,\n\ttargets: readonly CoordinateTarget[],\n): string | undefined {\n\tconst isAzure = isAzureProviderId(providerId);\n\tconst isGitHub = isGitHubProviderId(providerId);\n\tfor (const target of targets) {\n\t\tif (!Number.isSafeInteger(target.number) || target.number <= 0) {\n\t\t\treturn `Issue batch target '${target.key}' has number ${target.number}; expected a positive integer.`;\n\t\t}\n\t\tif (isGitHub && target.number > githubGraphQLInt32Max) {\n\t\t\treturn `Issue batch target '${target.key}' has number ${target.number}; GitHub numbers are 32-bit and cannot exceed ${githubGraphQLInt32Max}.`;\n\t\t}\n\t\tif (target.owner.length === 0) {\n\t\t\treturn `Issue batch target '${target.key}' requires a non-empty owner.`;\n\t\t}\n\t\tif (isAzure) {\n\t\t\tif (!target.project) {\n\t\t\t\treturn `Issue batch target '${target.key}' requires a project for '${providerId}'.`;\n\t\t\t}\n\t\t} else if (target.repo.length === 0) {\n\t\t\treturn `Issue batch target '${target.key}' requires a non-empty repo.`;\n\t\t}\n\t}\n\treturn undefined;\n}\n\nfunction findInvalidTrackerTarget(\n\tproviderId: IssuesHostIntegrationIds,\n\ttargets: readonly TrackerTarget[],\n): string | undefined {\n\tfor (const target of targets) {\n\t\tif (target.resourceId.length === 0) {\n\t\t\treturn `Issue batch target '${target.key}' requires a resource id.`;\n\t\t}\n\t\tif (target.identifier.length === 0) {\n\t\t\treturn `Issue batch target '${target.key}' requires an issue identifier.`;\n\t\t}\n\t\tif (providerId === IssuesCloudHostIntegrationId.Jira && !target.resourceUrl) {\n\t\t\treturn `Issue batch target '${target.key}' requires the Jira resource URL so the result contains a browser link without resource discovery.`;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * The answer when no integration resolves for a batch read. Targets were asked about, so this is never a silent\n * empty success: a supplied connection or domain that no longer resolves gets its own warning, and the untargeted\n * primary path (e.g. a self-managed provider with no configured host) a connection warning, as `getCurrentAccount`\n * gives — either way `fetchFailed`, so no caller mistakes the dropped targets for an answer.\n */\nexport function unresolvedIntegration<T>(\n\tctx: ProviderReadContext,\n\tproviderId: IntegrationIds,\n\tconnectionId: string | undefined,\n\tdomain: string | undefined,\n): ProviderResult<T> {\n\tconst early = ctx.earlyReturnConnectionWarnings(providerId, connectionId, domain);\n\tif (early.warnings.length > 0) return { items: [], warnings: early.warnings, fetchFailed: true };\n\n\tconst resolvedDomain = ctx.resolveDomainForRead(providerId, connectionId, domain);\n\treturn { items: [], warnings: [noConnectionWarning(providerId, resolvedDomain, connectionId)], fetchFailed: true };\n}\n\nexport function findDuplicateKey(targets: readonly { key: string }[]): string | undefined {\n\tconst seen = new Set<string>();\n\tfor (const target of targets) {\n\t\tif (seen.has(target.key)) return target.key;\n\n\t\tseen.add(target.key);\n\t}\n\treturn undefined;\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { IssueShape, IssueSorting, IssueStateFilter } from "../../../git/models/issue.js";
|
|
2
2
|
import { IntegrationIds } from "../constants.js";
|
|
3
|
-
import { IssueFilter } from "../providerFilters.js";
|
|
4
3
|
import { ProviderPagedResult } from "../results.js";
|
|
4
|
+
import { IssueFilter } from "../providerFilters.js";
|
|
5
5
|
import "../providers/models.js";
|
|
6
6
|
import { ProviderReadContext } from "./context.js";
|
|
7
7
|
//#region ../plus/integrations/src/reads/issueTracker.d.ts
|
|
@@ -7,7 +7,7 @@ import { mergeCollectionMetadata, parsePageCursor } from "../providers/utils/pro
|
|
|
7
7
|
import { isIssuesHostIntegrationId } from "../utils/integration.utils.js";
|
|
8
8
|
import { providersMetadata } from "../providers/models.js";
|
|
9
9
|
import { parseIssueTrackerPageCursor, toIssueTrackerPageCursor } from "./cursors.js";
|
|
10
|
-
import { incompleteReadWarning, issueTrackerOnlySurfaceWarning, otherWarning, unmergeableIssueSortWarning, unsupportedIssueSortWarning } from "./warnings.js";
|
|
10
|
+
import { incompleteReadWarning, issueTrackerOnlySurfaceWarning, otherWarning, unmergeableIssueSortWarning, unsupportedIssueSortWarning, unsupportedWarning } from "./warnings.js";
|
|
11
11
|
import { runCaptured } from "./drains.js";
|
|
12
12
|
import { isIssuesIntegration } from "../models/issuesIntegration.js";
|
|
13
13
|
import { projectKey, resourceIdForProject, resourceLabel, resourceMatchesOrg } from "./hierarchy.utils.js";
|
|
@@ -81,7 +81,7 @@ async function listIssueTrackerIssuesPage(ctx, options) {
|
|
|
81
81
|
}
|
|
82
82
|
const sort = resolvedSort.sort;
|
|
83
83
|
if (options.state != null && (!issueStateFilters.includes(options.state) || options.state !== "open" && !providersMetadata[options.providerId]?.supportsIssueStates)) {
|
|
84
|
-
warnings.push(
|
|
84
|
+
warnings.push(issueStateFilters.includes(options.state) ? unsupportedWarning(options.providerId, domain, options.connectionId, `Issue state '${options.state}' is not supported by '${options.providerId}'.`) : otherWarning(options.providerId, domain, options.connectionId, `Unknown issue state; expected one of ${issueStateFilters.join(", ")}.`));
|
|
85
85
|
return emptyPage(true);
|
|
86
86
|
}
|
|
87
87
|
const cursorState = compositeCursor != null ? compositeCursor.state ?? "open" : options.cursor != null ? "open" : void 0;
|
|
@@ -112,12 +112,12 @@ async function listIssueTrackerIssuesPage(ctx, options) {
|
|
|
112
112
|
if (options.filters?.length) {
|
|
113
113
|
const supported = providersMetadata[options.providerId]?.supportedIssueFilters;
|
|
114
114
|
if (!(supported != null && options.filters.every((f) => supported.includes(f)))) {
|
|
115
|
-
warnings.push(
|
|
115
|
+
warnings.push(unsupportedWarning(options.providerId, domain, options.connectionId, `One or more requested issue filters are not supported by '${options.providerId}'.`));
|
|
116
116
|
return emptyPage(true);
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
if (options.includeAllAssignees === true && options.filters?.some((f) => f !== "assignee")) {
|
|
120
|
-
warnings.push(
|
|
120
|
+
warnings.push(unsupportedWarning(options.providerId, domain, options.connectionId, `\`includeAllAssignees\` cannot be combined with an author/mention filter for '${options.providerId}' (those filters require a user scope).`));
|
|
121
121
|
return emptyPage(true);
|
|
122
122
|
}
|
|
123
123
|
let usersByResourceId;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"issueTracker.js","names":[],"sources":["../../../../../plus/integrations/src/reads/issueTracker.ts"],"sourcesContent":["import type { CollectionMetadata } from '@gitkraken/provider-apis';\nimport type { IssueShape, IssueSorting, IssueStateFilter } from '@gitlens/git/models/issue.js';\nimport type { ResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport { mapBounded } from '@gitlens/utils/promise.js';\nimport { assessCollectionMetadata, mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { providerFanOutConcurrency } from '../constants.js';\nimport { isIssuesIntegration } from '../models/issuesIntegration.js';\nimport { IssueFilter, providersMetadata } from '../providers/models.js';\nimport { mergeCollectionMetadata, parsePageCursor } from '../providers/utils/providerPaging.js';\nimport type { ProviderPagedResult, ProviderWarning } from '../results.js';\nimport { appendDedupedWarning, reconcileOmissionsWithFailure } from '../results.js';\nimport { isIssuesHostIntegrationId } from '../utils/integration.utils.js';\nimport type { ProviderReadContext } from './context.js';\nimport { parseIssueTrackerPageCursor, toIssueTrackerPageCursor } from './cursors.js';\nimport { runCaptured } from './drains.js';\nimport { projectKey, resourceIdForProject, resourceLabel, resourceMatchesOrg } from './hierarchy.utils.js';\nimport { resolveIssueSort, toIssueOrdering } from './ordering.js';\nimport {\n\tincompleteReadWarning,\n\tissueTrackerOnlySurfaceWarning,\n\totherWarning,\n\tunmergeableIssueSortWarning,\n\tunsupportedIssueSortWarning,\n} from './warnings.js';\n\nconst issueStateFilters: readonly IssueStateFilter[] = ['open', 'closed', 'all'];\n\nexport async function listIssueTrackerIssuesPage(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\torg?: string;\n\t\tproject?: string;\n\t\tfilters?: IssueFilter[];\n\t\t/** Broadens the read to every assignee. Scopes to user-assigned issues when omitted. */\n\t\tincludeAllAssignees?: boolean;\n\t\t/**\n\t\t * How to order the issues, as `field:direction`. Omitted orders most-recently-updated-first where the\n\t\t * tracker can express it.\n\t\t *\n\t\t * Validated against `getSupportedFilters().issues`' sibling `issueSorts` — a tracker reports there, not\n\t\t * under the account-wide table, because resource -> project IS its only issue surface. A key it can't\n\t\t * express refuses the read rather than serving a differently-ordered list.\n\t\t *\n\t\t * One page can span SEVERAL projects, whose issues are merged here, so a key no normalized issue carries\n\t\t * (`priority`, `dueDate`, `resolved`) is refused for a multi-project page even though the tracker orders by\n\t\t * it perfectly well within one project.\n\t\t *\n\t\t * Unlike the git-host reads this one is safe to change mid-pagination: its cursor windows PROJECTS and\n\t\t * drains each to exhaustion, so which projects a round covers doesn't depend on how their issues are\n\t\t * ordered. Only the order within a page changes.\n\t\t */\n\t\tsort?: IssueSorting;\n\t\t/**\n\t\t * Which issue states to read. Omitted reads open issues only.\n\t\t *\n\t\t * Applied server-side by every project read, so it narrows each project's drain rather than the merged page.\n\t\t * Only a tracker declaring `ProviderMetadata.supportsIssueStates` can express anything but `'open'`; asking\n\t\t * another for `'closed'`/`'all'` refuses the read rather than serving its open issues.\n\t\t */\n\t\tstate?: IssueStateFilter;\n\t\tforceSync?: boolean;\n\t\tpage?: number;\n\t\tcursor?: string;\n\t\titemsPerPage?: number;\n\t\tconnectionId?: string;\n\t\t/**\n\t\t * Explicit self-managed tracker host. Used to select the instance when the requested connection has no\n\t\t * configured domain; it must come from the trusted authentication configuration, never from repository\n\t\t * or remote data. Ignored for the cloud trackers.\n\t\t */\n\t\tdomain?: string;\n\t},\n): Promise<ProviderPagedResult<IssueShape>> {\n\t// Pagination is opt-in: only window the projects when the caller actually asked to page. A caller that\n\t// passes none of page/cursor/itemsPerPage keeps the \"aggregate every matched project\" contract, so an\n\t// existing consumer that doesn't inspect `hasMore` never silently loses projects past a default window.\n\tconst compositeCursor = parseIssueTrackerPageCursor(options.cursor);\n\tconst paginated =\n\t\tcompositeCursor?.unpaged !== true &&\n\t\t(options.page != null || options.cursor != null || options.itemsPerPage != null);\n\t// A composite cursor can carry retries from earlier project windows alongside the next untouched window.\n\t// `currentPage` remains the caller-facing position; `nextPage` is the window this round should advance.\n\tconst page = Math.max(\n\t\t1,\n\t\tMath.trunc(compositeCursor?.currentPage ?? parsePageCursor(options.cursor) ?? options.page ?? 1),\n\t);\n\tconst projectsPerPage = Math.max(1, Math.trunc(options.itemsPerPage ?? 20));\n\n\tconst items: IssueShape[] = [];\n\tconst warnings: ProviderWarning[] = [];\n\tconst emptyPage = (\n\t\tfetchFailed?: boolean,\n\t\ttruncated?: boolean,\n\t\tretry?: {\n\t\t\tnextPage?: number;\n\t\t\tpages?: readonly number[];\n\t\t\tprojects?: readonly string[];\n\t\t\tcompletedProjects?: readonly string[];\n\t\t},\n\t): ProviderPagedResult<IssueShape> => {\n\t\tconst cursor =\n\t\t\tretry != null\n\t\t\t\t? toIssueTrackerPageCursor({\n\t\t\t\t\t\tcurrentPage: page + 1,\n\t\t\t\t\t\tunpaged: !paginated,\n\t\t\t\t\t\tnextPage: retry.nextPage,\n\t\t\t\t\t\tretryPages: retry.pages ?? [],\n\t\t\t\t\t\tretryProjects: retry.projects ?? [],\n\t\t\t\t\t\tcompletedProjects: retry.completedProjects ?? [],\n\t\t\t\t\t\tstate: options.state,\n\t\t\t\t\t})\n\t\t\t\t: undefined;\n\t\treturn {\n\t\t\titems: items,\n\t\t\twarnings: warnings,\n\t\t\tpage: { currentPage: page, itemsPerPage: items.length, truncated: truncated || undefined },\n\t\t\t// Retry-only state is deliberately manual: advertising it as forward progress would make a\n\t\t\t// persistent provider failure spin forever in a normal `while (hasMore)` consumer. An untouched\n\t\t\t// project window remains normal forward progress even when this window failed completely.\n\t\t\thasMore: retry?.nextPage != null && cursor != null,\n\t\t\tcursor: cursor,\n\t\t\tfetchFailed: fetchFailed || undefined,\n\t\t};\n\t};\n\tconst normalWindowPage = paginated\n\t\t? (compositeCursor?.nextPage ?? (compositeCursor == null ? page : undefined))\n\t\t: undefined;\n\tconst priorRetryPages = compositeCursor?.retryPages ?? [];\n\tconst priorRetryProjects = compositeCursor?.retryProjects ?? [];\n\tconst priorCompletedProjects = compositeCursor?.completedProjects ?? [];\n\tconst trackCompletedProjects =\n\t\tpriorCompletedProjects.length > 0 || priorRetryPages.length > 0 || compositeCursor?.completedProjects != null;\n\tconst retryPagesForDiscoveryFailure = (): number[] => [\n\t\t...priorRetryPages,\n\t\t...(normalWindowPage != null ? [normalWindowPage] : compositeCursor == null ? [1] : []),\n\t];\n\tconst discoveryFailureRetry = (): {\n\t\tpages: readonly number[];\n\t\tprojects: readonly string[];\n\t\tcompletedProjects: readonly string[];\n\t} => ({\n\t\tpages: retryPagesForDiscoveryFailure(),\n\t\tprojects: priorRetryProjects,\n\t\tcompletedProjects: priorCompletedProjects,\n\t});\n\n\tif (!isIssuesHostIntegrationId(options.providerId)) {\n\t\twarnings.push(\n\t\t\tissueTrackerOnlySurfaceWarning(options.providerId, options.connectionId, 'Issue-tracker project reads'),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\t// A supplied connectionId or domain that no longer resolves is a broken target, not an empty account.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\twarnings.push(...early.warnings);\n\t\treturn emptyPage(early.fetchFailed);\n\t}\n\tif (!isIssuesIntegration(integration)) {\n\t\twarnings.push(\n\t\t\tissueTrackerOnlySurfaceWarning(options.providerId, options.connectionId, 'Issue-tracker project reads'),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\n\t// Before any upstream request: the discovery fan-outs below (resources, projects, per-resource accounts) are\n\t// three round trips, and an order this tracker can't express refuses the read whatever they return. Placed after\n\t// `domain` only because every warning carries it.\n\tconst resolvedSort = resolveIssueSort(providersMetadata[options.providerId]?.supportedIssueSorts, options.sort);\n\tif (resolvedSort.rejection != null) {\n\t\twarnings.push(\n\t\t\tunsupportedIssueSortWarning(options.providerId, domain, options.connectionId, resolvedSort.rejection),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\tconst sort = resolvedSort.sort;\n\n\t// Refused before any request for the same reason as the sort: a state the tracker can't express would otherwise\n\t// be dropped by its project reads, and their open issues published as the requested state. A value outside the\n\t// vocabulary is refused too, since `toProviderIssueStates` would read it as omitted, which means open.\n\tif (\n\t\toptions.state != null &&\n\t\t(!issueStateFilters.includes(options.state) ||\n\t\t\t(options.state !== 'open' && !providersMetadata[options.providerId]?.supportsIssueStates))\n\t) {\n\t\twarnings.push(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\tissueStateFilters.includes(options.state)\n\t\t\t\t\t? `Issue state '${options.state}' is not supported by '${options.providerId}'.`\n\t\t\t\t\t: `Unknown issue state; expected one of ${issueStateFilters.join(', ')}.`,\n\t\t\t),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\t// A cursor resumed under another state than it was minted for skips what it already covered, whose other-state\n\t// issues were never read. A narrowed read only mints composite cursors, which record their state, so any other\n\t// cursor came from an open read.\n\tconst cursorState =\n\t\tcompositeCursor != null ? (compositeCursor.state ?? 'open') : options.cursor != null ? 'open' : undefined;\n\tif (cursorState != null && cursorState !== (options.state ?? 'open')) {\n\t\twarnings.push(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t'The cursor belongs to a read of another issue state; start again without it.',\n\t\t\t),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\tawait ctx.forceRefreshIfRequested(integration, options.forceSync, options.connectionId);\n\n\tconst { value: resources, warning: resourcesWarning } = await runCaptured(\n\t\toptions.providerId,\n\t\tdomain,\n\t\toptions.connectionId,\n\t\t() => integration.getResourcesForUserResult(options.connectionId),\n\t);\n\tif (resourcesWarning != null) {\n\t\twarnings.push(resourcesWarning);\n\t}\n\tif (resources == null || resources.length === 0) {\n\t\tconst fetchFailed = resourcesWarning != null && resources == null;\n\t\treturn emptyPage(fetchFailed, false, fetchFailed ? discoveryFailureRetry() : undefined);\n\t}\n\n\tconst scopedResources =\n\t\toptions.org != null ? resources.filter(r => resourceMatchesOrg(r, options.org!)) : resources;\n\tif (scopedResources.length === 0) {\n\t\treturn emptyPage();\n\t}\n\n\tconst { value: projectsResult, warning: projectsWarning } = await runCaptured(\n\t\toptions.providerId,\n\t\tdomain,\n\t\toptions.connectionId,\n\t\t() => integration.getProjectsForResourcesWithMetadataResult(scopedResources, options.connectionId),\n\t);\n\tif (projectsWarning != null) {\n\t\twarnings.push(projectsWarning);\n\t}\n\t// Partial project discovery: continue with the resources that succeeded, but surface per-resource\n\t// failures as warnings and remember to mark the page fetchFailed so the caller knows some issues may be\n\t// missing. `projectDiscoveryFailed`/`projectDiscoveryTruncated` are OR-ed into the page's\n\t// fetchFailed/truncated at every return below (a truncated-but-not-failed discovery, e.g. a paging\n\t// backstop, still means the project set is incomplete).\n\tconst projectDiscoveryAssessment = mergeAssessmentInto(\n\t\twarnings,\n\t\toptions.providerId,\n\t\tdomain,\n\t\toptions.connectionId,\n\t\tprojectsResult?.metadata,\n\t);\n\tconst projectDiscoveryFailed = projectDiscoveryAssessment.fetchFailed;\n\tconst projectDiscoveryTruncated = projectDiscoveryAssessment.truncated;\n\tconst projects = projectsResult?.values;\n\tif (projects == null || projects.length === 0) {\n\t\tconst fetchFailed = (projectsWarning != null && projectsResult == null) || projectDiscoveryFailed;\n\t\treturn emptyPage(\n\t\t\tfetchFailed,\n\t\t\tprojectDiscoveryTruncated,\n\t\t\tfetchFailed || projectDiscoveryTruncated ? discoveryFailureRetry() : undefined,\n\t\t);\n\t}\n\n\tconst matchedProjects =\n\t\toptions.project != null ? projects.filter(p => resourceMatchesOrg(p, options.project!)) : projects;\n\n\t// Validate the requested filters against what this provider supports (e.g. Linear/Trello support only\n\t// Assignee). An unsupported filter must not silently degrade — Linear/Trello ignore the requested type\n\t// and apply Assignee regardless — so warn + fetchFailed instead of returning a differently-scoped set.\n\tif (options.filters?.length) {\n\t\tconst supported = providersMetadata[options.providerId]?.supportedIssueFilters;\n\t\tconst allSupported = supported != null && options.filters.every(f => supported.includes(f));\n\t\tif (!allSupported) {\n\t\t\twarnings.push(\n\t\t\t\totherWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t`One or more requested issue filters are not supported by '${options.providerId}'.`,\n\t\t\t\t),\n\t\t\t);\n\t\t\treturn emptyPage(true);\n\t\t}\n\t}\n\n\t// `includeAllAssignees` drops the user scope, but a user-relative filter (Author/Mention) is meaningless\n\t// without a user. Passing both to the provider degrades silently: Jira, seeing no user, falls through to\n\t// an unscoped project fetch and returns EVERY issue instead of the requested author's/mentions. Reject\n\t// the incompatible combination up front rather than publishing a differently-scoped set as the result.\n\tif (options.includeAllAssignees === true && options.filters?.some(f => f !== IssueFilter.Assignee)) {\n\t\twarnings.push(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`\\`includeAllAssignees\\` cannot be combined with an author/mention filter for '${options.providerId}' (those filters require a user scope).`,\n\t\t\t),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\t// Scope to the current user's assigned issues unless the caller broadens to all assignees. Resolve the\n\t// handle from each resource's own account (multi-account safe), capturing any error so its kind\n\t// (e.g. auth) is preserved rather than collapsed to a generic warning.\n\tlet usersByResourceId: Map<string, string> | undefined;\n\t// The same accounts keyed by the same resource, holding the provider's stable id rather than the handle. A\n\t// separate map rather than a widened value because the two are resolved and consumed independently: a\n\t// provider may carry an account with no id, and `scopedProjectsWithUsers` gates on the HANDLE alone, so\n\t// folding them together would drop a resource whose id happened to be missing.\n\tlet userIdsByResourceId: Map<string, string> | undefined;\n\tlet accountLookupFailed = false;\n\tif (options.includeAllAssignees !== true) {\n\t\tusersByResourceId = new Map<string, string>();\n\t\tuserIdsByResourceId = new Map<string, string>();\n\t\tconst accounts = await mapBounded(scopedResources, providerFanOutConcurrency, async resource => ({\n\t\t\tresource: resource,\n\t\t\t...(await runCaptured(options.providerId, domain, options.connectionId, () =>\n\t\t\t\tintegration.getAccountForResourceResult(resource, options.connectionId),\n\t\t\t)),\n\t\t}));\n\n\t\tfor (const { resource, value: account, warning: accountWarning } of accounts) {\n\t\t\tconst user = account?.username ?? account?.name ?? undefined;\n\t\t\tif (user != null) {\n\t\t\t\tconst resourceKey = resourceIdForProject(resource) ?? resource.key;\n\t\t\t\tusersByResourceId.set(resourceKey, user);\n\t\t\t\t// Empty-checked, not just null-checked: an account shape declares `id` as a plain string, and a\n\t\t\t\t// provider with nothing to put there sets `''` rather than omitting it — which would otherwise\n\t\t\t\t// reach a user-field query as an empty identity and match nothing.\n\t\t\t\tif (account?.id != null && account.id !== '') {\n\t\t\t\t\tuserIdsByResourceId.set(resourceKey, account.id);\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Deduped: a captured `accountWarning` carries only the provider error, with no resource in its\n\t\t\t// message, so one expired token across N resources would otherwise repeat verbatim N times. The\n\t\t\t// synthesized fallback names the resource, so those stay distinct on their own.\n\t\t\tappendDedupedWarning(\n\t\t\t\twarnings,\n\t\t\t\taccountWarning ??\n\t\t\t\t\totherWarning(\n\t\t\t\t\t\toptions.providerId,\n\t\t\t\t\t\tdomain,\n\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t\t`Could not resolve the current user for '${resourceLabel(resource)}'; skipping that resource to avoid returning issues assigned to others.`,\n\t\t\t\t\t),\n\t\t\t);\n\t\t\taccountLookupFailed = true;\n\t\t}\n\t}\n\n\tconst fallbackUserForUnscopedProject =\n\t\tscopedResources.length === 1 && usersByResourceId?.size === 1\n\t\t\t? usersByResourceId.values().next().value\n\t\t\t: undefined;\n\tconst userForProject = (project: ResourceDescriptor): string | undefined => {\n\t\tconst resourceId = resourceIdForProject(project);\n\t\tif (resourceId != null) {\n\t\t\tconst user = usersByResourceId?.get(resourceId);\n\t\t\tif (user != null) {\n\t\t\t\treturn user;\n\t\t\t}\n\t\t}\n\n\t\t// Some providers/tests return project descriptors without their parent resource id. When we have only\n\t\t// one scoped resource, re-use that sole resolved user rather than silently dropping every project.\n\t\treturn fallbackUserForUnscopedProject;\n\t};\n\t// Deliberately NOT falling back the way `userForProject` does. That fallback exists so a project with no\n\t// parent resource id still gets scoped rather than dropped, and the handle is what the drop is judged on;\n\t// the id is optional everywhere it is read, so a miss degrades to the handle instead of widening the read.\n\tconst userIdForProject = (project: ResourceDescriptor): string | undefined => {\n\t\tconst resourceId = resourceIdForProject(project);\n\t\treturn resourceId != null ? userIdsByResourceId?.get(resourceId) : undefined;\n\t};\n\n\tconst retryProjectKeys = new Set<string>();\n\tconst completedProjectKeys = new Set(priorCompletedProjects);\n\tfor (const retryProject of priorRetryProjects) {\n\t\tcompletedProjectKeys.delete(retryProject);\n\t}\n\tconst scopedProjectsWithUsers =\n\t\tusersByResourceId != null\n\t\t\t? matchedProjects.filter(project => {\n\t\t\t\t\tif (userForProject(project) != null) return true;\n\n\t\t\t\t\tconst key = projectKey(project);\n\t\t\t\t\tretryProjectKeys.add(key);\n\t\t\t\t\tcompletedProjectKeys.delete(key);\n\t\t\t\t\treturn false;\n\t\t\t\t})\n\t\t\t: matchedProjects;\n\n\t// Select the untouched window plus any earlier windows/projects that explicitly need retrying. Keys\n\t// include the parent resource so two trackers can reuse the same project id without collapsing.\n\tconst projectsByRetryKey = new Map(scopedProjectsWithUsers.map(project => [projectKey(project), project]));\n\tconst scopedProjectsByKey = new Map<string, ResourceDescriptor>();\n\tconst addScopedProject = (project: ResourceDescriptor): void => {\n\t\tconst key = projectKey(project);\n\t\tif (completedProjectKeys.has(key)) return;\n\n\t\tscopedProjectsByKey.set(key, project);\n\t};\n\tfor (const retryProject of priorRetryProjects) {\n\t\tconst project = projectsByRetryKey.get(retryProject);\n\t\tif (project != null) {\n\t\t\taddScopedProject(project);\n\t\t} else if (projectDiscoveryFailed || projectDiscoveryTruncated) {\n\t\t\tretryProjectKeys.add(retryProject);\n\t\t}\n\t}\n\tconst windowPages = new Set<number>(priorRetryPages);\n\tif (normalWindowPage != null) {\n\t\twindowPages.add(normalWindowPage);\n\t}\n\tif (!paginated && (projectDiscoveryFailed || projectDiscoveryTruncated)) {\n\t\twindowPages.add(1);\n\t}\n\tif (paginated) {\n\t\tfor (const windowPage of windowPages) {\n\t\t\tconst windowStart = (windowPage - 1) * projectsPerPage;\n\t\t\tfor (const project of scopedProjectsWithUsers.slice(windowStart, windowStart + projectsPerPage)) {\n\t\t\t\taddScopedProject(project);\n\t\t\t}\n\t\t}\n\t} else if (compositeCursor == null || priorRetryPages.length > 0) {\n\t\t// An unpaged retry-only cursor should re-read only the explicitly failed projects already added\n\t\t// above. Discovery retries still rescan the aggregate set so newly recovered projects are included.\n\t\tfor (const project of scopedProjectsWithUsers) {\n\t\t\taddScopedProject(project);\n\t\t}\n\t}\n\tconst scopedProjects = [...scopedProjectsByKey.values()];\n\tconst furthestWindowPage = windowPages.size > 0 ? Math.max(...windowPages) : normalWindowPage;\n\tconst normalWindowEnd =\n\t\tfurthestWindowPage != null ? furthestWindowPage * projectsPerPage : scopedProjectsWithUsers.length;\n\tconst nextPage =\n\t\tpaginated && furthestWindowPage != null && scopedProjectsWithUsers.length > normalWindowEnd\n\t\t\t? furthestWindowPage + 1\n\t\t\t: undefined;\n\tconst retryWindowPages = (): number[] => {\n\t\tif (!projectDiscoveryFailed && !projectDiscoveryTruncated && retryProjectKeys.size === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\tconst pages = new Set(windowPages);\n\t\tif (pages.size === 0) {\n\t\t\t// A terminal retry has no untouched next window to identify its origin. Keep a manual window\n\t\t\t// marker so a later partial discovery can reconcile against the projects already emitted.\n\t\t\tpages.add(paginated ? Math.max(1, page - 1) : 1);\n\t\t}\n\t\treturn [...pages];\n\t};\n\tif (scopedProjects.length === 0) {\n\t\t// The discovered projects didn't intersect the requested filter/window, or every matching resource\n\t\t// failed user resolution. If discovery or account lookup was partial, the empty result is not a\n\t\t// proven-empty account — carry `fetchFailed` so the caller knows issues may be missing.\n\t\treturn emptyPage(projectDiscoveryFailed || accountLookupFailed, projectDiscoveryTruncated, {\n\t\t\tnextPage: nextPage,\n\t\t\tpages: retryWindowPages(),\n\t\t\tprojects: [...retryProjectKeys],\n\t\t\tcompletedProjects: [...completedProjectKeys],\n\t\t});\n\t}\n\n\t// A read spanning several projects is a merge, so it can only honor a key a normalized issue carries. Bound here\n\t// rather than next to the key's validation above, because the project count is only known now — while still\n\t// before any project read, so a refusal costs no request. Refused rather than served as concatenated\n\t// per-project runs, which would look ordered without being so.\n\t//\n\t// Counted over every project the READ covers, not the `scopedProjects` window this page happens to hold: the\n\t// window is `itemsPerPage` projects wide, so counting it would refuse a 20-project page and then serve the\n\t// final 1-project page of the same paginated call — the same key answered two ways, with the earlier pages'\n\t// issues missing from a read that reported success.\n\t//\n\t// Projects are not the only thing a tracker read merges: a user-scoped read asking for several relationships\n\t// issues one provider query PER relationship and unions them within each project (Jira runs one JQL drain per\n\t// filter and folds them into one map, in filter order), so a single-project page of two filters is just as\n\t// concatenated as a two-project one. Counting only projects published those runs under the requested key.\n\t// Not gated on the provider: the two trackers that don't fan out (Linear, Trello) declare `Assignee` as their\n\t// only supported filter, so a multi-filter read is already refused above and can never reach this.\n\tconst mergesFilters = options.includeAllAssignees !== true && (options.filters?.length ?? 0) > 1;\n\tconst ordering = toIssueOrdering(sort, scopedProjectsWithUsers.length > 1 || mergesFilters);\n\tif (ordering.unmergeable != null) {\n\t\twarnings.push(\n\t\t\tunmergeableIssueSortWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\tordering.unmergeable,\n\t\t\t\tscopedProjectsWithUsers.length > 1 ? 'projects' : 'filters',\n\t\t\t),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\tconst perProject = await mapBounded(scopedProjects, providerFanOutConcurrency, async project => ({\n\t\tproject: project,\n\t\t...(await runCaptured(options.providerId, domain, options.connectionId, () =>\n\t\t\tintegration.getIssuesForProjectWithTruncationResult(\n\t\t\t\tproject,\n\t\t\t\t{\n\t\t\t\t\tuser: userForProject(project),\n\t\t\t\t\tuserId: userIdForProject(project),\n\t\t\t\t\tfilters: options.filters,\n\t\t\t\t\tsort: sort,\n\t\t\t\t\tstate: options.state,\n\t\t\t\t},\n\t\t\t\toptions.connectionId,\n\t\t\t),\n\t\t)),\n\t}));\n\n\t// Partial project discovery means some projects' issues are missing from this page; propagate it so the\n\t// page reports fetchFailed even when every discovered project's own read succeeded.\n\tlet fetchFailed = projectDiscoveryFailed || accountLookupFailed;\n\tlet truncationRecovery: 'narrow-scope' | 'none' | undefined = projectDiscoveryTruncated ? 'none' : undefined;\n\tlet drainMetadata: CollectionMetadata | undefined;\n\tfor (const { project, value: result, warning } of perProject) {\n\t\tconst key = projectKey(project);\n\t\tconst retryProject = result == null;\n\t\tif (warning != null) {\n\t\t\t// Deduped: the per-project warning is built from the provider error alone and names no project, so a\n\t\t\t// single failing token repeats verbatim once per project in the window.\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t\tfetchFailed = true;\n\t\t\ttruncationRecovery = 'none';\n\t\t}\n\t\t// A thrown/unsupported read (e.g. Linear not-implemented) surfaces as a warning with no value;\n\t\t// mark the aggregate as fetchFailed so an empty result isn't mistaken for \"no issues\".\n\t\tif (result == null) {\n\t\t\tfetchFailed = true;\n\t\t}\n\t\tif (result != null) {\n\t\t\titems.push(...result.values);\n\t\t\tif (result.truncated) {\n\t\t\t\tconst recovery = result.recovery;\n\t\t\t\ttruncationRecovery = truncationRecovery == null || truncationRecovery === recovery ? recovery : 'none';\n\t\t\t}\n\t\t\tif (result.metadata != null) {\n\t\t\t\tdrainMetadata = mergeCollectionMetadata(drainMetadata, result.metadata);\n\t\t\t}\n\t\t}\n\t\tif (retryProject) {\n\t\t\tretryProjectKeys.add(key);\n\t\t\tcompletedProjectKeys.delete(key);\n\t\t} else {\n\t\t\t// A usable partial/truncated project is emitted once and remains structurally incomplete.\n\t\t\t// Re-running the same project cursor would normally return the same prefix and duplicate it\n\t\t\t// across facade pages; only a project that returned no value is safe to retry automatically.\n\t\t\tcompletedProjectKeys.add(key);\n\t\t}\n\t}\n\n\t// Each provider query arrived ordered by the tracker; the union of them — across projects, and across the\n\t// per-relationship queries a multi-filter read fans out into — is not, and the union is what this read\n\t// publishes. A no-op when nothing merged, since the tracker already ordered that single run.\n\tconst orderedItems = ordering.order(items);\n\n\tconst projectReadReportedTruncation = truncationRecovery != null;\n\tconst drainAssessment = assessCollectionMetadata(options.providerId, domain, options.connectionId, drainMetadata);\n\tfor (const warning of drainAssessment.warnings) {\n\t\t// A project result already supplies the recovery semantics. Replace metadata's unstructured fallback with\n\t\t// the structured warning below, while retaining specific failures and omissions.\n\t\tif (projectReadReportedTruncation && !drainAssessment.fetchFailed && warning.omission == null) continue;\n\n\t\tappendDedupedWarning(warnings, warning);\n\t}\n\tfetchFailed = fetchFailed || drainAssessment.fetchFailed;\n\tif (drainAssessment.truncated) {\n\t\ttruncationRecovery = 'none';\n\t}\n\n\tconst projectTruncated = truncationRecovery != null;\n\tconst scopeTooLarge = !fetchFailed && truncationRecovery === 'narrow-scope';\n\tif (scopeTooLarge || (!fetchFailed && projectTruncated && !warnings.some(warning => warning.omission != null))) {\n\t\tappendDedupedWarning(\n\t\t\twarnings,\n\t\t\tincompleteReadWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\tscopeTooLarge\n\t\t\t\t\t? 'Some projects hold more issues than one read can return; narrow the scope to read the rest.'\n\t\t\t\t\t: 'Some issues were omitted; the provider returned an incomplete result.',\n\t\t\t\tscopeTooLarge ? 'scope-too-large' : 'exhausted',\n\t\t\t),\n\t\t);\n\t}\n\t// A metadata omission from an earlier project asserts the read succeeded; a later one may since have failed.\n\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\n\tconst retryPages = retryWindowPages();\n\tconst cursor = toIssueTrackerPageCursor({\n\t\tcurrentPage: page + 1,\n\t\tunpaged: !paginated,\n\t\tnextPage: nextPage,\n\t\tretryPages: retryPages,\n\t\tretryProjects: [...retryProjectKeys],\n\t\tcompletedProjects:\n\t\t\t(trackCompletedProjects ||\n\t\t\t\tprojectDiscoveryFailed ||\n\t\t\t\tprojectDiscoveryTruncated ||\n\t\t\t\tretryProjectKeys.size > 0) &&\n\t\t\t(retryPages.length > 0 || nextPage != null)\n\t\t\t\t? [...completedProjectKeys]\n\t\t\t\t: [],\n\t\tstate: options.state,\n\t});\n\treturn {\n\t\titems: orderedItems,\n\t\twarnings: warnings,\n\t\tpage: { currentPage: page, itemsPerPage: orderedItems.length, truncated: projectTruncated || undefined },\n\t\t// Failed-project retries alone are manual. Only an untouched project window is automatic progress.\n\t\thasMore: nextPage != null && cursor != null,\n\t\tcursor: cursor,\n\t\tfetchFailed: fetchFailed || undefined,\n\t};\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"issueTracker.js","names":[],"sources":["../../../../../plus/integrations/src/reads/issueTracker.ts"],"sourcesContent":["import type { CollectionMetadata } from '@gitkraken/provider-apis';\nimport type { IssueShape, IssueSorting, IssueStateFilter } from '@gitlens/git/models/issue.js';\nimport type { ResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport { mapBounded } from '@gitlens/utils/promise.js';\nimport { assessCollectionMetadata, mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { providerFanOutConcurrency } from '../constants.js';\nimport { isIssuesIntegration } from '../models/issuesIntegration.js';\nimport { IssueFilter, providersMetadata } from '../providers/models.js';\nimport { mergeCollectionMetadata, parsePageCursor } from '../providers/utils/providerPaging.js';\nimport type { ProviderPagedResult, ProviderWarning } from '../results.js';\nimport { appendDedupedWarning, reconcileOmissionsWithFailure } from '../results.js';\nimport { isIssuesHostIntegrationId } from '../utils/integration.utils.js';\nimport type { ProviderReadContext } from './context.js';\nimport { parseIssueTrackerPageCursor, toIssueTrackerPageCursor } from './cursors.js';\nimport { runCaptured } from './drains.js';\nimport { projectKey, resourceIdForProject, resourceLabel, resourceMatchesOrg } from './hierarchy.utils.js';\nimport { resolveIssueSort, toIssueOrdering } from './ordering.js';\nimport {\n\tincompleteReadWarning,\n\tissueTrackerOnlySurfaceWarning,\n\totherWarning,\n\tunmergeableIssueSortWarning,\n\tunsupportedIssueSortWarning,\n\tunsupportedWarning,\n} from './warnings.js';\n\nconst issueStateFilters: readonly IssueStateFilter[] = ['open', 'closed', 'all'];\n\nexport async function listIssueTrackerIssuesPage(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\torg?: string;\n\t\tproject?: string;\n\t\tfilters?: IssueFilter[];\n\t\t/** Broadens the read to every assignee. Scopes to user-assigned issues when omitted. */\n\t\tincludeAllAssignees?: boolean;\n\t\t/**\n\t\t * How to order the issues, as `field:direction`. Omitted orders most-recently-updated-first where the\n\t\t * tracker can express it.\n\t\t *\n\t\t * Validated against `getSupportedFilters().issues`' sibling `issueSorts` — a tracker reports there, not\n\t\t * under the account-wide table, because resource -> project IS its only issue surface. A key it can't\n\t\t * express refuses the read rather than serving a differently-ordered list.\n\t\t *\n\t\t * One page can span SEVERAL projects, whose issues are merged here, so a key no normalized issue carries\n\t\t * (`priority`, `dueDate`, `resolved`) is refused for a multi-project page even though the tracker orders by\n\t\t * it perfectly well within one project.\n\t\t *\n\t\t * Unlike the git-host reads this one is safe to change mid-pagination: its cursor windows PROJECTS and\n\t\t * drains each to exhaustion, so which projects a round covers doesn't depend on how their issues are\n\t\t * ordered. Only the order within a page changes.\n\t\t */\n\t\tsort?: IssueSorting;\n\t\t/**\n\t\t * Which issue states to read. Omitted reads open issues only.\n\t\t *\n\t\t * Applied server-side by every project read, so it narrows each project's drain rather than the merged page.\n\t\t * Only a tracker declaring `ProviderMetadata.supportsIssueStates` can express anything but `'open'`; asking\n\t\t * another for `'closed'`/`'all'` refuses the read rather than serving its open issues.\n\t\t */\n\t\tstate?: IssueStateFilter;\n\t\tforceSync?: boolean;\n\t\tpage?: number;\n\t\tcursor?: string;\n\t\titemsPerPage?: number;\n\t\tconnectionId?: string;\n\t\t/**\n\t\t * Explicit self-managed tracker host. Used to select the instance when the requested connection has no\n\t\t * configured domain; it must come from the trusted authentication configuration, never from repository\n\t\t * or remote data. Ignored for the cloud trackers.\n\t\t */\n\t\tdomain?: string;\n\t},\n): Promise<ProviderPagedResult<IssueShape>> {\n\t// Pagination is opt-in: only window the projects when the caller actually asked to page. A caller that\n\t// passes none of page/cursor/itemsPerPage keeps the \"aggregate every matched project\" contract, so an\n\t// existing consumer that doesn't inspect `hasMore` never silently loses projects past a default window.\n\tconst compositeCursor = parseIssueTrackerPageCursor(options.cursor);\n\tconst paginated =\n\t\tcompositeCursor?.unpaged !== true &&\n\t\t(options.page != null || options.cursor != null || options.itemsPerPage != null);\n\t// A composite cursor can carry retries from earlier project windows alongside the next untouched window.\n\t// `currentPage` remains the caller-facing position; `nextPage` is the window this round should advance.\n\tconst page = Math.max(\n\t\t1,\n\t\tMath.trunc(compositeCursor?.currentPage ?? parsePageCursor(options.cursor) ?? options.page ?? 1),\n\t);\n\tconst projectsPerPage = Math.max(1, Math.trunc(options.itemsPerPage ?? 20));\n\n\tconst items: IssueShape[] = [];\n\tconst warnings: ProviderWarning[] = [];\n\tconst emptyPage = (\n\t\tfetchFailed?: boolean,\n\t\ttruncated?: boolean,\n\t\tretry?: {\n\t\t\tnextPage?: number;\n\t\t\tpages?: readonly number[];\n\t\t\tprojects?: readonly string[];\n\t\t\tcompletedProjects?: readonly string[];\n\t\t},\n\t): ProviderPagedResult<IssueShape> => {\n\t\tconst cursor =\n\t\t\tretry != null\n\t\t\t\t? toIssueTrackerPageCursor({\n\t\t\t\t\t\tcurrentPage: page + 1,\n\t\t\t\t\t\tunpaged: !paginated,\n\t\t\t\t\t\tnextPage: retry.nextPage,\n\t\t\t\t\t\tretryPages: retry.pages ?? [],\n\t\t\t\t\t\tretryProjects: retry.projects ?? [],\n\t\t\t\t\t\tcompletedProjects: retry.completedProjects ?? [],\n\t\t\t\t\t\tstate: options.state,\n\t\t\t\t\t})\n\t\t\t\t: undefined;\n\t\treturn {\n\t\t\titems: items,\n\t\t\twarnings: warnings,\n\t\t\tpage: { currentPage: page, itemsPerPage: items.length, truncated: truncated || undefined },\n\t\t\t// Retry-only state is deliberately manual: advertising it as forward progress would make a\n\t\t\t// persistent provider failure spin forever in a normal `while (hasMore)` consumer. An untouched\n\t\t\t// project window remains normal forward progress even when this window failed completely.\n\t\t\thasMore: retry?.nextPage != null && cursor != null,\n\t\t\tcursor: cursor,\n\t\t\tfetchFailed: fetchFailed || undefined,\n\t\t};\n\t};\n\tconst normalWindowPage = paginated\n\t\t? (compositeCursor?.nextPage ?? (compositeCursor == null ? page : undefined))\n\t\t: undefined;\n\tconst priorRetryPages = compositeCursor?.retryPages ?? [];\n\tconst priorRetryProjects = compositeCursor?.retryProjects ?? [];\n\tconst priorCompletedProjects = compositeCursor?.completedProjects ?? [];\n\tconst trackCompletedProjects =\n\t\tpriorCompletedProjects.length > 0 || priorRetryPages.length > 0 || compositeCursor?.completedProjects != null;\n\tconst retryPagesForDiscoveryFailure = (): number[] => [\n\t\t...priorRetryPages,\n\t\t...(normalWindowPage != null ? [normalWindowPage] : compositeCursor == null ? [1] : []),\n\t];\n\tconst discoveryFailureRetry = (): {\n\t\tpages: readonly number[];\n\t\tprojects: readonly string[];\n\t\tcompletedProjects: readonly string[];\n\t} => ({\n\t\tpages: retryPagesForDiscoveryFailure(),\n\t\tprojects: priorRetryProjects,\n\t\tcompletedProjects: priorCompletedProjects,\n\t});\n\n\tif (!isIssuesHostIntegrationId(options.providerId)) {\n\t\twarnings.push(\n\t\t\tissueTrackerOnlySurfaceWarning(options.providerId, options.connectionId, 'Issue-tracker project reads'),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\t// A supplied connectionId or domain that no longer resolves is a broken target, not an empty account.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\twarnings.push(...early.warnings);\n\t\treturn emptyPage(early.fetchFailed);\n\t}\n\tif (!isIssuesIntegration(integration)) {\n\t\twarnings.push(\n\t\t\tissueTrackerOnlySurfaceWarning(options.providerId, options.connectionId, 'Issue-tracker project reads'),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\n\t// Before any upstream request: the discovery fan-outs below (resources, projects, per-resource accounts) are\n\t// three round trips, and an order this tracker can't express refuses the read whatever they return. Placed after\n\t// `domain` only because every warning carries it.\n\tconst resolvedSort = resolveIssueSort(providersMetadata[options.providerId]?.supportedIssueSorts, options.sort);\n\tif (resolvedSort.rejection != null) {\n\t\twarnings.push(\n\t\t\tunsupportedIssueSortWarning(options.providerId, domain, options.connectionId, resolvedSort.rejection),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\tconst sort = resolvedSort.sort;\n\n\t// Refused before any request for the same reason as the sort: a state the tracker can't express would otherwise\n\t// be dropped by its project reads, and their open issues published as the requested state. A value outside the\n\t// vocabulary is refused too, since `toProviderIssueStates` would read it as omitted, which means open.\n\tif (\n\t\toptions.state != null &&\n\t\t(!issueStateFilters.includes(options.state) ||\n\t\t\t(options.state !== 'open' && !providersMetadata[options.providerId]?.supportsIssueStates))\n\t) {\n\t\t// A known state this tracker can't express is an unsupported capability; a value outside the vocabulary is\n\t\t// caller input the caller fixes in the request.\n\t\twarnings.push(\n\t\t\tissueStateFilters.includes(options.state)\n\t\t\t\t? unsupportedWarning(\n\t\t\t\t\t\toptions.providerId,\n\t\t\t\t\t\tdomain,\n\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t\t`Issue state '${options.state}' is not supported by '${options.providerId}'.`,\n\t\t\t\t\t)\n\t\t\t\t: otherWarning(\n\t\t\t\t\t\toptions.providerId,\n\t\t\t\t\t\tdomain,\n\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t\t`Unknown issue state; expected one of ${issueStateFilters.join(', ')}.`,\n\t\t\t\t\t),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\t// A cursor resumed under another state than it was minted for skips what it already covered, whose other-state\n\t// issues were never read. A narrowed read only mints composite cursors, which record their state, so any other\n\t// cursor came from an open read.\n\tconst cursorState =\n\t\tcompositeCursor != null ? (compositeCursor.state ?? 'open') : options.cursor != null ? 'open' : undefined;\n\tif (cursorState != null && cursorState !== (options.state ?? 'open')) {\n\t\twarnings.push(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t'The cursor belongs to a read of another issue state; start again without it.',\n\t\t\t),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\tawait ctx.forceRefreshIfRequested(integration, options.forceSync, options.connectionId);\n\n\tconst { value: resources, warning: resourcesWarning } = await runCaptured(\n\t\toptions.providerId,\n\t\tdomain,\n\t\toptions.connectionId,\n\t\t() => integration.getResourcesForUserResult(options.connectionId),\n\t);\n\tif (resourcesWarning != null) {\n\t\twarnings.push(resourcesWarning);\n\t}\n\tif (resources == null || resources.length === 0) {\n\t\tconst fetchFailed = resourcesWarning != null && resources == null;\n\t\treturn emptyPage(fetchFailed, false, fetchFailed ? discoveryFailureRetry() : undefined);\n\t}\n\n\tconst scopedResources =\n\t\toptions.org != null ? resources.filter(r => resourceMatchesOrg(r, options.org!)) : resources;\n\tif (scopedResources.length === 0) {\n\t\treturn emptyPage();\n\t}\n\n\tconst { value: projectsResult, warning: projectsWarning } = await runCaptured(\n\t\toptions.providerId,\n\t\tdomain,\n\t\toptions.connectionId,\n\t\t() => integration.getProjectsForResourcesWithMetadataResult(scopedResources, options.connectionId),\n\t);\n\tif (projectsWarning != null) {\n\t\twarnings.push(projectsWarning);\n\t}\n\t// Partial project discovery: continue with the resources that succeeded, but surface per-resource\n\t// failures as warnings and remember to mark the page fetchFailed so the caller knows some issues may be\n\t// missing. `projectDiscoveryFailed`/`projectDiscoveryTruncated` are OR-ed into the page's\n\t// fetchFailed/truncated at every return below (a truncated-but-not-failed discovery, e.g. a paging\n\t// backstop, still means the project set is incomplete).\n\tconst projectDiscoveryAssessment = mergeAssessmentInto(\n\t\twarnings,\n\t\toptions.providerId,\n\t\tdomain,\n\t\toptions.connectionId,\n\t\tprojectsResult?.metadata,\n\t);\n\tconst projectDiscoveryFailed = projectDiscoveryAssessment.fetchFailed;\n\tconst projectDiscoveryTruncated = projectDiscoveryAssessment.truncated;\n\tconst projects = projectsResult?.values;\n\tif (projects == null || projects.length === 0) {\n\t\tconst fetchFailed = (projectsWarning != null && projectsResult == null) || projectDiscoveryFailed;\n\t\treturn emptyPage(\n\t\t\tfetchFailed,\n\t\t\tprojectDiscoveryTruncated,\n\t\t\tfetchFailed || projectDiscoveryTruncated ? discoveryFailureRetry() : undefined,\n\t\t);\n\t}\n\n\tconst matchedProjects =\n\t\toptions.project != null ? projects.filter(p => resourceMatchesOrg(p, options.project!)) : projects;\n\n\t// Validate the requested filters against what this provider supports (e.g. Linear/Trello support only\n\t// Assignee). An unsupported filter must not silently degrade — Linear/Trello ignore the requested type\n\t// and apply Assignee regardless — so warn + fetchFailed instead of returning a differently-scoped set.\n\tif (options.filters?.length) {\n\t\tconst supported = providersMetadata[options.providerId]?.supportedIssueFilters;\n\t\tconst allSupported = supported != null && options.filters.every(f => supported.includes(f));\n\t\tif (!allSupported) {\n\t\t\twarnings.push(\n\t\t\t\tunsupportedWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t`One or more requested issue filters are not supported by '${options.providerId}'.`,\n\t\t\t\t),\n\t\t\t);\n\t\t\treturn emptyPage(true);\n\t\t}\n\t}\n\n\t// `includeAllAssignees` drops the user scope, but a user-relative filter (Author/Mention) is meaningless\n\t// without a user. Passing both to the provider degrades silently: Jira, seeing no user, falls through to\n\t// an unscoped project fetch and returns EVERY issue instead of the requested author's/mentions. Reject\n\t// the incompatible combination up front rather than publishing a differently-scoped set as the result.\n\tif (options.includeAllAssignees === true && options.filters?.some(f => f !== IssueFilter.Assignee)) {\n\t\twarnings.push(\n\t\t\tunsupportedWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`\\`includeAllAssignees\\` cannot be combined with an author/mention filter for '${options.providerId}' (those filters require a user scope).`,\n\t\t\t),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\t// Scope to the current user's assigned issues unless the caller broadens to all assignees. Resolve the\n\t// handle from each resource's own account (multi-account safe), capturing any error so its kind\n\t// (e.g. auth) is preserved rather than collapsed to a generic warning.\n\tlet usersByResourceId: Map<string, string> | undefined;\n\t// The same accounts keyed by the same resource, holding the provider's stable id rather than the handle. A\n\t// separate map rather than a widened value because the two are resolved and consumed independently: a\n\t// provider may carry an account with no id, and `scopedProjectsWithUsers` gates on the HANDLE alone, so\n\t// folding them together would drop a resource whose id happened to be missing.\n\tlet userIdsByResourceId: Map<string, string> | undefined;\n\tlet accountLookupFailed = false;\n\tif (options.includeAllAssignees !== true) {\n\t\tusersByResourceId = new Map<string, string>();\n\t\tuserIdsByResourceId = new Map<string, string>();\n\t\tconst accounts = await mapBounded(scopedResources, providerFanOutConcurrency, async resource => ({\n\t\t\tresource: resource,\n\t\t\t...(await runCaptured(options.providerId, domain, options.connectionId, () =>\n\t\t\t\tintegration.getAccountForResourceResult(resource, options.connectionId),\n\t\t\t)),\n\t\t}));\n\n\t\tfor (const { resource, value: account, warning: accountWarning } of accounts) {\n\t\t\tconst user = account?.username ?? account?.name ?? undefined;\n\t\t\tif (user != null) {\n\t\t\t\tconst resourceKey = resourceIdForProject(resource) ?? resource.key;\n\t\t\t\tusersByResourceId.set(resourceKey, user);\n\t\t\t\t// Empty-checked, not just null-checked: an account shape declares `id` as a plain string, and a\n\t\t\t\t// provider with nothing to put there sets `''` rather than omitting it — which would otherwise\n\t\t\t\t// reach a user-field query as an empty identity and match nothing.\n\t\t\t\tif (account?.id != null && account.id !== '') {\n\t\t\t\t\tuserIdsByResourceId.set(resourceKey, account.id);\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Deduped: a captured `accountWarning` carries only the provider error, with no resource in its\n\t\t\t// message, so one expired token across N resources would otherwise repeat verbatim N times. The\n\t\t\t// synthesized fallback names the resource, so those stay distinct on their own.\n\t\t\tappendDedupedWarning(\n\t\t\t\twarnings,\n\t\t\t\taccountWarning ??\n\t\t\t\t\totherWarning(\n\t\t\t\t\t\toptions.providerId,\n\t\t\t\t\t\tdomain,\n\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t\t`Could not resolve the current user for '${resourceLabel(resource)}'; skipping that resource to avoid returning issues assigned to others.`,\n\t\t\t\t\t),\n\t\t\t);\n\t\t\taccountLookupFailed = true;\n\t\t}\n\t}\n\n\tconst fallbackUserForUnscopedProject =\n\t\tscopedResources.length === 1 && usersByResourceId?.size === 1\n\t\t\t? usersByResourceId.values().next().value\n\t\t\t: undefined;\n\tconst userForProject = (project: ResourceDescriptor): string | undefined => {\n\t\tconst resourceId = resourceIdForProject(project);\n\t\tif (resourceId != null) {\n\t\t\tconst user = usersByResourceId?.get(resourceId);\n\t\t\tif (user != null) {\n\t\t\t\treturn user;\n\t\t\t}\n\t\t}\n\n\t\t// Some providers/tests return project descriptors without their parent resource id. When we have only\n\t\t// one scoped resource, re-use that sole resolved user rather than silently dropping every project.\n\t\treturn fallbackUserForUnscopedProject;\n\t};\n\t// Deliberately NOT falling back the way `userForProject` does. That fallback exists so a project with no\n\t// parent resource id still gets scoped rather than dropped, and the handle is what the drop is judged on;\n\t// the id is optional everywhere it is read, so a miss degrades to the handle instead of widening the read.\n\tconst userIdForProject = (project: ResourceDescriptor): string | undefined => {\n\t\tconst resourceId = resourceIdForProject(project);\n\t\treturn resourceId != null ? userIdsByResourceId?.get(resourceId) : undefined;\n\t};\n\n\tconst retryProjectKeys = new Set<string>();\n\tconst completedProjectKeys = new Set(priorCompletedProjects);\n\tfor (const retryProject of priorRetryProjects) {\n\t\tcompletedProjectKeys.delete(retryProject);\n\t}\n\tconst scopedProjectsWithUsers =\n\t\tusersByResourceId != null\n\t\t\t? matchedProjects.filter(project => {\n\t\t\t\t\tif (userForProject(project) != null) return true;\n\n\t\t\t\t\tconst key = projectKey(project);\n\t\t\t\t\tretryProjectKeys.add(key);\n\t\t\t\t\tcompletedProjectKeys.delete(key);\n\t\t\t\t\treturn false;\n\t\t\t\t})\n\t\t\t: matchedProjects;\n\n\t// Select the untouched window plus any earlier windows/projects that explicitly need retrying. Keys\n\t// include the parent resource so two trackers can reuse the same project id without collapsing.\n\tconst projectsByRetryKey = new Map(scopedProjectsWithUsers.map(project => [projectKey(project), project]));\n\tconst scopedProjectsByKey = new Map<string, ResourceDescriptor>();\n\tconst addScopedProject = (project: ResourceDescriptor): void => {\n\t\tconst key = projectKey(project);\n\t\tif (completedProjectKeys.has(key)) return;\n\n\t\tscopedProjectsByKey.set(key, project);\n\t};\n\tfor (const retryProject of priorRetryProjects) {\n\t\tconst project = projectsByRetryKey.get(retryProject);\n\t\tif (project != null) {\n\t\t\taddScopedProject(project);\n\t\t} else if (projectDiscoveryFailed || projectDiscoveryTruncated) {\n\t\t\tretryProjectKeys.add(retryProject);\n\t\t}\n\t}\n\tconst windowPages = new Set<number>(priorRetryPages);\n\tif (normalWindowPage != null) {\n\t\twindowPages.add(normalWindowPage);\n\t}\n\tif (!paginated && (projectDiscoveryFailed || projectDiscoveryTruncated)) {\n\t\twindowPages.add(1);\n\t}\n\tif (paginated) {\n\t\tfor (const windowPage of windowPages) {\n\t\t\tconst windowStart = (windowPage - 1) * projectsPerPage;\n\t\t\tfor (const project of scopedProjectsWithUsers.slice(windowStart, windowStart + projectsPerPage)) {\n\t\t\t\taddScopedProject(project);\n\t\t\t}\n\t\t}\n\t} else if (compositeCursor == null || priorRetryPages.length > 0) {\n\t\t// An unpaged retry-only cursor should re-read only the explicitly failed projects already added\n\t\t// above. Discovery retries still rescan the aggregate set so newly recovered projects are included.\n\t\tfor (const project of scopedProjectsWithUsers) {\n\t\t\taddScopedProject(project);\n\t\t}\n\t}\n\tconst scopedProjects = [...scopedProjectsByKey.values()];\n\tconst furthestWindowPage = windowPages.size > 0 ? Math.max(...windowPages) : normalWindowPage;\n\tconst normalWindowEnd =\n\t\tfurthestWindowPage != null ? furthestWindowPage * projectsPerPage : scopedProjectsWithUsers.length;\n\tconst nextPage =\n\t\tpaginated && furthestWindowPage != null && scopedProjectsWithUsers.length > normalWindowEnd\n\t\t\t? furthestWindowPage + 1\n\t\t\t: undefined;\n\tconst retryWindowPages = (): number[] => {\n\t\tif (!projectDiscoveryFailed && !projectDiscoveryTruncated && retryProjectKeys.size === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\tconst pages = new Set(windowPages);\n\t\tif (pages.size === 0) {\n\t\t\t// A terminal retry has no untouched next window to identify its origin. Keep a manual window\n\t\t\t// marker so a later partial discovery can reconcile against the projects already emitted.\n\t\t\tpages.add(paginated ? Math.max(1, page - 1) : 1);\n\t\t}\n\t\treturn [...pages];\n\t};\n\tif (scopedProjects.length === 0) {\n\t\t// The discovered projects didn't intersect the requested filter/window, or every matching resource\n\t\t// failed user resolution. If discovery or account lookup was partial, the empty result is not a\n\t\t// proven-empty account — carry `fetchFailed` so the caller knows issues may be missing.\n\t\treturn emptyPage(projectDiscoveryFailed || accountLookupFailed, projectDiscoveryTruncated, {\n\t\t\tnextPage: nextPage,\n\t\t\tpages: retryWindowPages(),\n\t\t\tprojects: [...retryProjectKeys],\n\t\t\tcompletedProjects: [...completedProjectKeys],\n\t\t});\n\t}\n\n\t// A read spanning several projects is a merge, so it can only honor a key a normalized issue carries. Bound here\n\t// rather than next to the key's validation above, because the project count is only known now — while still\n\t// before any project read, so a refusal costs no request. Refused rather than served as concatenated\n\t// per-project runs, which would look ordered without being so.\n\t//\n\t// Counted over every project the READ covers, not the `scopedProjects` window this page happens to hold: the\n\t// window is `itemsPerPage` projects wide, so counting it would refuse a 20-project page and then serve the\n\t// final 1-project page of the same paginated call — the same key answered two ways, with the earlier pages'\n\t// issues missing from a read that reported success.\n\t//\n\t// Projects are not the only thing a tracker read merges: a user-scoped read asking for several relationships\n\t// issues one provider query PER relationship and unions them within each project (Jira runs one JQL drain per\n\t// filter and folds them into one map, in filter order), so a single-project page of two filters is just as\n\t// concatenated as a two-project one. Counting only projects published those runs under the requested key.\n\t// Not gated on the provider: the two trackers that don't fan out (Linear, Trello) declare `Assignee` as their\n\t// only supported filter, so a multi-filter read is already refused above and can never reach this.\n\tconst mergesFilters = options.includeAllAssignees !== true && (options.filters?.length ?? 0) > 1;\n\tconst ordering = toIssueOrdering(sort, scopedProjectsWithUsers.length > 1 || mergesFilters);\n\tif (ordering.unmergeable != null) {\n\t\twarnings.push(\n\t\t\tunmergeableIssueSortWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\tordering.unmergeable,\n\t\t\t\tscopedProjectsWithUsers.length > 1 ? 'projects' : 'filters',\n\t\t\t),\n\t\t);\n\t\treturn emptyPage(true);\n\t}\n\n\tconst perProject = await mapBounded(scopedProjects, providerFanOutConcurrency, async project => ({\n\t\tproject: project,\n\t\t...(await runCaptured(options.providerId, domain, options.connectionId, () =>\n\t\t\tintegration.getIssuesForProjectWithTruncationResult(\n\t\t\t\tproject,\n\t\t\t\t{\n\t\t\t\t\tuser: userForProject(project),\n\t\t\t\t\tuserId: userIdForProject(project),\n\t\t\t\t\tfilters: options.filters,\n\t\t\t\t\tsort: sort,\n\t\t\t\t\tstate: options.state,\n\t\t\t\t},\n\t\t\t\toptions.connectionId,\n\t\t\t),\n\t\t)),\n\t}));\n\n\t// Partial project discovery means some projects' issues are missing from this page; propagate it so the\n\t// page reports fetchFailed even when every discovered project's own read succeeded.\n\tlet fetchFailed = projectDiscoveryFailed || accountLookupFailed;\n\tlet truncationRecovery: 'narrow-scope' | 'none' | undefined = projectDiscoveryTruncated ? 'none' : undefined;\n\tlet drainMetadata: CollectionMetadata | undefined;\n\tfor (const { project, value: result, warning } of perProject) {\n\t\tconst key = projectKey(project);\n\t\tconst retryProject = result == null;\n\t\tif (warning != null) {\n\t\t\t// Deduped: the per-project warning is built from the provider error alone and names no project, so a\n\t\t\t// single failing token repeats verbatim once per project in the window.\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t\tfetchFailed = true;\n\t\t\ttruncationRecovery = 'none';\n\t\t}\n\t\t// A thrown/unsupported read (e.g. Linear not-implemented) surfaces as a warning with no value;\n\t\t// mark the aggregate as fetchFailed so an empty result isn't mistaken for \"no issues\".\n\t\tif (result == null) {\n\t\t\tfetchFailed = true;\n\t\t}\n\t\tif (result != null) {\n\t\t\titems.push(...result.values);\n\t\t\tif (result.truncated) {\n\t\t\t\tconst recovery = result.recovery;\n\t\t\t\ttruncationRecovery = truncationRecovery == null || truncationRecovery === recovery ? recovery : 'none';\n\t\t\t}\n\t\t\tif (result.metadata != null) {\n\t\t\t\tdrainMetadata = mergeCollectionMetadata(drainMetadata, result.metadata);\n\t\t\t}\n\t\t}\n\t\tif (retryProject) {\n\t\t\tretryProjectKeys.add(key);\n\t\t\tcompletedProjectKeys.delete(key);\n\t\t} else {\n\t\t\t// A usable partial/truncated project is emitted once and remains structurally incomplete.\n\t\t\t// Re-running the same project cursor would normally return the same prefix and duplicate it\n\t\t\t// across facade pages; only a project that returned no value is safe to retry automatically.\n\t\t\tcompletedProjectKeys.add(key);\n\t\t}\n\t}\n\n\t// Each provider query arrived ordered by the tracker; the union of them — across projects, and across the\n\t// per-relationship queries a multi-filter read fans out into — is not, and the union is what this read\n\t// publishes. A no-op when nothing merged, since the tracker already ordered that single run.\n\tconst orderedItems = ordering.order(items);\n\n\tconst projectReadReportedTruncation = truncationRecovery != null;\n\tconst drainAssessment = assessCollectionMetadata(options.providerId, domain, options.connectionId, drainMetadata);\n\tfor (const warning of drainAssessment.warnings) {\n\t\t// A project result already supplies the recovery semantics. Replace metadata's unstructured fallback with\n\t\t// the structured warning below, while retaining specific failures and omissions.\n\t\tif (projectReadReportedTruncation && !drainAssessment.fetchFailed && warning.omission == null) continue;\n\n\t\tappendDedupedWarning(warnings, warning);\n\t}\n\tfetchFailed = fetchFailed || drainAssessment.fetchFailed;\n\tif (drainAssessment.truncated) {\n\t\ttruncationRecovery = 'none';\n\t}\n\n\tconst projectTruncated = truncationRecovery != null;\n\tconst scopeTooLarge = !fetchFailed && truncationRecovery === 'narrow-scope';\n\tif (scopeTooLarge || (!fetchFailed && projectTruncated && !warnings.some(warning => warning.omission != null))) {\n\t\tappendDedupedWarning(\n\t\t\twarnings,\n\t\t\tincompleteReadWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\tscopeTooLarge\n\t\t\t\t\t? 'Some projects hold more issues than one read can return; narrow the scope to read the rest.'\n\t\t\t\t\t: 'Some issues were omitted; the provider returned an incomplete result.',\n\t\t\t\tscopeTooLarge ? 'scope-too-large' : 'exhausted',\n\t\t\t),\n\t\t);\n\t}\n\t// A metadata omission from an earlier project asserts the read succeeded; a later one may since have failed.\n\treconcileOmissionsWithFailure(warnings, fetchFailed);\n\n\tconst retryPages = retryWindowPages();\n\tconst cursor = toIssueTrackerPageCursor({\n\t\tcurrentPage: page + 1,\n\t\tunpaged: !paginated,\n\t\tnextPage: nextPage,\n\t\tretryPages: retryPages,\n\t\tretryProjects: [...retryProjectKeys],\n\t\tcompletedProjects:\n\t\t\t(trackCompletedProjects ||\n\t\t\t\tprojectDiscoveryFailed ||\n\t\t\t\tprojectDiscoveryTruncated ||\n\t\t\t\tretryProjectKeys.size > 0) &&\n\t\t\t(retryPages.length > 0 || nextPage != null)\n\t\t\t\t? [...completedProjectKeys]\n\t\t\t\t: [],\n\t\tstate: options.state,\n\t});\n\treturn {\n\t\titems: orderedItems,\n\t\twarnings: warnings,\n\t\tpage: { currentPage: page, itemsPerPage: orderedItems.length, truncated: projectTruncated || undefined },\n\t\t// Failed-project retries alone are manual. Only an untouched project window is automatic progress.\n\t\thasMore: nextPage != null && cursor != null,\n\t\tcursor: cursor,\n\t\tfetchFailed: fetchFailed || undefined,\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { IssueShape, IssueSorting } from "../../../git/models/issue.js";
|
|
2
2
|
import { IntegrationIds } from "../constants.js";
|
|
3
|
-
import { IssueFilter } from "../providerFilters.js";
|
|
4
3
|
import { ProviderPagedResult } from "../results.js";
|
|
4
|
+
import { IssueFilter } from "../providerFilters.js";
|
|
5
5
|
import { ProviderReposInput } from "../providers/models.js";
|
|
6
6
|
import { ProviderReadContext } from "./context.js";
|
|
7
7
|
//#region ../plus/integrations/src/reads/issues.d.ts
|
|
@@ -5,7 +5,7 @@ import { mergeCollectionMetadata } from "../providers/utils/providerPaging.js";
|
|
|
5
5
|
import { isGitHostIntegration, isIssuesHostIntegrationId, warnOnMissingSessionForDomain } from "../utils/integration.utils.js";
|
|
6
6
|
import { isRepoIdsInput, providersMetadata } from "../providers/models.js";
|
|
7
7
|
import { drainFlatPagesToRequestedPage, drainToRequestedPage, isPageNumberAdvanceable, pageToCursor, refusedPage, resolveContinuation, resolveCurrentPage, toProviderPageInfo, usableCursor } from "./paging.js";
|
|
8
|
-
import { gitHostOnlySurfaceWarning, issuesUnsupportedWarning, otherWarning, truncationWarning, unmergeableIssueSortWarning, unsupportedAccountWideIssueFiltersWarning, unsupportedIssueSortWarning } from "./warnings.js";
|
|
8
|
+
import { gitHostOnlySurfaceWarning, issuesUnsupportedWarning, otherWarning, truncationWarning, unmergeableIssueSortWarning, unsupportedAccountWideIssueFiltersWarning, unsupportedIssueSortWarning, unsupportedWarning } from "./warnings.js";
|
|
9
9
|
import { runCaptured } from "./drains.js";
|
|
10
10
|
import { resolveAccountWideIssueFilters } from "./filters.js";
|
|
11
11
|
import { resolveIssueSort, toIssueOrdering } from "./ordering.js";
|
|
@@ -69,8 +69,8 @@ async function listIssuesPage(ctx, options) {
|
|
|
69
69
|
async function readAccountWideIssuesPage({ options, integration, domain, warnOnMissingSession, page }) {
|
|
70
70
|
const refused = (warning) => refusedPage(1, [warning], true);
|
|
71
71
|
if (options.filters?.length && options.includeAllAssignees === true) return refused(otherWarning(options.providerId, domain, options.connectionId, "`filters` and `includeAllAssignees` are contradictory for an account-wide issue read; pass only one."));
|
|
72
|
-
if (options.includeAllAssignees === true && (options.providerId === "github" || options.providerId === "cloud-github-enterprise")) return refused(
|
|
73
|
-
if ((options.org != null || options.project != null) && !integration.supportsProjectDiscovery) return refused(
|
|
72
|
+
if (options.includeAllAssignees === true && (options.providerId === "github" || options.providerId === "cloud-github-enterprise")) return refused(unsupportedWarning(options.providerId, domain, options.connectionId, "`includeAllAssignees` is not supported for account-wide GitHub issue reads; scope the read to repositories instead."));
|
|
73
|
+
if ((options.org != null || options.project != null) && !integration.supportsProjectDiscovery) return refused(unsupportedWarning(options.providerId, domain, options.connectionId, `Project-scoped issue reads are not supported by '${options.providerId}'; scope the read to repositories instead.`));
|
|
74
74
|
const resolvedIssueFilters = resolveAccountWideIssueFilters(options.providerId, options.filters);
|
|
75
75
|
if (resolvedIssueFilters.unsupported) return refused(unsupportedAccountWideIssueFiltersWarning(options.providerId, domain, options.connectionId, options.filters));
|
|
76
76
|
const resolvedSort = resolveIssueSort(providersMetadata[options.providerId]?.supportedAccountWideIssueSorts, options.sort);
|
|
@@ -100,7 +100,7 @@ async function readAccountWideIssuesPage({ options, integration, domain, warnOnM
|
|
|
100
100
|
const { value, currentPage, requestedPageMissing } = drained;
|
|
101
101
|
const currentTruncated = drained.truncated;
|
|
102
102
|
const pageFetchFailed = drained.fetchFailed;
|
|
103
|
-
if (value == null && warnings.length === 0) return refused(
|
|
103
|
+
if (value == null && warnings.length === 0) return refused(unsupportedWarning(options.providerId, domain, options.connectionId, `Account-wide issue search is not supported by '${options.providerId}'; scope the read to repositories instead.`));
|
|
104
104
|
const items = requestedPageMissing ? [] : value?.values ?? [];
|
|
105
105
|
const orderedItems = accountWideOrdering.order(items);
|
|
106
106
|
const assessment = mergeAssessmentInto(warnings, options.providerId, domain, options.connectionId, allMetadata);
|