@gitkraken/core-gitlens 0.9.3 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -1
- package/dist/git/models/issue.d.ts +25 -2
- package/dist/git/models/issue.js +3 -1
- package/dist/git/models/issue.js.map +1 -1
- package/dist/git/models/pullRequest.d.ts +55 -5
- package/dist/git/models/pullRequest.js +5 -1
- package/dist/git/models/pullRequest.js.map +1 -1
- package/dist/git/utils/issue.utils.d.ts +4 -0
- package/dist/git/utils/issue.utils.js +4 -0
- package/dist/git/utils/issue.utils.js.map +1 -1
- package/dist/git/utils/pullRequest.utils.d.ts +4 -0
- package/dist/git/utils/pullRequest.utils.js +8 -0
- package/dist/git/utils/pullRequest.utils.js.map +1 -1
- package/dist/plus/git-github/api/github.d.ts +64 -4
- package/dist/plus/git-github/api/github.js +232 -15
- package/dist/plus/git-github/api/github.js.map +1 -1
- package/dist/plus/git-github/models.d.ts +48 -6
- package/dist/plus/git-github/models.js +11 -10
- package/dist/plus/git-github/models.js.map +1 -1
- package/dist/plus/integrations/fieldPresence.d.ts +75 -0
- package/dist/plus/integrations/fieldPresence.js +381 -0
- package/dist/plus/integrations/fieldPresence.js.map +1 -0
- package/dist/plus/integrations/index.d.ts +6 -4
- package/dist/plus/integrations/index.js +2 -1
- package/dist/plus/integrations/index.js.map +1 -1
- package/dist/plus/integrations/integrationService.d.ts +13 -4
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/manager.d.ts +62 -7
- package/dist/plus/integrations/models/gitHostIntegration.d.ts +103 -9
- package/dist/plus/integrations/models/gitHostIntegration.js +116 -5
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
- package/dist/plus/integrations/models/integration.d.ts +91 -4
- package/dist/plus/integrations/models/integration.js +56 -7
- package/dist/plus/integrations/models/integration.js.map +1 -1
- package/dist/plus/integrations/models/issueReads.d.ts +14 -0
- package/dist/plus/integrations/models/issuesIntegration.d.ts +44 -3
- package/dist/plus/integrations/models/issuesIntegration.js +51 -1
- package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
- package/dist/plus/integrations/providers/azure/azure.js +3 -3
- package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
- package/dist/plus/integrations/providers/azure/models.d.ts +36 -4
- package/dist/plus/integrations/providers/azure/models.js +118 -8
- package/dist/plus/integrations/providers/azure/models.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOps.d.ts +102 -5
- package/dist/plus/integrations/providers/azureDevOps.js +264 -45
- package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOpsServer.d.ts +30 -6
- package/dist/plus/integrations/providers/azureDevOpsServer.js +82 -21
- package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket/bitbucket.d.ts +21 -1
- package/dist/plus/integrations/providers/bitbucket/bitbucket.js +74 -10
- package/dist/plus/integrations/providers/bitbucket/bitbucket.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket/models.d.ts +45 -8
- package/dist/plus/integrations/providers/bitbucket/models.js +32 -9
- package/dist/plus/integrations/providers/bitbucket/models.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket-server/models.d.ts +2 -6
- package/dist/plus/integrations/providers/bitbucket-server/models.js +1 -3
- package/dist/plus/integrations/providers/bitbucket-server/models.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.d.ts +3 -1
- package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.js +4 -1
- package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket-server.d.ts +2 -2
- package/dist/plus/integrations/providers/bitbucket-server.js +7 -5
- package/dist/plus/integrations/providers/bitbucket-server.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket.d.ts +22 -2
- package/dist/plus/integrations/providers/bitbucket.js +29 -6
- package/dist/plus/integrations/providers/bitbucket.js.map +1 -1
- package/dist/plus/integrations/providers/github.d.ts +40 -3
- package/dist/plus/integrations/providers/github.js +133 -11
- package/dist/plus/integrations/providers/github.js.map +1 -1
- package/dist/plus/integrations/providers/gitlab/gitlab.d.ts +34 -1
- package/dist/plus/integrations/providers/gitlab/gitlab.js +118 -4
- package/dist/plus/integrations/providers/gitlab/gitlab.js.map +1 -1
- package/dist/plus/integrations/providers/gitlab/models.d.ts +70 -5
- package/dist/plus/integrations/providers/gitlab/models.js +106 -13
- package/dist/plus/integrations/providers/gitlab/models.js.map +1 -1
- package/dist/plus/integrations/providers/gitlab.d.ts +31 -2
- package/dist/plus/integrations/providers/gitlab.js +48 -8
- package/dist/plus/integrations/providers/gitlab.js.map +1 -1
- package/dist/plus/integrations/providers/jira-server.d.ts +3 -2
- package/dist/plus/integrations/providers/jira-server.js +18 -9
- package/dist/plus/integrations/providers/jira-server.js.map +1 -1
- package/dist/plus/integrations/providers/jira.d.ts +26 -2
- package/dist/plus/integrations/providers/jira.js +120 -9
- package/dist/plus/integrations/providers/jira.js.map +1 -1
- package/dist/plus/integrations/providers/jiraIssueByKey.d.ts +79 -0
- package/dist/plus/integrations/providers/jiraIssueByKey.js +58 -7
- package/dist/plus/integrations/providers/jiraIssueByKey.js.map +1 -1
- package/dist/plus/integrations/providers/linear.d.ts +19 -1
- package/dist/plus/integrations/providers/linear.js +105 -7
- package/dist/plus/integrations/providers/linear.js.map +1 -1
- package/dist/plus/integrations/providers/linearIssuesEtag.d.ts +42 -0
- package/dist/plus/integrations/providers/linearIssuesEtag.js +94 -0
- package/dist/plus/integrations/providers/linearIssuesEtag.js.map +1 -0
- package/dist/plus/integrations/providers/models.d.ts +70 -8
- package/dist/plus/integrations/providers/models.js +59 -21
- package/dist/plus/integrations/providers/models.js.map +1 -1
- package/dist/plus/integrations/providers/providerErrors.js +8 -1
- package/dist/plus/integrations/providers/providerErrors.js.map +1 -1
- package/dist/plus/integrations/providers/providersApi.d.ts +70 -4
- package/dist/plus/integrations/providers/providersApi.js +148 -4
- package/dist/plus/integrations/providers/providersApi.js.map +1 -1
- package/dist/plus/integrations/providers/pullRequestReviews.d.ts +13 -0
- package/dist/plus/integrations/providers/pullRequestReviews.js +30 -6
- package/dist/plus/integrations/providers/pullRequestReviews.js.map +1 -1
- package/dist/plus/integrations/providers/trello.js +11 -7
- package/dist/plus/integrations/providers/trello.js.map +1 -1
- package/dist/plus/integrations/providers/utils/providerPaging.d.ts +11 -0
- package/dist/plus/integrations/providers/utils/providerPaging.js +47 -1
- package/dist/plus/integrations/providers/utils/providerPaging.js.map +1 -1
- package/dist/plus/integrations/providers/utils.d.ts +1 -1
- package/dist/plus/integrations/reads/batchEtags.d.ts +69 -0
- package/dist/plus/integrations/reads/batchEtags.js +114 -0
- package/dist/plus/integrations/reads/batchEtags.js.map +1 -0
- package/dist/plus/integrations/reads/counts.js +2 -2
- package/dist/plus/integrations/reads/counts.js.map +1 -1
- package/dist/plus/integrations/reads/currentAccount.js +2 -2
- package/dist/plus/integrations/reads/currentAccount.js.map +1 -1
- package/dist/plus/integrations/reads/cursors.d.ts +8 -0
- package/dist/plus/integrations/reads/cursors.js +5 -3
- package/dist/plus/integrations/reads/cursors.js.map +1 -1
- package/dist/plus/integrations/reads/drains.d.ts +13 -4
- package/dist/plus/integrations/reads/drains.js +29 -5
- package/dist/plus/integrations/reads/drains.js.map +1 -1
- package/dist/plus/integrations/reads/etag.d.ts +26 -0
- package/dist/plus/integrations/reads/etag.js +108 -0
- package/dist/plus/integrations/reads/etag.js.map +1 -0
- package/dist/plus/integrations/reads/filters.d.ts +8 -0
- package/dist/plus/integrations/reads/filters.js +2 -1
- package/dist/plus/integrations/reads/filters.js.map +1 -1
- package/dist/plus/integrations/reads/hierarchy.js +3 -3
- package/dist/plus/integrations/reads/hierarchy.js.map +1 -1
- package/dist/plus/integrations/reads/issueBatch.d.ts +47 -16
- package/dist/plus/integrations/reads/issueBatch.js +55 -87
- package/dist/plus/integrations/reads/issueBatch.js.map +1 -1
- package/dist/plus/integrations/reads/issueTracker.d.ts +10 -2
- package/dist/plus/integrations/reads/issueTracker.js +23 -6
- package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
- package/dist/plus/integrations/reads/issues.d.ts +1 -1
- package/dist/plus/integrations/reads/issues.js +4 -4
- package/dist/plus/integrations/reads/issues.js.map +1 -1
- package/dist/plus/integrations/reads/pullRequestBatch.d.ts +44 -6
- package/dist/plus/integrations/reads/pullRequestBatch.js +31 -34
- package/dist/plus/integrations/reads/pullRequestBatch.js.map +1 -1
- package/dist/plus/integrations/reads/pullRequestBranches.d.ts +5 -2
- package/dist/plus/integrations/reads/pullRequestBranches.js +4 -3
- package/dist/plus/integrations/reads/pullRequestBranches.js.map +1 -1
- package/dist/plus/integrations/reads/pullRequests.js +8 -4
- package/dist/plus/integrations/reads/pullRequests.js.map +1 -1
- package/dist/plus/integrations/reads/sweeps.js +8 -4
- package/dist/plus/integrations/reads/sweeps.js.map +1 -1
- package/dist/plus/integrations/reads/warnings.d.ts +23 -8
- package/dist/plus/integrations/reads/warnings.js +43 -19
- package/dist/plus/integrations/reads/warnings.js.map +1 -1
- package/dist/plus/integrations/results.d.ts +12 -4
- package/dist/plus/integrations/results.js +2 -0
- package/dist/plus/integrations/results.js.map +1 -1
- package/docs/integrations.md +389 -23
- package/docs/kepler-read-api-parity.md +16 -2
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hierarchy.js","names":[],"sources":["../../../../../plus/integrations/src/reads/hierarchy.ts"],"sourcesContent":["import { mapBounded } from '@gitlens/utils/promise.js';\nimport { mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport {\n\tGitCloudHostIntegrationId,\n\tGitSelfManagedHostIntegrationId,\n\tproviderFanOutConcurrency,\n\tsupportedOrderedCloudIntegrationIds,\n\tsupportedOrderedCloudIssuesIntegrationIds,\n} from '../constants.js';\nimport type { ListOrgsOptions, ListProjectsOptions } from '../manager.js';\nimport { isIssuesIntegration } from '../models/issuesIntegration.js';\nimport type { ProviderHierarchyResult } from '../providers/models.js';\nimport { toProviderRepositoryShape } from '../providers/models.js';\nimport type {\n\tProviderOrganization,\n\tProviderPagedResult,\n\tProviderRepositoryShape,\n\tProviderResult,\n\tProviderWarning,\n} from '../results.js';\nimport { appendDedupedWarning } from '../results.js';\nimport {\n\tisGitHostIntegration,\n\tisIssuesHostIntegrationId,\n\twarnOnMissingSessionForDomain,\n} from '../utils/integration.utils.js';\nimport type { ProviderReadContext } from './context.js';\nimport { runCaptured } from './drains.js';\nimport {\n\tassertHierarchyReadTarget,\n\tmergeHierarchyResults,\n\torgForProject,\n\tresourceMatchesOrg,\n\tresourceToOrg,\n\twithProviderContext,\n} from './hierarchy.utils.js';\nimport { pageToCursor, refusedPage, resolveContinuation, resolveCurrentPage, toProviderPageInfo } from './paging.js';\nimport { gitHostOnlySurfaceWarning, otherWarning, truncationWarning } from './warnings.js';\n\n/**\n * Folds a flat hierarchy result's incompleteness into `warnings`, returning whether it leaves the read\n * non-authoritative. The single funnel for both hierarchy reads, so neither can drift from the other on any of\n * the four decisions it makes:\n *\n * - The metadata is assessed FIRST, so the truncation warning can tell whether it already explained this gap.\n * Ordered deliberately: emitting first meant an expired credential produced BOTH a typed `auth` warning and\n * an unclassifiable one for the same cause, and a consumer routing on classification showed the second as\n * \"this provider failed to load\", with a remedy that doesn't exist, next to the reconnect prompt that was the\n * actual fix.\n * - The read states the truncation itself only when the metadata reported nothing — the rule\n * `assessCollectionMetadata` applies to its own generic fallback. Whatever the metadata reported already names\n * this provider and says something stronger, so restating it costs a false verdict and adds no information.\n * - `'exhausted'`, never `'page-budget'`: `truncated` is one boolean here, so a drain that stopped at its page\n * budget is indistinguishable from one whose cursor stalled, and `IncompleteReadCause` makes `'exhausted'`\n * the default whenever a raisable budget is not demonstrably the cause.\n * - A truncation counts as non-authoritative even though the omission above asserts the read succeeded. Unlike\n * paged repository reads, a flattened hierarchy result has no page object on which to carry incompleteness,\n * so `fetchFailed` is the only signal a consumer has that the list is short.\n */\nfunction mergeHierarchyIncompleteness(\n\twarnings: ProviderWarning[],\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tresult: Pick<ProviderHierarchyResult<unknown>, 'truncated' | 'metadata'>,\n\treadKind: 'Organization' | 'Project',\n): boolean {\n\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, result.metadata);\n\tif (!result.truncated) return assessment.fetchFailed || assessment.truncated;\n\n\tif (!assessment.reported) {\n\t\t// Deduped like every other `truncationWarning` emission (see `drains.ts`), not plain-pushed: no caller can\n\t\t// collide with it today, but nothing about this funnel guarantees a third read won't.\n\t\tappendDedupedWarning(warnings, truncationWarning(id, domain, connectionId, readKind, 'exhausted'));\n\t}\n\treturn true;\n}\n\nexport async function listOrgs(\n\tctx: ProviderReadContext,\n\toptions?: ListOrgsOptions,\n): Promise<ProviderResult<ProviderOrganization>> {\n\tassertHierarchyReadTarget('listOrgs', options);\n\tconst ids = options?.providerId != null ? [options.providerId] : supportedOrderedCloudIntegrationIds;\n\tconst singleProvider = ids.length === 1;\n\tconst connectionId = singleProvider ? options?.connectionId : undefined;\n\tconst requestedDomain = singleProvider ? options?.domain : undefined;\n\n\tconst results = await mapBounded(ids, providerFanOutConcurrency, async id => {\n\t\tconst integration = await ctx.getIntegrationForRead(id, connectionId, requestedDomain);\n\t\tif (integration == null) {\n\t\t\t// A specifically requested connection that can't be resolved is a broken connection, not a\n\t\t\t// provider with no orgs — surface it (warning + fetchFailed) instead of dropping the id\n\t\t\t// silently, so a caller can tell it apart from an account that genuinely has no orgs.\n\t\t\tconst early = ctx.earlyReturnConnectionWarnings(id, connectionId, requestedDomain);\n\t\t\treturn {\n\t\t\t\titems: [] as ProviderOrganization[],\n\t\t\t\twarnings: early.warnings,\n\t\t\t\tfetchFailed: early.fetchFailed,\n\t\t\t};\n\t\t}\n\n\t\tconst items: ProviderOrganization[] = [];\n\t\tconst warnings: ProviderWarning[] = [];\n\t\tlet fetchFailed = false;\n\t\tconst domain = ctx.domainForRead(integration, id, connectionId, requestedDomain);\n\t\tconst warnOnMissingSession = warnOnMissingSessionForDomain(id, requestedDomain);\n\t\tif (isIssuesIntegration(integration)) {\n\t\t\t// Issue trackers expose \"resources\" (Jira sites, Linear orgs, …) as their org analogue.\n\t\t\tconst { value: resources, warning } = await runCaptured(\n\t\t\t\tid,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t() => integration.getResourcesForUserResult(connectionId),\n\t\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t\t);\n\t\t\tif (resources != null) {\n\t\t\t\titems.push(...resources.map(r => resourceToOrg(id, r)));\n\t\t\t}\n\t\t\tif (warning != null) {\n\t\t\t\twarnings.push(warning);\n\t\t\t\t// A warning with no value is a hard read failure, not an empty account.\n\t\t\t\tif (resources == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (!integration.supportsOrganizationDiscovery) {\n\t\t\t// The provider registers no org-discovery hook. Report it as\n\t\t\t// explicitly unsupported rather than contributing a silent empty list that a caller can't\n\t\t\t// tell apart from \"this account has no orgs\".\n\t\t\tfetchFailed = true;\n\t\t\twarnings.push(\n\t\t\t\totherWarning(id, domain, connectionId, `Organization discovery is not supported by '${id}'.`),\n\t\t\t);\n\t\t} else {\n\t\t\tconst { value, warning } = await runCaptured(\n\t\t\t\tid,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t() => integration.getOrganizationsForUserResult(connectionId),\n\t\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t\t);\n\t\t\tif (value != null) {\n\t\t\t\titems.push(...value.values.map(org => withProviderContext(id, org)));\n\n\t\t\t\tif (mergeHierarchyIncompleteness(warnings, id, domain, connectionId, value, 'Organization')) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (warning != null) {\n\t\t\t\twarnings.push(warning);\n\t\t\t\tif (value == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed };\n\t});\n\n\treturn mergeHierarchyResults(results);\n}\n\nexport async function listProjects(\n\tctx: ProviderReadContext,\n\toptions?: ListProjectsOptions,\n): Promise<ProviderResult<ProviderOrganization>> {\n\tassertHierarchyReadTarget('listProjects', options);\n\tconst ids =\n\t\toptions?.providerId != null\n\t\t\t? [options.providerId]\n\t\t\t: [\n\t\t\t\t\t...supportedOrderedCloudIssuesIntegrationIds,\n\t\t\t\t\tGitCloudHostIntegrationId.AzureDevOps,\n\t\t\t\t\tGitSelfManagedHostIntegrationId.AzureDevOpsServer,\n\t\t\t\t];\n\tconst singleProvider = ids.length === 1;\n\tconst connectionId = singleProvider ? options?.connectionId : undefined;\n\tconst requestedDomain = singleProvider ? options?.domain : undefined;\n\n\tconst results = await mapBounded(ids, providerFanOutConcurrency, async id => {\n\t\tconst integration = await ctx.getIntegrationForRead(id, connectionId, requestedDomain);\n\t\tif (integration == null) {\n\t\t\t// A requested connection that can't be resolved is a broken connection, not a provider with\n\t\t\t// no projects — surface it (warning + fetchFailed) instead of dropping the id silently.\n\t\t\tconst early = ctx.earlyReturnConnectionWarnings(id, connectionId, requestedDomain);\n\t\t\treturn {\n\t\t\t\titems: [] as ProviderOrganization[],\n\t\t\t\twarnings: early.warnings,\n\t\t\t\tfetchFailed: early.fetchFailed,\n\t\t\t};\n\t\t}\n\n\t\tconst items: ProviderOrganization[] = [];\n\t\tconst warnings: ProviderWarning[] = [];\n\t\tlet fetchFailed = false;\n\t\tconst domain = ctx.domainForRead(integration, id, connectionId, requestedDomain);\n\t\tconst org = options?.org;\n\t\tconst warnOnMissingSession = warnOnMissingSessionForDomain(id, requestedDomain);\n\n\t\t// Git hosts with a project tier (Azure DevOps) read projects through their own hierarchy hook,\n\t\t// scoped to `org` when given. Non-Azure git hosts have no project tier and return undefined.\n\t\tif (!isIssuesIntegration(integration)) {\n\t\t\t// Check the capability first, like listOrgs/listRepos/listIssuesPage do. Without it, a host\n\t\t\t// with no project tier returns `undefined` from a successful read, which `runCaptured` can't\n\t\t\t// tell apart from an unresolvable session — turning a healthy provider into a `no-connection`\n\t\t\t// warning + `fetchFailed` (and, in Kepler, a spurious reconnect prompt).\n\t\t\tif (!integration.supportsProjectDiscovery) {\n\t\t\t\treturn { items: items, warnings: warnings, fetchFailed: false };\n\t\t\t}\n\n\t\t\tconst { value: projects, warning } = await runCaptured(\n\t\t\t\tid,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t() => integration.getProjectsForOrgResult(org, connectionId),\n\t\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t\t);\n\t\t\tif (warning != null) {\n\t\t\t\twarnings.push(warning);\n\t\t\t\tif (projects == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (projects != null) {\n\t\t\t\titems.push(...projects.values.map(project => withProviderContext(id, project)));\n\n\t\t\t\tif (mergeHierarchyIncompleteness(warnings, id, domain, connectionId, projects, 'Project')) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed };\n\t\t}\n\n\t\tconst { value: resources, warning: resourcesWarning } = await runCaptured(\n\t\t\tid,\n\t\t\tdomain,\n\t\t\tconnectionId,\n\t\t\t() => integration.getResourcesForUserResult(connectionId),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t);\n\t\tif (resourcesWarning != null) {\n\t\t\twarnings.push(resourcesWarning);\n\t\t\tif (resources == null) {\n\t\t\t\tfetchFailed = true;\n\t\t\t}\n\t\t}\n\n\t\tconst scopedResources =\n\t\t\torg != null ? resources?.filter(resource => resourceMatchesOrg(resource, org)) : resources;\n\t\tif (scopedResources != null && scopedResources.length !== 0) {\n\t\t\tconst { value: projects, warning: projectsWarning } = await runCaptured(id, domain, connectionId, () =>\n\t\t\t\tintegration.getProjectsForResourcesWithMetadataResult(scopedResources, connectionId),\n\t\t\t);\n\t\t\tif (projectsWarning != null) {\n\t\t\t\twarnings.push(projectsWarning);\n\t\t\t\tif (projects == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (projects != null) {\n\t\t\t\titems.push(\n\t\t\t\t\t...projects.values.map(project =>\n\t\t\t\t\t\tresourceToOrg(id, project, orgForProject(id, project, scopedResources)),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, projects?.metadata);\n\t\t\tif (assessment.fetchFailed || assessment.truncated) {\n\t\t\t\tfetchFailed = true;\n\t\t\t}\n\t\t}\n\n\t\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed };\n\t});\n\n\treturn mergeHierarchyResults(results);\n}\n\nexport async function listRepos(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\torg?: string;\n\t\tproject?: string;\n\t\tpage?: number;\n\t\tcursor?: string;\n\t\titemsPerPage?: number;\n\t\tconnectionId?: string;\n\t\tdomain?: string;\n\t},\n): Promise<ProviderPagedResult<ProviderRepositoryShape>> {\n\t// Truncated as well as floored, like every other paged read here: `page` is reported back as\n\t// `page.currentPage` and bounds the cursor walk below, and neither can be fractional.\n\tconst page = Math.max(1, Math.trunc(options.page ?? 1));\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'repository discovery')],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\t// A supplied connection or domain that no longer resolves is a broken target, not an empty account —\n\t\t// surface a no-connection warning + fetchFailed rather than a silent empty page.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\treturn refusedPage(page, early.warnings, early.fetchFailed);\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'repository discovery')],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\n\tconst accountWide = options.org == null;\n\tconst supported = accountWide\n\t\t? integration.supportsUserRepositoryDiscovery\n\t\t: integration.supportsRepositoryDiscovery;\n\tif (!supported) {\n\t\t// No matching repo-discovery hook — org-scoped (e.g. Bitbucket Data Center) or account-wide (e.g.\n\t\t// Bitbucket/Azure, whose repos can only be walked per workspace/org). Report unsupported rather than\n\t\t// a silent empty page indistinguishable from \"no repos\"; for the account-wide case the caller should\n\t\t// fan out per org from listOrgs instead.\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[\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\taccountWide\n\t\t\t\t\t\t? `Account-wide repository discovery is not supported by '${options.providerId}'; list repositories per org instead.`\n\t\t\t\t\t\t: `Repository discovery is not supported by '${options.providerId}'.`,\n\t\t\t\t),\n\t\t\t],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst org = options.org;\n\tconst warnOnMissingSession = warnOnMissingSessionForDomain(options.providerId, options.domain);\n\tconst readPage = (cursor: string | undefined) =>\n\t\trunCaptured(\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\t() =>\n\t\t\t\torg == null\n\t\t\t\t\t? integration.getRepositoriesForUserResult({ cursor: cursor, connectionId: options.connectionId })\n\t\t\t\t\t: integration.getRepositoriesForOrgResult(org, {\n\t\t\t\t\t\t\tproject: options.project,\n\t\t\t\t\t\t\tcursor: cursor,\n\t\t\t\t\t\t\tconnectionId: options.connectionId,\n\t\t\t\t\t\t}),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t);\n\n\t/**\n\t * Builds one page's result from what the provider returned. Shared by the initial read and by every round of\n\t * the cursor walk below, so a walked page N is described by exactly the same rules as a directly-requested one\n\t * (position, continuation, truncation) rather than by a second copy of them.\n\t */\n\tconst toResult = (\n\t\t{ value, warning }: Awaited<ReturnType<typeof readPage>>,\n\t\trequestedPage: number,\n\t\tsuppliedCursor: string | undefined,\n\t): ProviderPagedResult<ProviderRepositoryShape> => {\n\t\tconst items = value?.values ?? [];\n\t\tconst warnings = warning != null ? [warning] : [];\n\t\t// The repos read core is cursor-only and can't accept a page size, so don't echo the requested\n\t\t// `itemsPerPage` as if it were applied — report what the provider returned (its own pageSize when\n\t\t// available, else the actual item count).\n\t\tconst paged = toProviderPageInfo(items.length, value?.paging);\n\t\t// Convert the SDK collection metadata into scope-aware warnings + failure/truncation flags, appending\n\t\t// them to any captured thrown-error warning without discarding the partial result's items.\n\t\tconst assessment = mergeAssessmentInto(\n\t\t\twarnings,\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\tvalue?.metadata,\n\t\t);\n\t\t// Whether this read honors a page NUMBER, decided from what the provider actually reported rather than\n\t\t// guessed from the absence of a cursor. A numbered-page repos host reports its position (`paging.page`)\n\t\t// and/or its successor (`paging.nextPage`) — Bitbucket's workspace walk consumes `page` as a real 1-based\n\t\t// page and reports `nextPage` without echoing `currentPage`, so both signals have to count. Every wired\n\t\t// cursor-based repos read (GitHub's org/user walks, GitLab's user walk, Bitbucket's cursor read) reports\n\t\t// neither. Keying off `paged.cursor == null` instead read the paging layer's \"reported another page,\n\t\t// handed back no continuation\" sentinel as evidence of a numbered host, which is exactly backwards.\n\t\tconst pageAdvanceable = value?.paging?.page != null || value?.paging?.nextPage != null;\n\t\t// Numbered-page hosts that don't echo `currentPage` may still be advanced by the requested `page` (initial\n\t\t// read) or by the cursor the caller threaded back. A cursor-only host ignores an UNTHREADED `page`\n\t\t// request, so its page-less first page is page 1.\n\t\tconst currentPage = resolveCurrentPage({\n\t\t\tproviderPage: paged.page.currentPage,\n\t\t\trequestedPage: requestedPage,\n\t\t\tsuppliedCursor: suppliedCursor,\n\t\t\tpageAdvanceable: pageAdvanceable,\n\t\t});\n\t\t// Never advertise `hasMore` without a continuation the caller can act on — the same contract\n\t\t// `listPullRequestsPage`, `listIssuesPage` and `broadenIssues` hold, via the same helper. A provider that\n\t\t// reports another page but hands back neither `endCursor` nor `nextPage` (surfaced as the `'{}'` sentinel,\n\t\t// which `toProviderPageInfo` drops) is terminal-but-incomplete: `hasMore: false` + `page.truncated`.\n\t\t// Synthesizing a page-number cursor for a cursor-only host instead — which is what this read used to do\n\t\t// unconditionally — handed back a continuation the provider ignores, so it answered with its FIRST page\n\t\t// again while still reporting `hasMore: true`. A consumer draining until `hasMore` clears (Kepler's repo\n\t\t// drain does exactly that) then looped to its own page cap, accumulating a duplicate copy of every repo\n\t\t// per round, and never saw a truncation signal.\n\t\tconst continuation = resolveContinuation(paged, pageAdvanceable ? currentPage + 1 : undefined);\n\t\t// The org-hierarchy read can also stop at a defensive backstop with more repos unlisted and no cursor to\n\t\t// resume (top-level `truncated`, or `paging.truncated` on a single-page read). Metadata incompleteness and\n\t\t// a demoted continuation are two further, independent sources of the same signal.\n\t\tconst truncated =\n\t\t\t(value?.truncated ?? false) ||\n\t\t\t(value?.paging?.truncated ?? false) ||\n\t\t\tassessment.truncated ||\n\t\t\tcontinuation.truncated;\n\t\treturn {\n\t\t\t// Normalize the raw provider-apis repos to the GitLens-owned shape at the surface boundary.\n\t\t\titems: items.map(toProviderRepositoryShape),\n\t\t\twarnings: warnings,\n\t\t\tpage: { ...paged.page, currentPage: currentPage, truncated: truncated || undefined },\n\t\t\thasMore: continuation.hasMore,\n\t\t\tcursor: continuation.cursor,\n\t\t\tfetchFailed: assessment.fetchFailed || (warning != null && value == null) || undefined,\n\t\t};\n\t};\n\n\tconst first = await readPage(\n\t\toptions.cursor ??\n\t\t\t(integration.id === GitSelfManagedHostIntegrationId.BitbucketServer ? undefined : pageToCursor(page)),\n\t);\n\tconst result = toResult(first, page, options.cursor);\n\t// A numbered-page host honored the synthesized cursor, so this IS page N; a cursor-only host ignored it and\n\t// answered with page 1, which the walk below advances. Decided from the provider's own paging metadata rather\n\t// than assumed, hence after the read.\n\tconst pageAdvanceable = first.value?.paging?.page != null || first.value?.paging?.nextPage != null;\n\tif (options.cursor != null || page === 1 || pageAdvanceable) return result;\n\n\t// Walk the provider's real continuations to the requested page. Only the last page's items are kept — pages\n\t// 1..N returned as \"page N\" would duplicate items for a normal paged consumer — while warnings, truncation,\n\t// and failure accumulate across the whole walked prefix, since an earlier round's problem still means this\n\t// result isn't authoritative. The provider read is re-issued per round, but the integration is resolved once:\n\t// re-entering this function per page would repeat its auth resolution N times for page N.\n\tconst walkedWarnings = [...result.warnings];\n\tlet walkedFetchFailed = result.fetchFailed === true;\n\tlet walkedTruncated = result.page.truncated === true;\n\tlet previous = result;\n\tfor (let currentPage = 2; currentPage <= page; currentPage++) {\n\t\tif (!previous.hasMore || previous.cursor == null) {\n\t\t\t// The requested page lies past the provider's terminal cursor: an EMPTY page N, never the last\n\t\t\t// available page relabeled (see ProviderPageInfo.currentPage).\n\t\t\treturn {\n\t\t\t\titems: [],\n\t\t\t\twarnings: walkedWarnings,\n\t\t\t\tpage: { currentPage: page, itemsPerPage: 0, truncated: walkedTruncated || undefined },\n\t\t\t\thasMore: false,\n\t\t\t\tfetchFailed: walkedFetchFailed || undefined,\n\t\t\t};\n\t\t}\n\n\t\tconst suppliedCursor = previous.cursor;\n\t\tconst requested = toResult(await readPage(suppliedCursor), currentPage, suppliedCursor);\n\t\tfor (const warning of requested.warnings) {\n\t\t\tappendDedupedWarning(walkedWarnings, warning);\n\t\t}\n\t\twalkedFetchFailed ||= requested.fetchFailed === true;\n\t\twalkedTruncated ||= requested.page.truncated === true;\n\t\tif (currentPage === page) {\n\t\t\treturn {\n\t\t\t\t...requested,\n\t\t\t\twarnings: walkedWarnings,\n\t\t\t\tpage: { ...requested.page, truncated: walkedTruncated || undefined },\n\t\t\t\tfetchFailed: walkedFetchFailed || undefined,\n\t\t\t};\n\t\t}\n\n\t\tprevious = requested;\n\t}\n\n\treturn result;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"hierarchy.js","names":[],"sources":["../../../../../plus/integrations/src/reads/hierarchy.ts"],"sourcesContent":["import { mapBounded } from '@gitlens/utils/promise.js';\nimport { mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport {\n\tGitCloudHostIntegrationId,\n\tGitSelfManagedHostIntegrationId,\n\tproviderFanOutConcurrency,\n\tsupportedOrderedCloudIntegrationIds,\n\tsupportedOrderedCloudIssuesIntegrationIds,\n} from '../constants.js';\nimport type { ListOrgsOptions, ListProjectsOptions } from '../manager.js';\nimport { isIssuesIntegration } from '../models/issuesIntegration.js';\nimport type { ProviderHierarchyResult } from '../providers/models.js';\nimport { toProviderRepositoryShape } from '../providers/models.js';\nimport type {\n\tProviderOrganization,\n\tProviderPagedResult,\n\tProviderRepositoryShape,\n\tProviderResult,\n\tProviderWarning,\n} from '../results.js';\nimport { appendDedupedWarning } from '../results.js';\nimport {\n\tisGitHostIntegration,\n\tisIssuesHostIntegrationId,\n\twarnOnMissingSessionForDomain,\n} from '../utils/integration.utils.js';\nimport type { ProviderReadContext } from './context.js';\nimport { runCaptured } from './drains.js';\nimport {\n\tassertHierarchyReadTarget,\n\tmergeHierarchyResults,\n\torgForProject,\n\tresourceMatchesOrg,\n\tresourceToOrg,\n\twithProviderContext,\n} from './hierarchy.utils.js';\nimport { pageToCursor, refusedPage, resolveContinuation, resolveCurrentPage, toProviderPageInfo } from './paging.js';\nimport { gitHostOnlySurfaceWarning, truncationWarning, unsupportedWarning } from './warnings.js';\n\n/**\n * Folds a flat hierarchy result's incompleteness into `warnings`, returning whether it leaves the read\n * non-authoritative. The single funnel for both hierarchy reads, so neither can drift from the other on any of\n * the four decisions it makes:\n *\n * - The metadata is assessed FIRST, so the truncation warning can tell whether it already explained this gap.\n * Ordered deliberately: emitting first meant an expired credential produced BOTH a typed `auth` warning and\n * an unclassifiable one for the same cause, and a consumer routing on classification showed the second as\n * \"this provider failed to load\", with a remedy that doesn't exist, next to the reconnect prompt that was the\n * actual fix.\n * - The read states the truncation itself only when the metadata reported nothing — the rule\n * `assessCollectionMetadata` applies to its own generic fallback. Whatever the metadata reported already names\n * this provider and says something stronger, so restating it costs a false verdict and adds no information.\n * - `'exhausted'`, never `'page-budget'`: `truncated` is one boolean here, so a drain that stopped at its page\n * budget is indistinguishable from one whose cursor stalled, and `IncompleteReadCause` makes `'exhausted'`\n * the default whenever a raisable budget is not demonstrably the cause.\n * - A truncation counts as non-authoritative even though the omission above asserts the read succeeded. Unlike\n * paged repository reads, a flattened hierarchy result has no page object on which to carry incompleteness,\n * so `fetchFailed` is the only signal a consumer has that the list is short.\n */\nfunction mergeHierarchyIncompleteness(\n\twarnings: ProviderWarning[],\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tresult: Pick<ProviderHierarchyResult<unknown>, 'truncated' | 'metadata'>,\n\treadKind: 'Organization' | 'Project',\n): boolean {\n\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, result.metadata);\n\tif (!result.truncated) return assessment.fetchFailed || assessment.truncated;\n\n\tif (!assessment.reported) {\n\t\t// Deduped like every other `truncationWarning` emission (see `drains.ts`), not plain-pushed: no caller can\n\t\t// collide with it today, but nothing about this funnel guarantees a third read won't.\n\t\tappendDedupedWarning(warnings, truncationWarning(id, domain, connectionId, readKind, 'exhausted'));\n\t}\n\treturn true;\n}\n\nexport async function listOrgs(\n\tctx: ProviderReadContext,\n\toptions?: ListOrgsOptions,\n): Promise<ProviderResult<ProviderOrganization>> {\n\tassertHierarchyReadTarget('listOrgs', options);\n\tconst ids = options?.providerId != null ? [options.providerId] : supportedOrderedCloudIntegrationIds;\n\tconst singleProvider = ids.length === 1;\n\tconst connectionId = singleProvider ? options?.connectionId : undefined;\n\tconst requestedDomain = singleProvider ? options?.domain : undefined;\n\n\tconst results = await mapBounded(ids, providerFanOutConcurrency, async id => {\n\t\tconst integration = await ctx.getIntegrationForRead(id, connectionId, requestedDomain);\n\t\tif (integration == null) {\n\t\t\t// A specifically requested connection that can't be resolved is a broken connection, not a\n\t\t\t// provider with no orgs — surface it (warning + fetchFailed) instead of dropping the id\n\t\t\t// silently, so a caller can tell it apart from an account that genuinely has no orgs.\n\t\t\tconst early = ctx.earlyReturnConnectionWarnings(id, connectionId, requestedDomain);\n\t\t\treturn {\n\t\t\t\titems: [] as ProviderOrganization[],\n\t\t\t\twarnings: early.warnings,\n\t\t\t\tfetchFailed: early.fetchFailed,\n\t\t\t};\n\t\t}\n\n\t\tconst items: ProviderOrganization[] = [];\n\t\tconst warnings: ProviderWarning[] = [];\n\t\tlet fetchFailed = false;\n\t\tconst domain = ctx.domainForRead(integration, id, connectionId, requestedDomain);\n\t\tconst warnOnMissingSession = warnOnMissingSessionForDomain(id, requestedDomain);\n\t\tif (isIssuesIntegration(integration)) {\n\t\t\t// Issue trackers expose \"resources\" (Jira sites, Linear orgs, …) as their org analogue.\n\t\t\tconst { value: resources, warning } = await runCaptured(\n\t\t\t\tid,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t() => integration.getResourcesForUserResult(connectionId),\n\t\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t\t);\n\t\t\tif (resources != null) {\n\t\t\t\titems.push(...resources.map(r => resourceToOrg(id, r)));\n\t\t\t}\n\t\t\tif (warning != null) {\n\t\t\t\twarnings.push(warning);\n\t\t\t\t// A warning with no value is a hard read failure, not an empty account.\n\t\t\t\tif (resources == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (!integration.supportsOrganizationDiscovery) {\n\t\t\t// The provider registers no org-discovery hook. Report it as\n\t\t\t// explicitly unsupported rather than contributing a silent empty list that a caller can't\n\t\t\t// tell apart from \"this account has no orgs\".\n\t\t\tfetchFailed = true;\n\t\t\twarnings.push(\n\t\t\t\tunsupportedWarning(id, domain, connectionId, `Organization discovery is not supported by '${id}'.`),\n\t\t\t);\n\t\t} else {\n\t\t\tconst { value, warning } = await runCaptured(\n\t\t\t\tid,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t() => integration.getOrganizationsForUserResult(connectionId),\n\t\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t\t);\n\t\t\tif (value != null) {\n\t\t\t\titems.push(...value.values.map(org => withProviderContext(id, org)));\n\n\t\t\t\tif (mergeHierarchyIncompleteness(warnings, id, domain, connectionId, value, 'Organization')) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (warning != null) {\n\t\t\t\twarnings.push(warning);\n\t\t\t\tif (value == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed };\n\t});\n\n\treturn mergeHierarchyResults(results);\n}\n\nexport async function listProjects(\n\tctx: ProviderReadContext,\n\toptions?: ListProjectsOptions,\n): Promise<ProviderResult<ProviderOrganization>> {\n\tassertHierarchyReadTarget('listProjects', options);\n\tconst ids =\n\t\toptions?.providerId != null\n\t\t\t? [options.providerId]\n\t\t\t: [\n\t\t\t\t\t...supportedOrderedCloudIssuesIntegrationIds,\n\t\t\t\t\tGitCloudHostIntegrationId.AzureDevOps,\n\t\t\t\t\tGitSelfManagedHostIntegrationId.AzureDevOpsServer,\n\t\t\t\t];\n\tconst singleProvider = ids.length === 1;\n\tconst connectionId = singleProvider ? options?.connectionId : undefined;\n\tconst requestedDomain = singleProvider ? options?.domain : undefined;\n\n\tconst results = await mapBounded(ids, providerFanOutConcurrency, async id => {\n\t\tconst integration = await ctx.getIntegrationForRead(id, connectionId, requestedDomain);\n\t\tif (integration == null) {\n\t\t\t// A requested connection that can't be resolved is a broken connection, not a provider with\n\t\t\t// no projects — surface it (warning + fetchFailed) instead of dropping the id silently.\n\t\t\tconst early = ctx.earlyReturnConnectionWarnings(id, connectionId, requestedDomain);\n\t\t\treturn {\n\t\t\t\titems: [] as ProviderOrganization[],\n\t\t\t\twarnings: early.warnings,\n\t\t\t\tfetchFailed: early.fetchFailed,\n\t\t\t};\n\t\t}\n\n\t\tconst items: ProviderOrganization[] = [];\n\t\tconst warnings: ProviderWarning[] = [];\n\t\tlet fetchFailed = false;\n\t\tconst domain = ctx.domainForRead(integration, id, connectionId, requestedDomain);\n\t\tconst org = options?.org;\n\t\tconst warnOnMissingSession = warnOnMissingSessionForDomain(id, requestedDomain);\n\n\t\t// Git hosts with a project tier (Azure DevOps) read projects through their own hierarchy hook,\n\t\t// scoped to `org` when given. Non-Azure git hosts have no project tier and return undefined.\n\t\tif (!isIssuesIntegration(integration)) {\n\t\t\t// Check the capability first, like listOrgs/listRepos/listIssuesPage do. Without it, a host\n\t\t\t// with no project tier returns `undefined` from a successful read, which `runCaptured` can't\n\t\t\t// tell apart from an unresolvable session — turning a healthy provider into a `no-connection`\n\t\t\t// warning + `fetchFailed` (and, in Kepler, a spurious reconnect prompt).\n\t\t\tif (!integration.supportsProjectDiscovery) {\n\t\t\t\treturn { items: items, warnings: warnings, fetchFailed: false };\n\t\t\t}\n\n\t\t\tconst { value: projects, warning } = await runCaptured(\n\t\t\t\tid,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t() => integration.getProjectsForOrgResult(org, connectionId),\n\t\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t\t);\n\t\t\tif (warning != null) {\n\t\t\t\twarnings.push(warning);\n\t\t\t\tif (projects == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (projects != null) {\n\t\t\t\titems.push(...projects.values.map(project => withProviderContext(id, project)));\n\n\t\t\t\tif (mergeHierarchyIncompleteness(warnings, id, domain, connectionId, projects, 'Project')) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed };\n\t\t}\n\n\t\tconst { value: resources, warning: resourcesWarning } = await runCaptured(\n\t\t\tid,\n\t\t\tdomain,\n\t\t\tconnectionId,\n\t\t\t() => integration.getResourcesForUserResult(connectionId),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t);\n\t\tif (resourcesWarning != null) {\n\t\t\twarnings.push(resourcesWarning);\n\t\t\tif (resources == null) {\n\t\t\t\tfetchFailed = true;\n\t\t\t}\n\t\t}\n\n\t\tconst scopedResources =\n\t\t\torg != null ? resources?.filter(resource => resourceMatchesOrg(resource, org)) : resources;\n\t\tif (scopedResources != null && scopedResources.length !== 0) {\n\t\t\tconst { value: projects, warning: projectsWarning } = await runCaptured(id, domain, connectionId, () =>\n\t\t\t\tintegration.getProjectsForResourcesWithMetadataResult(scopedResources, connectionId),\n\t\t\t);\n\t\t\tif (projectsWarning != null) {\n\t\t\t\twarnings.push(projectsWarning);\n\t\t\t\tif (projects == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (projects != null) {\n\t\t\t\titems.push(\n\t\t\t\t\t...projects.values.map(project =>\n\t\t\t\t\t\tresourceToOrg(id, project, orgForProject(id, project, scopedResources)),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, projects?.metadata);\n\t\t\tif (assessment.fetchFailed || assessment.truncated) {\n\t\t\t\tfetchFailed = true;\n\t\t\t}\n\t\t}\n\n\t\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed };\n\t});\n\n\treturn mergeHierarchyResults(results);\n}\n\nexport async function listRepos(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\torg?: string;\n\t\tproject?: string;\n\t\tpage?: number;\n\t\tcursor?: string;\n\t\titemsPerPage?: number;\n\t\tconnectionId?: string;\n\t\tdomain?: string;\n\t},\n): Promise<ProviderPagedResult<ProviderRepositoryShape>> {\n\t// Truncated as well as floored, like every other paged read here: `page` is reported back as\n\t// `page.currentPage` and bounds the cursor walk below, and neither can be fractional.\n\tconst page = Math.max(1, Math.trunc(options.page ?? 1));\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'repository discovery')],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\t// A supplied connection or domain that no longer resolves is a broken target, not an empty account —\n\t\t// surface a no-connection warning + fetchFailed rather than a silent empty page.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\treturn refusedPage(page, early.warnings, early.fetchFailed);\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'repository discovery')],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\n\tconst accountWide = options.org == null;\n\tconst supported = accountWide\n\t\t? integration.supportsUserRepositoryDiscovery\n\t\t: integration.supportsRepositoryDiscovery;\n\tif (!supported) {\n\t\t// No matching repo-discovery hook — org-scoped (e.g. Bitbucket Data Center) or account-wide (e.g.\n\t\t// Bitbucket/Azure, whose repos can only be walked per workspace/org). Report unsupported rather than\n\t\t// a silent empty page indistinguishable from \"no repos\"; for the account-wide case the caller should\n\t\t// fan out per org from listOrgs instead.\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[\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\taccountWide\n\t\t\t\t\t\t? `Account-wide repository discovery is not supported by '${options.providerId}'; list repositories per org instead.`\n\t\t\t\t\t\t: `Repository discovery is not supported by '${options.providerId}'.`,\n\t\t\t\t),\n\t\t\t],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst org = options.org;\n\tconst warnOnMissingSession = warnOnMissingSessionForDomain(options.providerId, options.domain);\n\tconst readPage = (cursor: string | undefined) =>\n\t\trunCaptured(\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\t() =>\n\t\t\t\torg == null\n\t\t\t\t\t? integration.getRepositoriesForUserResult({ cursor: cursor, connectionId: options.connectionId })\n\t\t\t\t\t: integration.getRepositoriesForOrgResult(org, {\n\t\t\t\t\t\t\tproject: options.project,\n\t\t\t\t\t\t\tcursor: cursor,\n\t\t\t\t\t\t\tconnectionId: options.connectionId,\n\t\t\t\t\t\t}),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t);\n\n\t/**\n\t * Builds one page's result from what the provider returned. Shared by the initial read and by every round of\n\t * the cursor walk below, so a walked page N is described by exactly the same rules as a directly-requested one\n\t * (position, continuation, truncation) rather than by a second copy of them.\n\t */\n\tconst toResult = (\n\t\t{ value, warning }: Awaited<ReturnType<typeof readPage>>,\n\t\trequestedPage: number,\n\t\tsuppliedCursor: string | undefined,\n\t): ProviderPagedResult<ProviderRepositoryShape> => {\n\t\tconst items = value?.values ?? [];\n\t\tconst warnings = warning != null ? [warning] : [];\n\t\t// The repos read core is cursor-only and can't accept a page size, so don't echo the requested\n\t\t// `itemsPerPage` as if it were applied — report what the provider returned (its own pageSize when\n\t\t// available, else the actual item count).\n\t\tconst paged = toProviderPageInfo(items.length, value?.paging);\n\t\t// Convert the SDK collection metadata into scope-aware warnings + failure/truncation flags, appending\n\t\t// them to any captured thrown-error warning without discarding the partial result's items.\n\t\tconst assessment = mergeAssessmentInto(\n\t\t\twarnings,\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\tvalue?.metadata,\n\t\t);\n\t\t// Whether this read honors a page NUMBER, decided from what the provider actually reported rather than\n\t\t// guessed from the absence of a cursor. A numbered-page repos host reports its position (`paging.page`)\n\t\t// and/or its successor (`paging.nextPage`) — Bitbucket's workspace walk consumes `page` as a real 1-based\n\t\t// page and reports `nextPage` without echoing `currentPage`, so both signals have to count. Every wired\n\t\t// cursor-based repos read (GitHub's org/user walks, GitLab's user walk, Bitbucket's cursor read) reports\n\t\t// neither. Keying off `paged.cursor == null` instead read the paging layer's \"reported another page,\n\t\t// handed back no continuation\" sentinel as evidence of a numbered host, which is exactly backwards.\n\t\tconst pageAdvanceable = value?.paging?.page != null || value?.paging?.nextPage != null;\n\t\t// Numbered-page hosts that don't echo `currentPage` may still be advanced by the requested `page` (initial\n\t\t// read) or by the cursor the caller threaded back. A cursor-only host ignores an UNTHREADED `page`\n\t\t// request, so its page-less first page is page 1.\n\t\tconst currentPage = resolveCurrentPage({\n\t\t\tproviderPage: paged.page.currentPage,\n\t\t\trequestedPage: requestedPage,\n\t\t\tsuppliedCursor: suppliedCursor,\n\t\t\tpageAdvanceable: pageAdvanceable,\n\t\t});\n\t\t// Never advertise `hasMore` without a continuation the caller can act on — the same contract\n\t\t// `listPullRequestsPage`, `listIssuesPage` and `broadenIssues` hold, via the same helper. A provider that\n\t\t// reports another page but hands back neither `endCursor` nor `nextPage` (surfaced as the `'{}'` sentinel,\n\t\t// which `toProviderPageInfo` drops) is terminal-but-incomplete: `hasMore: false` + `page.truncated`.\n\t\t// Synthesizing a page-number cursor for a cursor-only host instead — which is what this read used to do\n\t\t// unconditionally — handed back a continuation the provider ignores, so it answered with its FIRST page\n\t\t// again while still reporting `hasMore: true`. A consumer draining until `hasMore` clears (Kepler's repo\n\t\t// drain does exactly that) then looped to its own page cap, accumulating a duplicate copy of every repo\n\t\t// per round, and never saw a truncation signal.\n\t\tconst continuation = resolveContinuation(paged, pageAdvanceable ? currentPage + 1 : undefined);\n\t\t// The org-hierarchy read can also stop at a defensive backstop with more repos unlisted and no cursor to\n\t\t// resume (top-level `truncated`, or `paging.truncated` on a single-page read). Metadata incompleteness and\n\t\t// a demoted continuation are two further, independent sources of the same signal.\n\t\tconst truncated =\n\t\t\t(value?.truncated ?? false) ||\n\t\t\t(value?.paging?.truncated ?? false) ||\n\t\t\tassessment.truncated ||\n\t\t\tcontinuation.truncated;\n\t\treturn {\n\t\t\t// Normalize the raw provider-apis repos to the GitLens-owned shape at the surface boundary.\n\t\t\titems: items.map(toProviderRepositoryShape),\n\t\t\twarnings: warnings,\n\t\t\tpage: { ...paged.page, currentPage: currentPage, truncated: truncated || undefined },\n\t\t\thasMore: continuation.hasMore,\n\t\t\tcursor: continuation.cursor,\n\t\t\tfetchFailed: assessment.fetchFailed || (warning != null && value == null) || undefined,\n\t\t};\n\t};\n\n\tconst first = await readPage(\n\t\toptions.cursor ??\n\t\t\t(integration.id === GitSelfManagedHostIntegrationId.BitbucketServer ? undefined : pageToCursor(page)),\n\t);\n\tconst result = toResult(first, page, options.cursor);\n\t// A numbered-page host honored the synthesized cursor, so this IS page N; a cursor-only host ignored it and\n\t// answered with page 1, which the walk below advances. Decided from the provider's own paging metadata rather\n\t// than assumed, hence after the read.\n\tconst pageAdvanceable = first.value?.paging?.page != null || first.value?.paging?.nextPage != null;\n\tif (options.cursor != null || page === 1 || pageAdvanceable) return result;\n\n\t// Walk the provider's real continuations to the requested page. Only the last page's items are kept — pages\n\t// 1..N returned as \"page N\" would duplicate items for a normal paged consumer — while warnings, truncation,\n\t// and failure accumulate across the whole walked prefix, since an earlier round's problem still means this\n\t// result isn't authoritative. The provider read is re-issued per round, but the integration is resolved once:\n\t// re-entering this function per page would repeat its auth resolution N times for page N.\n\tconst walkedWarnings = [...result.warnings];\n\tlet walkedFetchFailed = result.fetchFailed === true;\n\tlet walkedTruncated = result.page.truncated === true;\n\tlet previous = result;\n\tfor (let currentPage = 2; currentPage <= page; currentPage++) {\n\t\tif (!previous.hasMore || previous.cursor == null) {\n\t\t\t// The requested page lies past the provider's terminal cursor: an EMPTY page N, never the last\n\t\t\t// available page relabeled (see ProviderPageInfo.currentPage).\n\t\t\treturn {\n\t\t\t\titems: [],\n\t\t\t\twarnings: walkedWarnings,\n\t\t\t\tpage: { currentPage: page, itemsPerPage: 0, truncated: walkedTruncated || undefined },\n\t\t\t\thasMore: false,\n\t\t\t\tfetchFailed: walkedFetchFailed || undefined,\n\t\t\t};\n\t\t}\n\n\t\tconst suppliedCursor = previous.cursor;\n\t\tconst requested = toResult(await readPage(suppliedCursor), currentPage, suppliedCursor);\n\t\tfor (const warning of requested.warnings) {\n\t\t\tappendDedupedWarning(walkedWarnings, warning);\n\t\t}\n\t\twalkedFetchFailed ||= requested.fetchFailed === true;\n\t\twalkedTruncated ||= requested.page.truncated === true;\n\t\tif (currentPage === page) {\n\t\t\treturn {\n\t\t\t\t...requested,\n\t\t\t\twarnings: walkedWarnings,\n\t\t\t\tpage: { ...requested.page, truncated: walkedTruncated || undefined },\n\t\t\t\tfetchFailed: walkedFetchFailed || undefined,\n\t\t\t};\n\t\t}\n\n\t\tprevious = requested;\n\t}\n\n\treturn result;\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { IssueShape } from "../../../git/models/issue.js";
|
|
2
2
|
import { IntegrationIds } from "../constants.js";
|
|
3
|
-
import { ProviderResult
|
|
4
|
-
import {
|
|
3
|
+
import { ProviderResult } from "../results.js";
|
|
4
|
+
import { IssueEtagInclude } from "../models/integration.js";
|
|
5
5
|
import { ProviderReadContext } from "./context.js";
|
|
6
6
|
//#region ../plus/integrations/src/reads/issueBatch.d.ts
|
|
7
7
|
/**
|
|
@@ -21,10 +21,15 @@ import { ProviderReadContext } from "./context.js";
|
|
|
21
21
|
* Modeled on `countIssues` for its shape — caller-owned `key` echoed back, per-target isolation, as few requests
|
|
22
22
|
* as the provider allows — because both take a set of independent questions and answer them together.
|
|
23
23
|
*
|
|
24
|
-
* ONE integration call per invocation
|
|
25
|
-
* fans its targets out (chunked for GitHub/GHE, one request per target with
|
|
26
|
-
* Azure DevOps and on the trackers) and settles each independently, so failing
|
|
27
|
-
* of the integration's failure budget.
|
|
24
|
+
* ONE integration call per invocation when no target carries an `etag`, whatever the target count, as in
|
|
25
|
+
* `getPullRequestsBatch`: the integration fans its targets out (chunked for GitHub/GHE, one request per target with
|
|
26
|
+
* bounded concurrency on GitLab and Azure DevOps and on the trackers) and settles each independently, so failing
|
|
27
|
+
* targets spend at most one strike of the integration's failure budget.
|
|
28
|
+
*
|
|
29
|
+
* With etags, up to THREE on a host with a cheap check (GitHub/GHE, GitLab and Azure DevOps; Jira Cloud and Linear
|
|
30
|
+
* among the trackers): the cheap check of the targets that carry one, a full read of the rest started alongside it,
|
|
31
|
+
* and a full read of the targets whose etag no longer matches — see `readEtaggedBatch`. Every other tracker still makes
|
|
32
|
+
* one call and etags its rows.
|
|
28
33
|
*/
|
|
29
34
|
/** One issue to resolve, echoed back under the caller's own `key`. Which form a call takes depends on its provider. */
|
|
30
35
|
export type IssueBatchTarget =
|
|
@@ -45,6 +50,8 @@ export type IssueBatchTarget =
|
|
|
45
50
|
number: number;
|
|
46
51
|
/** Azure DevOps project. Required there, ignored elsewhere. */
|
|
47
52
|
project?: string;
|
|
53
|
+
/** The {@link IssueBatchResult.etag} of the copy the caller holds; see {@link IssueBatchResult}. */
|
|
54
|
+
etag?: string;
|
|
48
55
|
} |
|
|
49
56
|
/**
|
|
50
57
|
* An issue tracker's issue, by its own identifier (e.g. `ABC-123`) within a resource. Jira (Cloud and Data
|
|
@@ -57,12 +64,26 @@ export type IssueBatchTarget =
|
|
|
57
64
|
resourceId: string;
|
|
58
65
|
resourceUrl?: string;
|
|
59
66
|
identifier: string;
|
|
67
|
+
/**
|
|
68
|
+
* The {@link IssueBatchResult.etag} of the copy the caller holds. Checked cheaply on Jira Cloud and Linear;
|
|
69
|
+
* every other tracker accepts it but reads the issue in full.
|
|
70
|
+
*/
|
|
71
|
+
etag?: string;
|
|
60
72
|
};
|
|
61
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* The answer for one {@link IssueBatchTarget}, in one of four states:
|
|
75
|
+
* - `{ key, issue, etag }` — read in full. Every fully read row carries an `etag`, whether or not the caller sent
|
|
76
|
+
* one, so a caller can seed its etags from the reads it already makes.
|
|
77
|
+
* - `{ key, unchanged: true, etag }` — the cheap check proved the caller's copy current, and nothing else was
|
|
78
|
+
* fetched. Only for a target that sent an `etag`, on a host with a cheap check.
|
|
79
|
+
* - `{ key }` — PROVEN ABSENT.
|
|
80
|
+
* - no row, with `fetchFailed` and a warning — the read could not check. Never treat that as absent.
|
|
81
|
+
*/
|
|
62
82
|
export interface IssueBatchResult {
|
|
63
83
|
key: string;
|
|
64
84
|
/**
|
|
65
|
-
* The resolved issue, or `undefined` when it PROVABLY does not exist (or is not visible to this connection)
|
|
85
|
+
* The resolved issue, or `undefined` when it PROVABLY does not exist (or is not visible to this connection) —
|
|
86
|
+
* or when it is `unchanged`.
|
|
66
87
|
*
|
|
67
88
|
* Absent is an answer here, unlike every paged read on this facade: a target that FAILED — its own request
|
|
68
89
|
* outright, or just its own alias within an otherwise-answering GitHub chunk (e.g. an org enforcing SAML SSO
|
|
@@ -70,6 +91,16 @@ export interface IssueBatchResult {
|
|
|
70
91
|
* "proven absent" from "unknown" and cache the first without ever caching the second.
|
|
71
92
|
*/
|
|
72
93
|
issue?: IssueShape;
|
|
94
|
+
/**
|
|
95
|
+
* Opaque: compare for equality only, never parse. Computed by core from the issue's change state — its state and
|
|
96
|
+
* update time, plus each input the call's `etagIncludes` listed — and NOT the provider's HTTP ETag. Send it back
|
|
97
|
+
* as the target's `etag`. An etag from another scheme or another `etagIncludes` set simply compares unequal and
|
|
98
|
+
* costs a full read, never a false `unchanged`. A host can add a reaction without moving the update time, so
|
|
99
|
+
* unless `'reactions'` is listed, an `unchanged` copy's `thumbsUpCount` may be stale.
|
|
100
|
+
*/
|
|
101
|
+
etag?: string;
|
|
102
|
+
/** The caller's copy is current (its `etag` matched); `issue` is absent because nothing was read. */
|
|
103
|
+
unchanged?: true;
|
|
73
104
|
}
|
|
74
105
|
export declare function getIssuesBatch(ctx: ProviderReadContext, options: {
|
|
75
106
|
providerId: IntegrationIds;
|
|
@@ -80,6 +111,14 @@ export declare function getIssuesBatch(ctx: ProviderReadContext, options: {
|
|
|
80
111
|
* it must come from the trusted authentication configuration, not repository or remote data.
|
|
81
112
|
*/
|
|
82
113
|
domain?: string;
|
|
114
|
+
/**
|
|
115
|
+
* Widens every etag to the listed inputs, each of which a host changes without moving the issue's update
|
|
116
|
+
* time, and each of which costs its own fields in the cheap check. `'reactions'` is the thumbs-up count; it
|
|
117
|
+
* widens nothing on a host whose rows carry no real count (Azure DevOps work items and the trackers). Order
|
|
118
|
+
* and repeats don't matter. An unknown value refuses the whole call. Changes only which etag is computed; the
|
|
119
|
+
* full read is the same. Calls that differ in this set never match each other's etags.
|
|
120
|
+
*/
|
|
121
|
+
etagIncludes?: readonly IssueEtagInclude[];
|
|
83
122
|
}): Promise<ProviderResult<IssueBatchResult>>;
|
|
84
123
|
/**
|
|
85
124
|
* Trims `owner`, `repo` and `project` once, shared by every coordinate-form batch read (this one, the pull request
|
|
@@ -98,14 +137,6 @@ export declare function trimCoordinateFields<T extends {
|
|
|
98
137
|
* gives — either way `fetchFailed`, so no caller mistakes the dropped targets for an answer.
|
|
99
138
|
*/
|
|
100
139
|
export declare function unresolvedIntegration<T>(ctx: ProviderReadContext, providerId: IntegrationIds, connectionId: string | undefined, domain: string | undefined): ProviderResult<T>;
|
|
101
|
-
/**
|
|
102
|
-
* Appends the warning for a batch target that could not be checked, shared by every batch read: for a refusal, the
|
|
103
|
-
* scope failure it was recorded as (see `IntegrationBase.settleBatchRefusals`), published as an account-wide read
|
|
104
|
-
* publishes one, scoped and with its cause; otherwise, the reason itself.
|
|
105
|
-
*/
|
|
106
|
-
export declare function appendBatchSlotWarning(warnings: ProviderWarning[], providerId: IntegrationIds, domain: string | undefined, connectionId: string | undefined, slot: Extract<BatchSlot<unknown>, {
|
|
107
|
-
status: 'rejected';
|
|
108
|
-
}>): void;
|
|
109
140
|
export declare function findDuplicateKey(targets: readonly {
|
|
110
141
|
key: string;
|
|
111
142
|
}[]): string | undefined;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import "../constants.js";
|
|
2
2
|
import { githubGraphQLInt32Max, isAzureProviderId, isGitHubProviderId } from "../providers/providerErrors.js";
|
|
3
|
-
import { appendDedupedWarning, toProviderWarning } from "../results.js";
|
|
4
|
-
import { mergeAssessmentInto } from "../collectionMetadata.js";
|
|
5
3
|
import { areDomainsOnSameHost, hostFromDomain } from "../utils/domain.utils.js";
|
|
6
4
|
import { isGitHostIntegration, isIssuesHostIntegrationId, isIssuesSelfManagedHostIntegrationId } from "../utils/integration.utils.js";
|
|
7
|
-
import { gitHostOnlySurfaceWarning, issueTrackerOnlySurfaceWarning, issuesUnsupportedWarning, noConnectionWarning, otherWarning } from "./warnings.js";
|
|
5
|
+
import { gitHostOnlySurfaceWarning, issueTrackerOnlySurfaceWarning, issuesUnsupportedWarning, noConnectionWarning, otherWarning, unsupportedWarning } from "./warnings.js";
|
|
8
6
|
import { runCaptured } from "./drains.js";
|
|
7
|
+
import { issueEtagIncludes } from "../models/integration.js";
|
|
9
8
|
import { isIssuesIntegration } from "../models/issuesIntegration.js";
|
|
9
|
+
import { readEtaggedBatch } from "./batchEtags.js";
|
|
10
|
+
import { findInvalidIssueEtagInclude, issueEtag, issueEtagFieldsFromShape, normalizeIssueEtagIncludes } from "./etag.js";
|
|
10
11
|
//#region ../plus/integrations/src/reads/issueBatch.ts
|
|
11
12
|
const surface = "Batch issue resolution";
|
|
12
13
|
function refused(warning) {
|
|
@@ -23,7 +24,10 @@ async function getIssuesBatch(ctx, options) {
|
|
|
23
24
|
};
|
|
24
25
|
const duplicateKey = findDuplicateKey(options.targets);
|
|
25
26
|
if (duplicateKey != null) return refused(otherWarning(options.providerId, void 0, options.connectionId, `Duplicate issue batch target key '${duplicateKey}'; keys identify results, so each must be unique.`));
|
|
26
|
-
|
|
27
|
+
const invalidInclude = findInvalidIssueEtagInclude(options.etagIncludes ?? []);
|
|
28
|
+
if (invalidInclude != null) return refused(otherWarning(options.providerId, void 0, options.connectionId, `Unknown issue etag include '${invalidInclude}'; expected one of ${issueEtagIncludes.map((i) => `'${i}'`).join(", ")}.`));
|
|
29
|
+
const etagIncludes = normalizeIssueEtagIncludes(options.etagIncludes ?? []);
|
|
30
|
+
if (isIssuesHostIntegrationId(options.providerId)) return getIssuesBatchForTracker(ctx, options.providerId, options.targets, options.connectionId, options.domain, etagIncludes);
|
|
27
31
|
const targets = options.targets;
|
|
28
32
|
if (!targets.every(isCoordinateTarget)) return refused(otherWarning(options.providerId, void 0, options.connectionId, `${surface} for '${options.providerId}' takes repository coordinates { key, owner, repo, number, project? }, not tracker identifiers.`));
|
|
29
33
|
const trimmedTargets = targets.map(trimCoordinateFields);
|
|
@@ -34,47 +38,31 @@ async function getIssuesBatch(ctx, options) {
|
|
|
34
38
|
if (!isGitHostIntegration(integration)) return refused(gitHostOnlySurfaceWarning(options.providerId, void 0, options.connectionId, surface));
|
|
35
39
|
const domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);
|
|
36
40
|
if (!integration.supportsIssues) return refused(issuesUnsupportedWarning(options.providerId, domain, options.connectionId));
|
|
37
|
-
const
|
|
38
|
-
let fetchFailed = false;
|
|
39
|
-
const { value: slots, warning } = await runCaptured(options.providerId, domain, options.connectionId, () => integration.getIssuesBatchResult(trimmedTargets.map((t) => ({
|
|
41
|
+
const toCoordinate = (t) => ({
|
|
40
42
|
owner: t.owner,
|
|
41
43
|
repo: t.repo,
|
|
42
44
|
number: t.number,
|
|
43
45
|
project: t.project
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (slot.status === "rejected") {
|
|
58
|
-
appendBatchSlotWarning(warnings, options.providerId, domain, options.connectionId, slot);
|
|
59
|
-
fetchFailed = true;
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
const issue = slot.value;
|
|
63
|
-
items.push({
|
|
64
|
-
key: trimmedTargets[i].key,
|
|
65
|
-
...issue != null ? { issue } : {}
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
return {
|
|
69
|
-
items,
|
|
70
|
-
warnings,
|
|
71
|
-
fetchFailed: fetchFailed || void 0
|
|
72
|
-
};
|
|
46
|
+
});
|
|
47
|
+
return toIssueBatchResult(await readEtaggedBatch({
|
|
48
|
+
providerId: options.providerId,
|
|
49
|
+
domain,
|
|
50
|
+
connectionId: options.connectionId,
|
|
51
|
+
targets: trimmedTargets,
|
|
52
|
+
supportsEtags: integration.supportsIssueEtags,
|
|
53
|
+
readFull: (batch) => runCaptured(options.providerId, domain, options.connectionId, () => integration.getIssuesBatchResult(batch.map(toCoordinate), void 0, options.connectionId), { warnOnMissingSession: true }),
|
|
54
|
+
readEtagFields: (batch) => runCaptured(options.providerId, domain, options.connectionId, () => integration.getIssuesEtagFieldsResult(batch.map(toCoordinate), { etagIncludes }, void 0, options.connectionId), { warnOnMissingSession: true }),
|
|
55
|
+
itemEtag: (issue) => issueEtag(issueEtagFieldsFromShape(issue), etagIncludes),
|
|
56
|
+
fieldsEtag: (fields) => issueEtag(fields, etagIncludes),
|
|
57
|
+
unsupportedWarning: () => unsupportedWarning(options.providerId, domain, options.connectionId, `${surface} is not supported by '${options.providerId}'; resolve issues individually instead.`)
|
|
58
|
+
}));
|
|
73
59
|
}
|
|
74
60
|
/**
|
|
75
|
-
* The tracker form. ONE integration call per invocation
|
|
76
|
-
* single-issue request per target, with bounded concurrency, and settles each independently
|
|
77
|
-
* `IssuesIntegration.getIssuesByResourceIdBatchResult`.
|
|
61
|
+
* The tracker form. ONE integration call per invocation when no target carries an `etag`, whatever the target count:
|
|
62
|
+
* the integration makes one single-issue request per target, with bounded concurrency, and settles each independently
|
|
63
|
+
* — see `IssuesIntegration.getIssuesByResourceIdBatchResult`. With etags, a tracker with a cheap check (Jira Cloud's
|
|
64
|
+
* bulk fetch, Linear's per-team query) takes the coordinate form's `readEtaggedBatch` flow; every other tracker
|
|
65
|
+
* ignores a target's `etag` and reads and etags every found row in full.
|
|
78
66
|
*
|
|
79
67
|
* `resourceId` is trusted and the read does no resource discovery. It is handed to the provider's by-resource-id
|
|
80
68
|
* read as-is, never wrapped into a synthesized descriptor: Linear and Trello answer `undefined` for a descriptor
|
|
@@ -87,7 +75,7 @@ async function getIssuesBatch(ctx, options) {
|
|
|
87
75
|
* issue the same project and issue keys, and `issue: undefined` is a proven absence a caller may cache — an answer
|
|
88
76
|
* from whichever host happens to be primary would be cached under a key that names a different instance (#5872).
|
|
89
77
|
*/
|
|
90
|
-
async function getIssuesBatchForTracker(ctx, providerId, targets, connectionId, domain) {
|
|
78
|
+
async function getIssuesBatchForTracker(ctx, providerId, targets, connectionId, domain, etagIncludes) {
|
|
91
79
|
if (!targets.every(isTrackerTarget)) return refused(otherWarning(providerId, void 0, connectionId, `${surface} for '${providerId}' takes tracker identifiers { key, resourceId, resourceUrl?, identifier }, not repository coordinates.`));
|
|
92
80
|
const trimmedTargets = targets.map(trimTrackerTarget);
|
|
93
81
|
const invalid = findInvalidTrackerTarget(providerId, trimmedTargets);
|
|
@@ -96,47 +84,42 @@ async function getIssuesBatchForTracker(ctx, providerId, targets, connectionId,
|
|
|
96
84
|
const integration = await ctx.getIntegrationForRead(providerId, connectionId, domain);
|
|
97
85
|
if (integration == null) return unresolvedIntegration(ctx, providerId, connectionId, domain);
|
|
98
86
|
if (!isIssuesIntegration(integration)) return refused(issueTrackerOnlySurfaceWarning(providerId, connectionId, surface));
|
|
99
|
-
const unsupported = () =>
|
|
87
|
+
const unsupported = () => unsupportedWarning(providerId, void 0, connectionId, `${surface} is not supported by '${providerId}'; its single-issue read cannot prove an absence, so a miss would not be safe to cache.`);
|
|
100
88
|
if (!integration.supportsIssueLookupByResourceId) return refused(unsupported());
|
|
101
89
|
if (isIssuesSelfManagedHostIntegrationId(providerId)) {
|
|
102
90
|
const mismatched = trimmedTargets.find((t) => !areDomainsOnSameHost(t.resourceId, integration.domain));
|
|
103
91
|
if (mismatched != null) return refused(otherWarning(providerId, void 0, connectionId, `Issue batch target '${mismatched.key}' requires the resource id of '${providerId}' to name the host the read resolved to.`));
|
|
104
92
|
}
|
|
105
93
|
const resolvedDomain = ctx.domainForRead(integration, providerId, connectionId, domain);
|
|
106
|
-
const
|
|
107
|
-
let fetchFailed = false;
|
|
108
|
-
const { value: slots, warning } = await runCaptured(providerId, resolvedDomain, connectionId, () => integration.getIssuesByResourceIdBatchResult(trimmedTargets.map((t) => ({
|
|
94
|
+
const toResourceTarget = (t) => ({
|
|
109
95
|
resourceId: t.resourceId,
|
|
110
96
|
identifier: t.identifier,
|
|
111
97
|
resourceUrl: t.resourceUrl
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
const issue = slot.value;
|
|
131
|
-
items.push({
|
|
132
|
-
key: trimmedTargets[i].key,
|
|
133
|
-
...issue != null ? { issue } : {}
|
|
134
|
-
});
|
|
135
|
-
}
|
|
98
|
+
});
|
|
99
|
+
return toIssueBatchResult(await readEtaggedBatch({
|
|
100
|
+
providerId,
|
|
101
|
+
domain: resolvedDomain,
|
|
102
|
+
connectionId,
|
|
103
|
+
targets: trimmedTargets,
|
|
104
|
+
supportsEtags: integration.supportsIssueEtagsByResourceId,
|
|
105
|
+
readFull: (batch) => runCaptured(providerId, resolvedDomain, connectionId, () => integration.getIssuesByResourceIdBatchResult(batch.map(toResourceTarget), connectionId), { warnOnMissingSession: true }),
|
|
106
|
+
readEtagFields: (batch) => runCaptured(providerId, resolvedDomain, connectionId, () => integration.getIssuesEtagFieldsByResourceIdBatchResult(batch.map(toResourceTarget), { etagIncludes }, connectionId), { warnOnMissingSession: true }),
|
|
107
|
+
itemEtag: (issue) => issueEtag(issueEtagFieldsFromShape(issue), etagIncludes),
|
|
108
|
+
fieldsEtag: (fields) => issueEtag(fields, etagIncludes),
|
|
109
|
+
unsupportedWarning: unsupported
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
/** The etag flow's rows as this read's results, under the `issue` name the result type gives the item. */
|
|
113
|
+
function toIssueBatchResult(result) {
|
|
136
114
|
return {
|
|
137
|
-
items
|
|
138
|
-
|
|
139
|
-
|
|
115
|
+
items: result.items.map((row) => ({
|
|
116
|
+
key: row.key,
|
|
117
|
+
...row.value != null ? { issue: row.value } : {},
|
|
118
|
+
...row.unchanged ? { unchanged: true } : {},
|
|
119
|
+
...row.etag != null ? { etag: row.etag } : {}
|
|
120
|
+
})),
|
|
121
|
+
warnings: result.warnings,
|
|
122
|
+
fetchFailed: result.fetchFailed
|
|
140
123
|
};
|
|
141
124
|
}
|
|
142
125
|
function isCoordinateTarget(target) {
|
|
@@ -206,21 +189,6 @@ function unresolvedIntegration(ctx, providerId, connectionId, domain) {
|
|
|
206
189
|
fetchFailed: true
|
|
207
190
|
};
|
|
208
191
|
}
|
|
209
|
-
/**
|
|
210
|
-
* Appends the warning for a batch target that could not be checked, shared by every batch read: for a refusal, the
|
|
211
|
-
* scope failure it was recorded as (see `IntegrationBase.settleBatchRefusals`), published as an account-wide read
|
|
212
|
-
* publishes one, scoped and with its cause; otherwise, the reason itself.
|
|
213
|
-
*/
|
|
214
|
-
function appendBatchSlotWarning(warnings, providerId, domain, connectionId, slot) {
|
|
215
|
-
if (slot.failure != null) {
|
|
216
|
-
mergeAssessmentInto(warnings, providerId, domain, connectionId, {
|
|
217
|
-
completeness: "partial",
|
|
218
|
-
failures: [slot.failure]
|
|
219
|
-
});
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
appendDedupedWarning(warnings, toProviderWarning(providerId, domain, connectionId, slot.reason));
|
|
223
|
-
}
|
|
224
192
|
function findDuplicateKey(targets) {
|
|
225
193
|
const seen = /* @__PURE__ */ new Set();
|
|
226
194
|
for (const target of targets) {
|
|
@@ -229,6 +197,6 @@ function findDuplicateKey(targets) {
|
|
|
229
197
|
}
|
|
230
198
|
}
|
|
231
199
|
//#endregion
|
|
232
|
-
export {
|
|
200
|
+
export { findDuplicateKey, getIssuesBatch, trimCoordinateFields, unresolvedIntegration };
|
|
233
201
|
|
|
234
202
|
//# sourceMappingURL=issueBatch.js.map
|
|
@@ -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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"}
|