@gitkraken/core-gitlens 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/dist/plus/integrations/collectionMetadata.d.ts +7 -0
  3. package/dist/plus/integrations/collectionMetadata.js +10 -1
  4. package/dist/plus/integrations/collectionMetadata.js.map +1 -1
  5. package/dist/plus/integrations/errors.d.ts +5 -0
  6. package/dist/plus/integrations/errors.js +10 -1
  7. package/dist/plus/integrations/errors.js.map +1 -1
  8. package/dist/plus/integrations/index.d.ts +2 -2
  9. package/dist/plus/integrations/index.js.map +1 -1
  10. package/dist/plus/integrations/models/gitHostIntegration.d.ts +6 -2
  11. package/dist/plus/integrations/models/gitHostIntegration.js +37 -7
  12. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  13. package/dist/plus/integrations/models/issueReads.d.ts +1 -1
  14. package/dist/plus/integrations/models/issuesIntegration.d.ts +33 -3
  15. package/dist/plus/integrations/models/issuesIntegration.js +76 -5
  16. package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
  17. package/dist/plus/integrations/providers/azureDevOpsServer.d.ts +9 -1
  18. package/dist/plus/integrations/providers/azureDevOpsServer.js +17 -9
  19. package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -1
  20. package/dist/plus/integrations/providers/bitbucket-server.js +3 -2
  21. package/dist/plus/integrations/providers/bitbucket-server.js.map +1 -1
  22. package/dist/plus/integrations/providers/jira-server.d.ts +3 -0
  23. package/dist/plus/integrations/providers/jira-server.js +15 -7
  24. package/dist/plus/integrations/providers/jira-server.js.map +1 -1
  25. package/dist/plus/integrations/providers/jira.d.ts +8 -0
  26. package/dist/plus/integrations/providers/jira.js +88 -58
  27. package/dist/plus/integrations/providers/jira.js.map +1 -1
  28. package/dist/plus/integrations/providers/linear.d.ts +4 -0
  29. package/dist/plus/integrations/providers/linear.js +23 -2
  30. package/dist/plus/integrations/providers/linear.js.map +1 -1
  31. package/dist/plus/integrations/providers/providerErrors.d.ts +24 -0
  32. package/dist/plus/integrations/providers/providerErrors.js +41 -4
  33. package/dist/plus/integrations/providers/providerErrors.js.map +1 -1
  34. package/dist/plus/integrations/providers/providersApi.d.ts +1 -1
  35. package/dist/plus/integrations/providers/providersApi.js +15 -4
  36. package/dist/plus/integrations/providers/providersApi.js.map +1 -1
  37. package/dist/plus/integrations/providers/trello.d.ts +1 -0
  38. package/dist/plus/integrations/providers/trello.js +3 -0
  39. package/dist/plus/integrations/providers/trello.js.map +1 -1
  40. package/dist/plus/integrations/reads/cursors.js +23 -5
  41. package/dist/plus/integrations/reads/cursors.js.map +1 -1
  42. package/dist/plus/integrations/reads/drains.d.ts +16 -0
  43. package/dist/plus/integrations/reads/drains.js +19 -1
  44. package/dist/plus/integrations/reads/drains.js.map +1 -1
  45. package/dist/plus/integrations/reads/hierarchy.js +5 -3
  46. package/dist/plus/integrations/reads/hierarchy.js.map +1 -1
  47. package/dist/plus/integrations/reads/hierarchy.utils.d.ts +3 -2
  48. package/dist/plus/integrations/reads/hierarchy.utils.js +4 -7
  49. package/dist/plus/integrations/reads/hierarchy.utils.js.map +1 -1
  50. package/dist/plus/integrations/reads/issueTracker.js +71 -7
  51. package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
  52. package/dist/plus/integrations/reads/selectionConfirmation.d.ts +69 -0
  53. package/dist/plus/integrations/reads/selectionConfirmation.js +79 -0
  54. package/dist/plus/integrations/reads/selectionConfirmation.js.map +1 -0
  55. package/dist/plus/integrations/reads/warnings.d.ts +9 -3
  56. package/dist/plus/integrations/reads/warnings.js +19 -3
  57. package/dist/plus/integrations/reads/warnings.js.map +1 -1
  58. package/dist/plus/integrations/results.d.ts +30 -3
  59. package/dist/plus/integrations/results.js +2 -1
  60. package/dist/plus/integrations/results.js.map +1 -1
  61. package/docs/integrations.md +59 -6
  62. package/package.json +1 -1
@@ -1,12 +1,12 @@
1
1
  import { Logger as _logger } from "../../../utils/logger.js";
2
2
  import "../constants.js";
3
- import { isJiraMissingProjectError } from "./providerErrors.js";
3
+ import { isJiraServerMissingProjectError } from "./providerErrors.js";
4
4
  import { isIncompleteCollection, toCollectionScopeFailure } from "../collectionMetadata.js";
5
5
  import { toTokenWithInfo } from "../authentication/models.js";
6
6
  import { areDomainsOnSameHost, baseUrlFromDomain } from "../utils/domain.utils.js";
7
7
  import { mergeCollectionMetadata } from "./utils/providerPaging.js";
8
8
  import { providersMetadata, toAccount, toIssueShape, toProviderIssueStates } from "./models.js";
9
- import { IssuesIntegration, groupProjectIssuesSearches } from "../models/issuesIntegration.js";
9
+ import { IssuesIntegration, groupProjectIssuesSearches, toProjectsCollection } from "../models/issuesIntegration.js";
10
10
  import { DiscoveryCache } from "./utils/discoveryCache.js";
11
11
  import { drainJiraIssues, readUserScopedJiraIssues, searchJiraProjectIssues, summarizePageMetadata, toUniqueJiraIssueShapes, withFailureScope } from "./utils/jiraIssueDrains.js";
12
12
  import { JIRA_MAX_PROJECT_KEYS_PER_REQUEST } from "@gitkraken/provider-apis";
@@ -102,17 +102,25 @@ var JiraServerIntegration = class extends IssuesIntegration {
102
102
  const cached = this._projects.get(cacheKey);
103
103
  if (cached != null && !force) return cached;
104
104
  const generation = this._projects.generation;
105
+ const descriptors = await this.readProjects(session);
106
+ if (descriptors == null) return void 0;
107
+ this._projects.set(cacheKey, descriptors, { generation });
108
+ return descriptors;
109
+ }
110
+ async getProviderUncachedProjectsForResourcesWithMetadata(session, resources) {
111
+ if (resources.length === 0) return { values: [] };
112
+ return toProjectsCollection(await this.readProjects(session));
113
+ }
114
+ async readProjects(session) {
105
115
  const projects = await (await this.getProvidersApi()).getJiraServerProjects(toTokenWithInfo(this.id, session), this.apiBaseUrlFor(session));
106
116
  if (projects == null) return void 0;
107
117
  const resourceId = this.resourceFor(session).id;
108
- const descriptors = projects.map((p) => ({
118
+ return projects.map((p) => ({
109
119
  id: p.id,
110
120
  key: p.key?.trim() || p.id,
111
121
  name: p.name,
112
122
  resourceId
113
123
  }));
114
- this._projects.set(cacheKey, descriptors, { generation });
115
- return descriptors;
116
124
  }
117
125
  async getProviderIssuesForProject(session, project, options) {
118
126
  return (await this.getProviderIssuesForProjectWithTruncation(session, project, options))?.values;
@@ -133,11 +141,11 @@ var JiraServerIntegration = class extends IssuesIntegration {
133
141
  failureScope: {
134
142
  providerId: this.id,
135
143
  resourceId: project.resourceId,
136
- projectId: project.id
144
+ projectId: project.key
137
145
  },
138
146
  providerName: "Jira Server",
139
147
  isEmptyFailure: (ex) => {
140
- if (!isJiraMissingProjectError(ex)) return false;
148
+ if (!isJiraServerMissingProjectError(ex)) return false;
141
149
  _logger.warn(`Jira Server project '${project.name}' (${project.id}) no longer exists or is not visible`);
142
150
  this._projects.delete(this.projectsCacheKey(session));
143
151
  return true;
@@ -1 +1 @@
1
- {"version":3,"file":"jira-server.js","names":[],"sources":["../../../../../plus/integrations/src/providers/jira-server.ts"],"sourcesContent":["import type { CollectionMetadata } from '@gitkraken/provider-apis';\nimport { JIRA_MAX_PROJECT_KEYS_PER_REQUEST } from '@gitkraken/provider-apis';\nimport type { Account } from '@gitlens/git/models/author.js';\nimport type { Issue, IssueProjection, IssueShape } from '@gitlens/git/models/issue.js';\nimport type { IssueOrPullRequest } from '@gitlens/git/models/issueOrPullRequest.js';\nimport type { IssueResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport type { Emitter } from '@gitlens/utils/event.js';\nimport { Logger } from '@gitlens/utils/logger.js';\nimport type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider.js';\nimport type { IntegrationAuthenticationService } from '../authentication/integrationAuthenticationService.js';\nimport type { ProviderAuthenticationSession } from '../authentication/models.js';\nimport { toTokenWithInfo } from '../authentication/models.js';\nimport { isIncompleteCollection, toCollectionScopeFailure } from '../collectionMetadata.js';\nimport { IssuesSelfManagedHostIntegrationId } from '../constants.js';\nimport type { IntegrationServiceContext } from '../context.js';\nimport type { IntegrationConnectionChangeEvent } from '../integrationService.js';\nimport type { IntegrationKey } from '../models/integration.js';\nimport type {\n\tAccountWideIssuesResult,\n\tIssuesForProjectOptions,\n\tProjectIssuesDrain,\n\tProjectIssuesRequest,\n\tSearchMyIssuesOptions,\n} from '../models/issueReads.js';\nimport { groupProjectIssuesSearches, IssuesIntegration } from '../models/issuesIntegration.js';\nimport { areDomainsOnSameHost, baseUrlFromDomain } from '../utils/domain.utils.js';\nimport { providersMetadata, toAccount, toIssueShape, toProviderIssueStates } from './models.js';\nimport { isJiraMissingProjectError } from './providerErrors.js';\nimport type { ProvidersApi } from './providersApi.js';\nimport { DiscoveryCache } from './utils/discoveryCache.js';\nimport type { JiraDrainOptions, JiraIssuePageFetcher } from './utils/jiraIssueDrains.js';\nimport {\n\tdrainJiraIssues,\n\treadUserScopedJiraIssues,\n\tsearchJiraProjectIssues,\n\tsummarizePageMetadata,\n\ttoUniqueJiraIssueShapes,\n\twithFailureScope,\n} from './utils/jiraIssueDrains.js';\nimport { mergeCollectionMetadata } from './utils/providerPaging.js';\n\nconst metadata = providersMetadata[IssuesSelfManagedHostIntegrationId.JiraServer];\nconst authProvider = Object.freeze({ id: metadata.id, scopes: metadata.scopes });\nconst maxPagesPerRequest = 10;\n\n/**\n * Every clause is scoped by the HANDLE, and `options.userId` is deliberately ignored here — the one\n * place this provider diverges from Jira Cloud's, which applies it to `assignee`/`creator` (#5857).\n *\n * The two are not interchangeable because `Account.id` means something different per deployment. On\n * Cloud it is the `accountId`, which JQL resolves, so scoping by it fixes the deactivated-account case\n * where the display name no longer resolves. On Server it is the user KEY (`JIRAUSER10224`), which JQL\n * does not resolve at all: measured against a live Jira Server 8.20.13 site, `assignee in\n * (\"JIRAUSER10224\")` returns 0 with `\"The value 'JIRAUSER10224' does not exist for the field\n * 'assignee'\"` — the identical warning a genuinely nonexistent user produces — while the username and\n * the display name both return the real 4.\n *\n * So applying `userId` here would silently empty the read rather than harden it. Do not \"fix\" this\n * asymmetry to match Cloud without re-measuring against a Server instance.\n *\n * The handle itself is already the right one: `provider-apis` 0.60.0 maps `Account.username` from the\n * Server payload's `name` (falling back to `displayName` when absent), so these clauses are scoped by the\n * real username rather than a display name (GKDEV-3629). That is what makes the `mention` clause work at\n * all — it is a free-text `comment ~ \"...\"` over comment bodies, and a Jira mention is stored as\n * `[~username]`, which a display name never matches.\n */\nfunction toUserIdentities(user: string): { userField: string; mention: string } {\n\treturn { userField: user, mention: user };\n}\n\n/**\n * The single resource a Jira Server connection has: the instance itself.\n *\n * Jira Cloud's account spans many sites, so its resources come from an API call that lists them. A\n * self-hosted instance IS the one site the connection addresses, so there is nothing to enumerate — this\n * descriptor is synthesized from the configured host. Modelling it as a resource anyway keeps the\n * resource → project contract of {@link IssuesIntegration} intact, so every tracker read (`listOrgs`,\n * `listProjects`, `listIssueTrackerIssuesPage`) works unchanged.\n */\nexport interface JiraServerResourceDescriptor extends IssueResourceDescriptor {\n\turl: string;\n}\n\nexport interface JiraServerProjectDescriptor extends IssueResourceDescriptor {\n\tresourceId: string;\n}\n\nexport class JiraServerIntegration extends IssuesIntegration<IssuesSelfManagedHostIntegrationId.JiraServer> {\n\treadonly authProvider: IntegrationAuthenticationProviderDescriptor = authProvider;\n\treadonly id = IssuesSelfManagedHostIntegrationId.JiraServer;\n\tprotected readonly key: IntegrationKey<IssuesSelfManagedHostIntegrationId.JiraServer>;\n\treadonly name: string = 'Jira Data Center';\n\n\tconstructor(\n\t\tctx: IntegrationServiceContext,\n\t\tauthenticationService: IntegrationAuthenticationService,\n\t\tgetProvidersApi: () => Promise<ProvidersApi>,\n\t\tdidChangeConnection: Emitter<IntegrationConnectionChangeEvent>,\n\t\tprivate readonly _domain: string,\n\t) {\n\t\tsuper(ctx, authenticationService, getProvidersApi, didChangeConnection);\n\t\tthis.key = `${this.id}:${this._domain}`;\n\t}\n\n\tget domain(): string {\n\t\treturn this._domain;\n\t}\n\n\t/**\n\t * The URL every request for this connection is addressed to, built from the SESSION's domain rather than\n\t * the integration's.\n\t *\n\t * `this.domain` is the host-keyed identity — `IntegrationService.get()` runs it through `hostFromDomain`\n\t * so one host can't build two instances — which means it has had any context path stripped, and so has\n\t * `session.domain`, which is stored as that same normalized host. Jira Data Center officially supports\n\t * being mounted below a context path (`https://jira.example.com/jira`), so the address comes from\n\t * {@link ProviderAuthenticationSession.baseUrl}, which carries the configured value verbatim; it falls\n\t * back to the host when the backend reported nothing more, which is the common case.\n\t *\n\t * Takes the session rather than reading `_session` so a per-connection (multi-account) read is addressed\n\t * to the host of the connection it resolved, not to whichever one happens to be current.\n\t */\n\tprotected apiBaseUrlFor(session: ProviderAuthenticationSession | undefined): string {\n\t\tconst configured = baseUrlFromDomain(session?.baseUrl, session?.protocol);\n\t\t// A `baseUrl` from another host is not this connection's address — refuse it rather than routing this\n\t\t// integration's reads somewhere its key never named.\n\t\tif (configured != null && areDomainsOnSameHost(session?.baseUrl, this.domain)) return configured;\n\n\t\treturn (\n\t\t\tbaseUrlFromDomain(session?.domain, session?.protocol) ??\n\t\t\tbaseUrlFromDomain(this.domain, session?.protocol) ??\n\t\t\t`${session?.protocol ?? 'https:'}//${this.domain}`\n\t\t);\n\t}\n\n\t/** The one resource this connection has — see {@link JiraServerResourceDescriptor}. */\n\tprivate resourceFor(session: ProviderAuthenticationSession | undefined): JiraServerResourceDescriptor {\n\t\treturn { id: this.domain, key: this.domain, name: this.domain, url: this.apiBaseUrlFor(session) };\n\t}\n\n\t// No `autolinks()` override yet, unlike Jira Cloud: an autolink prefix has to be the Jira project KEY, which\n\t// is what a commit message or branch name carries (`PROJ-123`). The project descriptors carry it now (see\n\t// `getProviderProjectsForResources`), so the override can follow Cloud's; until then none is registered.\n\n\tprotected override async getProviderAccountForResource(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resource: JiraServerResourceDescriptor,\n\t): Promise<Account | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst user = await api.getJiraServerCurrentUser(toTokenWithInfo(this.id, session), this.apiBaseUrlFor(session));\n\n\t\tif (user == null) return undefined;\n\t\treturn toAccount(user, this);\n\t}\n\n\tprotected override getProviderResourcesForUser(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_force?: boolean,\n\t): Promise<JiraServerResourceDescriptor[] | undefined> {\n\t\t// Synthesized from the configured host rather than fetched, so there is nothing to cache or force.\n\t\treturn Promise.resolve([this.resourceFor(session)]);\n\t}\n\n\t/** Expires, and is dropped on a re-sync, so a project created or deleted mid-session is picked up (#5907). */\n\tprivate readonly _projects = new DiscoveryCache<JiraServerProjectDescriptor[]>();\n\tprotected override async getProviderProjectsForResources(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources: JiraServerResourceDescriptor[],\n\t\tforce: boolean = false,\n\t): Promise<JiraServerProjectDescriptor[] | undefined> {\n\t\t// Every resource here is this instance (see `getProviderResourcesForUser`), so one read serves them\n\t\t// all; a caller that passes none is asking for nothing.\n\t\tif (resources.length === 0) return [];\n\n\t\tconst cacheKey = this.projectsCacheKey(session);\n\t\tconst cached = this._projects.get(cacheKey);\n\t\tif (cached != null && !force) return cached;\n\n\t\tconst generation = this._projects.generation;\n\t\tconst api = await this.getProvidersApi();\n\t\tconst projects = await api.getJiraServerProjects(\n\t\t\ttoTokenWithInfo(this.id, session),\n\t\t\tthis.apiBaseUrlFor(session),\n\t\t);\n\t\tif (projects == null) return undefined;\n\n\t\tconst resourceId = this.resourceFor(session).id;\n\t\tconst descriptors = projects.map<JiraServerProjectDescriptor>(p => ({\n\t\t\tid: p.id,\n\t\t\t// The project key, which selects the project (`listProjects`' `name`) and is what an issue key starts\n\t\t\t// with. The SDK does not validate it, so a missing or blank one falls back to the numeric id, never to the\n\t\t\t// display name, which can equal another project's key. The reads below still address the project by\n\t\t\t// `id`, which no other project can share.\n\t\t\tkey: p.key?.trim() || p.id,\n\t\t\tname: p.name,\n\t\t\tresourceId: resourceId,\n\t\t}));\n\n\t\tthis._projects.set(cacheKey, descriptors, { generation: generation });\n\n\t\treturn descriptors;\n\t}\n\n\tprotected override async getProviderIssuesForProject(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: JiraServerProjectDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<IssueShape[] | undefined> {\n\t\treturn (await this.getProviderIssuesForProjectWithTruncation(session, project, options))?.values;\n\t}\n\n\tprotected override async getProviderIssuesForProjectWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: JiraServerProjectDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<ProjectIssuesDrain | undefined> {\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\tconst api = await this.getProvidersApi();\n\t\tconst baseUrl = this.apiBaseUrlFor(session);\n\n\t\t// The same drain as Jira Cloud's (`jiraIssueDrains.ts`): page to a defensive backstop, and report `truncated`\n\t\t// plus a structured failure rather than discarding an already-fetched prefix.\n\t\tconst states = toProviderIssueStates(options?.state);\n\t\t// The numeric project id, not the name: JQL resolves `project = \"<value>\"` against key first, so a display\n\t\t// name that matches another project's key reads that project's issues instead. A Jira project key must begin\n\t\t// with a letter, so a purely numeric value can only be an id.\n\t\tconst fetchPage: JiraIssuePageFetcher = (scope, cursor) =>\n\t\t\tapi.getJiraServerIssuesForProjectPaged(tokenWithInfo, baseUrl, project.id, {\n\t\t\t\t...scope,\n\t\t\t\tcursor: cursor,\n\t\t\t\tstates: states,\n\t\t\t\tsort: options?.sort,\n\t\t\t});\n\t\tconst drainOptions: JiraDrainOptions = {\n\t\t\tmaxPages: maxPagesPerRequest,\n\t\t\tfailureScope: { providerId: this.id, resourceId: project.resourceId, projectId: project.id },\n\t\t\tproviderName: 'Jira Server',\n\t\t\t// A project this connection can no longer see (deleted, or its browse permission removed) holds\n\t\t\t// no issues for it, and the project list that named it is stale: drop that list so the next read\n\t\t\t// re-reads it, instead of failing the whole provider on every read until a restart (#5907).\n\t\t\tisEmptyFailure: ex => {\n\t\t\t\tif (!isJiraMissingProjectError(ex)) return false;\n\n\t\t\t\tLogger.warn(`Jira Server project '${project.name}' (${project.id}) no longer exists or is not visible`);\n\t\t\t\tthis._projects.delete(this.projectsCacheKey(session));\n\t\t\t\treturn true;\n\t\t\t},\n\t\t};\n\n\t\tif (options?.user != null) {\n\t\t\tconst read = await readUserScopedJiraIssues(\n\t\t\t\tfetchPage,\n\t\t\t\toptions.filters,\n\t\t\t\ttoUserIdentities(options.user),\n\t\t\t\tdrainOptions,\n\t\t\t);\n\t\t\tconst values = toUniqueJiraIssueShapes(read.issues, this);\n\t\t\tconst drain = { values: values, metadata: read.metadata, scope: drainOptions.failureScope };\n\t\t\t// `recovery: 'none'` for the whole set: the filter branches merge here, so even a pure backstop in one\n\t\t\t// branch is not fixable by narrowing this read's scope.\n\t\t\treturn read.truncated ? { ...drain, truncated: true, recovery: 'none' } : { ...drain, truncated: false };\n\t\t}\n\n\t\tconst unscoped = await drainJiraIssues(fetchPage, {}, drainOptions);\n\t\tconst values = unscoped.issues\n\t\t\t.map(issue => toIssueShape(issue, this, { projection: 'project' }))\n\t\t\t.filter((result): result is IssueShape => result !== undefined);\n\t\tconst drain = { values: values, metadata: unscoped.metadata, scope: drainOptions.failureScope };\n\t\treturn unscoped.status !== 'complete'\n\t\t\t? { ...drain, truncated: true, recovery: unscoped.status === 'backstop' ? 'narrow-scope' : 'none' }\n\t\t\t: { ...drain, truncated: false };\n\t}\n\n\t/**\n\t * Searches the instance's projects together for a user-scoped read, as Jira Cloud does per site: one JQL names\n\t * up to {@link JIRA_MAX_PROJECT_KEYS_PER_REQUEST} projects of one user scope. An unscoped read stays per project,\n\t * since its backstop is reported as `narrow-scope` per project.\n\t */\n\tprotected override getProjectIssuesSearches(\n\t\trequests: readonly ProjectIssuesRequest<JiraServerProjectDescriptor>[],\n\t): number[][] | undefined {\n\t\treturn groupProjectIssuesSearches(\n\t\t\trequests,\n\t\t\t({ options }) =>\n\t\t\t\toptions.user != null\n\t\t\t\t\t? [options.user, options.sort ?? '', options.state ?? '', options.filters?.join(',') ?? ''].join(\n\t\t\t\t\t\t\t'\\0',\n\t\t\t\t\t\t)\n\t\t\t\t\t: undefined,\n\t\t\tJIRA_MAX_PROJECT_KEYS_PER_REQUEST,\n\t\t);\n\t}\n\n\tprotected override async searchProviderProjectIssues(\n\t\tsession: ProviderAuthenticationSession,\n\t\trequests: readonly ProjectIssuesRequest<JiraServerProjectDescriptor>[],\n\t): Promise<IssueShape[][] | undefined> {\n\t\t// Every request of a search shares its user scope (see `getProjectIssuesSearches`).\n\t\tconst { options } = requests[0];\n\t\tif (options.user == null) return undefined;\n\t\t// One project's search is its own read, by the same numeric id, so it costs no more to read it as one. And it\n\t\t// reports what the search can't: a dropped issue's failure, which a search can only answer by being read again.\n\t\tif (requests.length === 1) return undefined;\n\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\tconst api = await this.getProvidersApi();\n\t\tconst baseUrl = this.apiBaseUrlFor(session);\n\t\t// By numeric id, as the per-project read names them (see there).\n\t\tconst projectIds = requests.map(request => request.project.id);\n\t\tconst states = toProviderIssueStates(options.state);\n\n\t\treturn searchJiraProjectIssues(\n\t\t\t(userScope, cursor) =>\n\t\t\t\tapi.getJiraServerIssuesForProjectsPaged(tokenWithInfo, baseUrl, projectIds, {\n\t\t\t\t\t...userScope,\n\t\t\t\t\tcursor: cursor,\n\t\t\t\t\tstates: states,\n\t\t\t\t\tsort: options.sort,\n\t\t\t\t}),\n\t\t\tprojectIds,\n\t\t\toptions.filters,\n\t\t\ttoUserIdentities(options.user),\n\t\t\t{ maxPages: maxPagesPerRequest, failureScope: { providerId: this.id }, providerName: 'Jira Server' },\n\t\t\tthis,\n\t\t);\n\t}\n\n\tprotected override async searchProviderMyIssues(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources?: JiraServerResourceDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t): Promise<IssueShape[] | undefined> {\n\t\treturn (await this.searchProviderMyIssuesWithTruncation(session, resources, cancellation))?.values;\n\t}\n\n\t/**\n\t * Account-wide \"my issues\" for this instance, drained to the backstop and reporting whether it got there.\n\t *\n\t * The override exists for the same reason the project drain reports `status`: the default\n\t * implementation wraps whatever the normalized read returned as `truncated: false`, so a run that stopped\n\t * at `maxPagesPerRequest` with pages left, or on a continuation the server never advanced, would be\n\t * published as a complete account. Cancellation is NOT truncation — the caller asked for the stop and\n\t * already knows the prefix is partial — so it breaks without setting the flag.\n\t */\n\tprotected override async searchProviderMyIssuesWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resources?: JiraServerResourceDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t\toptions?: SearchMyIssuesOptions,\n\t): Promise<AccountWideIssuesResult | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst states = toProviderIssueStates(options?.state);\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\tconst baseUrl = this.apiBaseUrlFor(session);\n\n\t\t// Errors propagate, unlike Jira Cloud's sibling. Cloud swallows per resource because it loops over the\n\t\t// account's many sites and one bad site must not discard the others; this connection addresses exactly\n\t\t// one instance, so there is no sibling to protect and swallowing would turn every failure — an expired\n\t\t// token, an unreachable host — into an empty success. `searchMyIssuesWithTruncationResult` recovers the\n\t\t// throw into `{ error }`, which is what lets the caller warn instead of reporting \"no issues\".\n\t\t// Only the provider: the instance is this connection's single resource, so naming it would publish a\n\t\t// failure of the whole connection as one confined to a sub-scope (see `ProviderWarning.scope`).\n\t\tconst accountScope = { providerId: this.id };\n\t\tconst results: IssueShape[] = [];\n\t\tlet cursor: string | undefined;\n\t\tlet truncated = false;\n\t\tlet metadata: CollectionMetadata | undefined;\n\t\tconst pages: CollectionMetadata[] = [];\n\n\t\tconst stopIncomplete = (reason: string) => {\n\t\t\ttruncated = true;\n\t\t\tmetadata = mergeCollectionMetadata(metadata, {\n\t\t\t\tcompleteness: 'partial',\n\t\t\t\tfailures: [toCollectionScopeFailure(accountScope, new Error(reason))],\n\t\t\t});\n\t\t};\n\n\t\tfor (let i = 0; i < maxPagesPerRequest; i++) {\n\t\t\tif (cancellation?.aborted) break;\n\n\t\t\tconst page = await api.getJiraServerIssuesForCurrentUser(tokenWithInfo, baseUrl, {\n\t\t\t\tcursor: cursor,\n\t\t\t\tstates: states,\n\t\t\t});\n\t\t\tif (page == null) {\n\t\t\t\t// Nothing fetched yet and no page is an empty account, not a failure; after a continuation it\n\t\t\t\t// means the server dropped the drain mid-way.\n\t\t\t\tif (cursor != null) {\n\t\t\t\t\tstopIncomplete('Jira Server returned no page after advertising a continuation');\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tresults.push(\n\t\t\t\t...page.data\n\t\t\t\t\t.map(issue => toIssueShape(issue, this, { projection: 'account' }))\n\t\t\t\t\t.filter((r): r is IssueShape => r != null),\n\t\t\t);\n\t\t\t// An issue the page dropped because it could not be read arrives here, not as a missing row; recorded under\n\t\t\t// the same scope as this read's other failures.\n\t\t\tif (page.metadata != null) {\n\t\t\t\t// Truncated too, as Jira Cloud's read is through `getPagedResult`: the account is missing that issue.\n\t\t\t\ttruncated ||= isIncompleteCollection(page.metadata);\n\t\t\t\tpages.push(withFailureScope(page.metadata, accountScope));\n\t\t\t}\n\n\t\t\tif (!page.hasMore) break;\n\n\t\t\t// More pages are claimed but no advancing cursor came back: the drain can't continue, so flag it\n\t\t\t// rather than stopping silently.\n\t\t\tif (page.nextCursor == null || page.nextCursor === cursor) {\n\t\t\t\tstopIncomplete('Jira Server returned no advancing issue continuation');\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcursor = page.nextCursor;\n\t\t\t// Pages remain but this was the last allowed iteration: the backstop stopped the drain.\n\t\t\tif (i === maxPagesPerRequest - 1) {\n\t\t\t\tstopIncomplete(\n\t\t\t\t\t`Jira Server account-wide issue drain stopped at the ${maxPagesPerRequest}-page backstop`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tvalues: results,\n\t\t\ttruncated: truncated,\n\t\t\tmetadata: mergeCollectionMetadata(summarizePageMetadata(pages), metadata),\n\t\t};\n\t}\n\n\tprotected override async getProviderLinkedIssueOrPullRequest(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resource: JiraServerResourceDescriptor,\n\t\t{ key }: { id: string; key: string },\n\t): Promise<IssueOrPullRequest | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst issue = await api.getJiraServerIssue(toTokenWithInfo(this.id, session), this.apiBaseUrlFor(session), key);\n\t\treturn issue != null ? toIssueShape(issue, this, { projection: 'point' }) : undefined;\n\t}\n\n\tprotected override async getProviderIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resource: JiraServerResourceDescriptor,\n\t\tid: string,\n\t): Promise<Issue | undefined> {\n\t\treturn this.readIssue(session, id, 'point');\n\t}\n\n\tprivate async readIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\tid: string,\n\t\tprojection: IssueProjection,\n\t): Promise<Issue | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst apiResult = await api.getJiraServerIssue(\n\t\t\ttoTokenWithInfo(this.id, session),\n\t\t\tthis.apiBaseUrlFor(session),\n\t\t\tid,\n\t\t);\n\t\tconst issue = apiResult != null ? toIssueShape(apiResult, this, { projection: projection }) : undefined;\n\t\treturn issue != null ? { ...issue, type: 'issue' } : undefined;\n\t}\n\n\t/**\n\t * The point read behind `getIssuesBatch`'s tracker targets (#5872). `resourceId` is this connection's single\n\t * synthetic resource — the host itself, see {@link JiraServerResourceDescriptor} — so it selects nothing further\n\t * here: the host was chosen upstream by `domain` (or the connection's configured domain) and the request is\n\t * addressed to the SESSION's base URL like every other read. No `resourceUrl` is needed either, unlike Jira\n\t * Cloud: its REST `self` link is an API endpoint, whereas `getJiraServerIssue` builds the browser link from\n\t * the base URL it was addressed to. A key that names no issue resolves to `undefined`, a proven absence.\n\t */\n\tprotected override getProviderIssueByResourceId(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resourceId: string,\n\t\tid: string,\n\t\t_resourceUrl: string | undefined,\n\t): Promise<Issue | undefined> {\n\t\treturn this.readIssue(session, id, 'batch');\n\t}\n\n\t/**\n\t * Caches are keyed by the ADDRESS the read was made against as well as the token: one integration instance\n\t * exists per configured host and two hosts can hold the same token, so a token-only key would serve one\n\t * host's projects for the other — and the host alone is not enough either, because a connection re-pointed\n\t * to another context path on the same host keeps its token and would otherwise be served the old path's\n\t * projects.\n\t */\n\tprivate projectsCacheKey(session: ProviderAuthenticationSession): string {\n\t\treturn `${this.apiBaseUrlFor(session)}:${session.accessToken}`;\n\t}\n\n\toverride invalidateDiscoveryCaches(): void {\n\t\tthis._projects.clear();\n\t}\n\n\tprotected override providerOnDisconnect(): void {\n\t\tthis._projects.clear();\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"jira-server.js","names":[],"sources":["../../../../../plus/integrations/src/providers/jira-server.ts"],"sourcesContent":["import type { CollectionMetadata } from '@gitkraken/provider-apis';\nimport { JIRA_MAX_PROJECT_KEYS_PER_REQUEST } from '@gitkraken/provider-apis';\nimport type { Account } from '@gitlens/git/models/author.js';\nimport type { Issue, IssueProjection, IssueShape } from '@gitlens/git/models/issue.js';\nimport type { IssueOrPullRequest } from '@gitlens/git/models/issueOrPullRequest.js';\nimport type { IssueResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport type { Emitter } from '@gitlens/utils/event.js';\nimport { Logger } from '@gitlens/utils/logger.js';\nimport type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider.js';\nimport type { IntegrationAuthenticationService } from '../authentication/integrationAuthenticationService.js';\nimport type { ProviderAuthenticationSession } from '../authentication/models.js';\nimport { toTokenWithInfo } from '../authentication/models.js';\nimport { isIncompleteCollection, toCollectionScopeFailure } from '../collectionMetadata.js';\nimport { IssuesSelfManagedHostIntegrationId } from '../constants.js';\nimport type { IntegrationServiceContext } from '../context.js';\nimport type { IntegrationConnectionChangeEvent } from '../integrationService.js';\nimport type { IntegrationKey } from '../models/integration.js';\nimport type {\n\tAccountWideIssuesResult,\n\tIssuesForProjectOptions,\n\tProjectIssuesDrain,\n\tProjectIssuesRequest,\n\tSearchMyIssuesOptions,\n} from '../models/issueReads.js';\nimport { groupProjectIssuesSearches, IssuesIntegration, toProjectsCollection } from '../models/issuesIntegration.js';\nimport { areDomainsOnSameHost, baseUrlFromDomain } from '../utils/domain.utils.js';\nimport type { ProviderApiCollectionResult } from './models.js';\nimport { providersMetadata, toAccount, toIssueShape, toProviderIssueStates } from './models.js';\nimport { isJiraServerMissingProjectError } from './providerErrors.js';\nimport type { ProvidersApi } from './providersApi.js';\nimport { DiscoveryCache } from './utils/discoveryCache.js';\nimport type { JiraDrainOptions, JiraIssuePageFetcher } from './utils/jiraIssueDrains.js';\nimport {\n\tdrainJiraIssues,\n\treadUserScopedJiraIssues,\n\tsearchJiraProjectIssues,\n\tsummarizePageMetadata,\n\ttoUniqueJiraIssueShapes,\n\twithFailureScope,\n} from './utils/jiraIssueDrains.js';\nimport { mergeCollectionMetadata } from './utils/providerPaging.js';\n\nconst metadata = providersMetadata[IssuesSelfManagedHostIntegrationId.JiraServer];\nconst authProvider = Object.freeze({ id: metadata.id, scopes: metadata.scopes });\nconst maxPagesPerRequest = 10;\n\n/**\n * Every clause is scoped by the HANDLE, and `options.userId` is deliberately ignored here — the one\n * place this provider diverges from Jira Cloud's, which applies it to `assignee`/`creator` (#5857).\n *\n * The two are not interchangeable because `Account.id` means something different per deployment. On\n * Cloud it is the `accountId`, which JQL resolves, so scoping by it fixes the deactivated-account case\n * where the display name no longer resolves. On Server it is the user KEY (`JIRAUSER10224`), which JQL\n * does not resolve at all: measured against a live Jira Server 8.20.13 site, `assignee in\n * (\"JIRAUSER10224\")` returns 0 with `\"The value 'JIRAUSER10224' does not exist for the field\n * 'assignee'\"` — the identical warning a genuinely nonexistent user produces — while the username and\n * the display name both return the real 4.\n *\n * So applying `userId` here would silently empty the read rather than harden it. Do not \"fix\" this\n * asymmetry to match Cloud without re-measuring against a Server instance.\n *\n * The handle itself is already the right one: `provider-apis` 0.60.0 maps `Account.username` from the\n * Server payload's `name` (falling back to `displayName` when absent), so these clauses are scoped by the\n * real username rather than a display name (GKDEV-3629). That is what makes the `mention` clause work at\n * all — it is a free-text `comment ~ \"...\"` over comment bodies, and a Jira mention is stored as\n * `[~username]`, which a display name never matches.\n */\nfunction toUserIdentities(user: string): { userField: string; mention: string } {\n\treturn { userField: user, mention: user };\n}\n\n/**\n * The single resource a Jira Server connection has: the instance itself.\n *\n * Jira Cloud's account spans many sites, so its resources come from an API call that lists them. A\n * self-hosted instance IS the one site the connection addresses, so there is nothing to enumerate — this\n * descriptor is synthesized from the configured host. Modelling it as a resource anyway keeps the\n * resource → project contract of {@link IssuesIntegration} intact, so every tracker read (`listOrgs`,\n * `listProjects`, `listIssueTrackerIssuesPage`) works unchanged.\n */\nexport interface JiraServerResourceDescriptor extends IssueResourceDescriptor {\n\turl: string;\n}\n\nexport interface JiraServerProjectDescriptor extends IssueResourceDescriptor {\n\tresourceId: string;\n}\n\nexport class JiraServerIntegration extends IssuesIntegration<IssuesSelfManagedHostIntegrationId.JiraServer> {\n\treadonly authProvider: IntegrationAuthenticationProviderDescriptor = authProvider;\n\treadonly id = IssuesSelfManagedHostIntegrationId.JiraServer;\n\tprotected readonly key: IntegrationKey<IssuesSelfManagedHostIntegrationId.JiraServer>;\n\treadonly name: string = 'Jira Data Center';\n\n\tconstructor(\n\t\tctx: IntegrationServiceContext,\n\t\tauthenticationService: IntegrationAuthenticationService,\n\t\tgetProvidersApi: () => Promise<ProvidersApi>,\n\t\tdidChangeConnection: Emitter<IntegrationConnectionChangeEvent>,\n\t\tprivate readonly _domain: string,\n\t) {\n\t\tsuper(ctx, authenticationService, getProvidersApi, didChangeConnection);\n\t\tthis.key = `${this.id}:${this._domain}`;\n\t}\n\n\tget domain(): string {\n\t\treturn this._domain;\n\t}\n\n\t/**\n\t * The URL every request for this connection is addressed to, built from the SESSION's domain rather than\n\t * the integration's.\n\t *\n\t * `this.domain` is the host-keyed identity — `IntegrationService.get()` runs it through `hostFromDomain`\n\t * so one host can't build two instances — which means it has had any context path stripped, and so has\n\t * `session.domain`, which is stored as that same normalized host. Jira Data Center officially supports\n\t * being mounted below a context path (`https://jira.example.com/jira`), so the address comes from\n\t * {@link ProviderAuthenticationSession.baseUrl}, which carries the configured value verbatim; it falls\n\t * back to the host when the backend reported nothing more, which is the common case.\n\t *\n\t * Takes the session rather than reading `_session` so a per-connection (multi-account) read is addressed\n\t * to the host of the connection it resolved, not to whichever one happens to be current.\n\t */\n\tprotected apiBaseUrlFor(session: ProviderAuthenticationSession | undefined): string {\n\t\tconst configured = baseUrlFromDomain(session?.baseUrl, session?.protocol);\n\t\t// A `baseUrl` from another host is not this connection's address — refuse it rather than routing this\n\t\t// integration's reads somewhere its key never named.\n\t\tif (configured != null && areDomainsOnSameHost(session?.baseUrl, this.domain)) return configured;\n\n\t\treturn (\n\t\t\tbaseUrlFromDomain(session?.domain, session?.protocol) ??\n\t\t\tbaseUrlFromDomain(this.domain, session?.protocol) ??\n\t\t\t`${session?.protocol ?? 'https:'}//${this.domain}`\n\t\t);\n\t}\n\n\t/** The one resource this connection has — see {@link JiraServerResourceDescriptor}. */\n\tprivate resourceFor(session: ProviderAuthenticationSession | undefined): JiraServerResourceDescriptor {\n\t\treturn { id: this.domain, key: this.domain, name: this.domain, url: this.apiBaseUrlFor(session) };\n\t}\n\n\t// No `autolinks()` override yet, unlike Jira Cloud: an autolink prefix has to be the Jira project KEY, which\n\t// is what a commit message or branch name carries (`PROJ-123`). The project descriptors carry it now (see\n\t// `getProviderProjectsForResources`), so the override can follow Cloud's; until then none is registered.\n\n\tprotected override async getProviderAccountForResource(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resource: JiraServerResourceDescriptor,\n\t): Promise<Account | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst user = await api.getJiraServerCurrentUser(toTokenWithInfo(this.id, session), this.apiBaseUrlFor(session));\n\n\t\tif (user == null) return undefined;\n\t\treturn toAccount(user, this);\n\t}\n\n\tprotected override getProviderResourcesForUser(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_force?: boolean,\n\t): Promise<JiraServerResourceDescriptor[] | undefined> {\n\t\t// Synthesized from the configured host rather than fetched, so there is nothing to cache or force.\n\t\treturn Promise.resolve([this.resourceFor(session)]);\n\t}\n\n\t/** Expires, and is dropped on a re-sync, so a project created or deleted mid-session is picked up (#5907). */\n\tprivate readonly _projects = new DiscoveryCache<JiraServerProjectDescriptor[]>();\n\tprotected override async getProviderProjectsForResources(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources: JiraServerResourceDescriptor[],\n\t\tforce: boolean = false,\n\t): Promise<JiraServerProjectDescriptor[] | undefined> {\n\t\t// Every resource here is this instance (see `getProviderResourcesForUser`), so one read serves them\n\t\t// all; a caller that passes none is asking for nothing.\n\t\tif (resources.length === 0) return [];\n\n\t\tconst cacheKey = this.projectsCacheKey(session);\n\t\tconst cached = this._projects.get(cacheKey);\n\t\tif (cached != null && !force) return cached;\n\n\t\tconst generation = this._projects.generation;\n\t\tconst descriptors = await this.readProjects(session);\n\t\tif (descriptors == null) return undefined;\n\n\t\tthis._projects.set(cacheKey, descriptors, { generation: generation });\n\n\t\treturn descriptors;\n\t}\n\n\tprotected override async getProviderUncachedProjectsForResourcesWithMetadata(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources: JiraServerResourceDescriptor[],\n\t): Promise<ProviderApiCollectionResult<JiraServerProjectDescriptor>> {\n\t\tif (resources.length === 0) return { values: [] };\n\n\t\treturn toProjectsCollection(await this.readProjects(session));\n\t}\n\n\tprivate async readProjects(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<JiraServerProjectDescriptor[] | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst projects = await api.getJiraServerProjects(\n\t\t\ttoTokenWithInfo(this.id, session),\n\t\t\tthis.apiBaseUrlFor(session),\n\t\t);\n\t\tif (projects == null) return undefined;\n\n\t\tconst resourceId = this.resourceFor(session).id;\n\t\treturn projects.map<JiraServerProjectDescriptor>(p => ({\n\t\t\tid: p.id,\n\t\t\t// The project key, which selects the project (`listProjects`' `name`) and is what an issue key starts\n\t\t\t// with. The SDK does not validate it, so a missing or blank one falls back to the numeric id, never to the\n\t\t\t// display name, which can equal another project's key. The reads below still address the project by\n\t\t\t// `id`, which no other project can share.\n\t\t\tkey: p.key?.trim() || p.id,\n\t\t\tname: p.name,\n\t\t\tresourceId: resourceId,\n\t\t}));\n\t}\n\n\tprotected override async getProviderIssuesForProject(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: JiraServerProjectDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<IssueShape[] | undefined> {\n\t\treturn (await this.getProviderIssuesForProjectWithTruncation(session, project, options))?.values;\n\t}\n\n\tprotected override async getProviderIssuesForProjectWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: JiraServerProjectDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<ProjectIssuesDrain | undefined> {\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\tconst api = await this.getProvidersApi();\n\t\tconst baseUrl = this.apiBaseUrlFor(session);\n\n\t\t// The same drain as Jira Cloud's (`jiraIssueDrains.ts`): page to a defensive backstop, and report `truncated`\n\t\t// plus a structured failure rather than discarding an already-fetched prefix.\n\t\tconst states = toProviderIssueStates(options?.state);\n\t\t// The numeric project id, not the name: JQL resolves `project = \"<value>\"` against key first, so a display\n\t\t// name that matches another project's key reads that project's issues instead. A Jira project key must begin\n\t\t// with a letter, so a purely numeric value can only be an id.\n\t\tconst fetchPage: JiraIssuePageFetcher = (scope, cursor) =>\n\t\t\tapi.getJiraServerIssuesForProjectPaged(tokenWithInfo, baseUrl, project.id, {\n\t\t\t\t...scope,\n\t\t\t\tcursor: cursor,\n\t\t\t\tstates: states,\n\t\t\t\tsort: options?.sort,\n\t\t\t});\n\t\tconst drainOptions: JiraDrainOptions = {\n\t\t\tmaxPages: maxPagesPerRequest,\n\t\t\t// Scoped by key, as on Jira Cloud: the id addresses the read, the key is how a consumer names the project.\n\t\t\tfailureScope: { providerId: this.id, resourceId: project.resourceId, projectId: project.key },\n\t\t\tproviderName: 'Jira Server',\n\t\t\t// A project this connection can no longer see (deleted, or its browse permission removed) holds\n\t\t\t// no issues for it, and the project list that named it is stale: drop that list so the next read\n\t\t\t// re-reads it, instead of failing the whole provider on every read until a restart (#5907).\n\t\t\tisEmptyFailure: ex => {\n\t\t\t\tif (!isJiraServerMissingProjectError(ex)) return false;\n\n\t\t\t\tLogger.warn(`Jira Server project '${project.name}' (${project.id}) no longer exists or is not visible`);\n\t\t\t\tthis._projects.delete(this.projectsCacheKey(session));\n\t\t\t\treturn true;\n\t\t\t},\n\t\t};\n\n\t\tif (options?.user != null) {\n\t\t\tconst read = await readUserScopedJiraIssues(\n\t\t\t\tfetchPage,\n\t\t\t\toptions.filters,\n\t\t\t\ttoUserIdentities(options.user),\n\t\t\t\tdrainOptions,\n\t\t\t);\n\t\t\tconst values = toUniqueJiraIssueShapes(read.issues, this);\n\t\t\tconst drain = { values: values, metadata: read.metadata, scope: drainOptions.failureScope };\n\t\t\t// `recovery: 'none'` for the whole set: the filter branches merge here, so even a pure backstop in one\n\t\t\t// branch is not fixable by narrowing this read's scope.\n\t\t\treturn read.truncated ? { ...drain, truncated: true, recovery: 'none' } : { ...drain, truncated: false };\n\t\t}\n\n\t\tconst unscoped = await drainJiraIssues(fetchPage, {}, drainOptions);\n\t\tconst values = unscoped.issues\n\t\t\t.map(issue => toIssueShape(issue, this, { projection: 'project' }))\n\t\t\t.filter((result): result is IssueShape => result !== undefined);\n\t\tconst drain = { values: values, metadata: unscoped.metadata, scope: drainOptions.failureScope };\n\t\treturn unscoped.status !== 'complete'\n\t\t\t? { ...drain, truncated: true, recovery: unscoped.status === 'backstop' ? 'narrow-scope' : 'none' }\n\t\t\t: { ...drain, truncated: false };\n\t}\n\n\t/**\n\t * Searches the instance's projects together for a user-scoped read, as Jira Cloud does per site: one JQL names\n\t * up to {@link JIRA_MAX_PROJECT_KEYS_PER_REQUEST} projects of one user scope. An unscoped read stays per project,\n\t * since its backstop is reported as `narrow-scope` per project.\n\t */\n\tprotected override getProjectIssuesSearches(\n\t\trequests: readonly ProjectIssuesRequest<JiraServerProjectDescriptor>[],\n\t): number[][] | undefined {\n\t\treturn groupProjectIssuesSearches(\n\t\t\trequests,\n\t\t\t({ options }) =>\n\t\t\t\toptions.user != null\n\t\t\t\t\t? [options.user, options.sort ?? '', options.state ?? '', options.filters?.join(',') ?? ''].join(\n\t\t\t\t\t\t\t'\\0',\n\t\t\t\t\t\t)\n\t\t\t\t\t: undefined,\n\t\t\tJIRA_MAX_PROJECT_KEYS_PER_REQUEST,\n\t\t);\n\t}\n\n\tprotected override async searchProviderProjectIssues(\n\t\tsession: ProviderAuthenticationSession,\n\t\trequests: readonly ProjectIssuesRequest<JiraServerProjectDescriptor>[],\n\t): Promise<IssueShape[][] | undefined> {\n\t\t// Every request of a search shares its user scope (see `getProjectIssuesSearches`).\n\t\tconst { options } = requests[0];\n\t\tif (options.user == null) return undefined;\n\t\t// One project's search is its own read, by the same numeric id, so it costs no more to read it as one. And it\n\t\t// reports what the search can't: a dropped issue's failure, which a search can only answer by being read again.\n\t\tif (requests.length === 1) return undefined;\n\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\tconst api = await this.getProvidersApi();\n\t\tconst baseUrl = this.apiBaseUrlFor(session);\n\t\t// By numeric id, as the per-project read names them (see there).\n\t\tconst projectIds = requests.map(request => request.project.id);\n\t\tconst states = toProviderIssueStates(options.state);\n\n\t\treturn searchJiraProjectIssues(\n\t\t\t(userScope, cursor) =>\n\t\t\t\tapi.getJiraServerIssuesForProjectsPaged(tokenWithInfo, baseUrl, projectIds, {\n\t\t\t\t\t...userScope,\n\t\t\t\t\tcursor: cursor,\n\t\t\t\t\tstates: states,\n\t\t\t\t\tsort: options.sort,\n\t\t\t\t}),\n\t\t\tprojectIds,\n\t\t\toptions.filters,\n\t\t\ttoUserIdentities(options.user),\n\t\t\t{ maxPages: maxPagesPerRequest, failureScope: { providerId: this.id }, providerName: 'Jira Server' },\n\t\t\tthis,\n\t\t);\n\t}\n\n\tprotected override async searchProviderMyIssues(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresources?: JiraServerResourceDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t): Promise<IssueShape[] | undefined> {\n\t\treturn (await this.searchProviderMyIssuesWithTruncation(session, resources, cancellation))?.values;\n\t}\n\n\t/**\n\t * Account-wide \"my issues\" for this instance, drained to the backstop and reporting whether it got there.\n\t *\n\t * The override exists for the same reason the project drain reports `status`: the default\n\t * implementation wraps whatever the normalized read returned as `truncated: false`, so a run that stopped\n\t * at `maxPagesPerRequest` with pages left, or on a continuation the server never advanced, would be\n\t * published as a complete account. Cancellation is NOT truncation — the caller asked for the stop and\n\t * already knows the prefix is partial — so it breaks without setting the flag.\n\t */\n\tprotected override async searchProviderMyIssuesWithTruncation(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resources?: JiraServerResourceDescriptor[],\n\t\tcancellation?: AbortSignal,\n\t\toptions?: SearchMyIssuesOptions,\n\t): Promise<AccountWideIssuesResult | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst states = toProviderIssueStates(options?.state);\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\tconst baseUrl = this.apiBaseUrlFor(session);\n\n\t\t// Errors propagate, unlike Jira Cloud's sibling. Cloud swallows per resource because it loops over the\n\t\t// account's many sites and one bad site must not discard the others; this connection addresses exactly\n\t\t// one instance, so there is no sibling to protect and swallowing would turn every failure — an expired\n\t\t// token, an unreachable host — into an empty success. `searchMyIssuesWithTruncationResult` recovers the\n\t\t// throw into `{ error }`, which is what lets the caller warn instead of reporting \"no issues\".\n\t\t// Only the provider: the instance is this connection's single resource, so naming it would publish a\n\t\t// failure of the whole connection as one confined to a sub-scope (see `ProviderWarning.scope`).\n\t\tconst accountScope = { providerId: this.id };\n\t\tconst results: IssueShape[] = [];\n\t\tlet cursor: string | undefined;\n\t\tlet truncated = false;\n\t\tlet metadata: CollectionMetadata | undefined;\n\t\tconst pages: CollectionMetadata[] = [];\n\n\t\tconst stopIncomplete = (reason: string) => {\n\t\t\ttruncated = true;\n\t\t\tmetadata = mergeCollectionMetadata(metadata, {\n\t\t\t\tcompleteness: 'partial',\n\t\t\t\tfailures: [toCollectionScopeFailure(accountScope, new Error(reason))],\n\t\t\t});\n\t\t};\n\n\t\tfor (let i = 0; i < maxPagesPerRequest; i++) {\n\t\t\tif (cancellation?.aborted) break;\n\n\t\t\tconst page = await api.getJiraServerIssuesForCurrentUser(tokenWithInfo, baseUrl, {\n\t\t\t\tcursor: cursor,\n\t\t\t\tstates: states,\n\t\t\t});\n\t\t\tif (page == null) {\n\t\t\t\t// Nothing fetched yet and no page is an empty account, not a failure; after a continuation it\n\t\t\t\t// means the server dropped the drain mid-way.\n\t\t\t\tif (cursor != null) {\n\t\t\t\t\tstopIncomplete('Jira Server returned no page after advertising a continuation');\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tresults.push(\n\t\t\t\t...page.data\n\t\t\t\t\t.map(issue => toIssueShape(issue, this, { projection: 'account' }))\n\t\t\t\t\t.filter((r): r is IssueShape => r != null),\n\t\t\t);\n\t\t\t// An issue the page dropped because it could not be read arrives here, not as a missing row; recorded under\n\t\t\t// the same scope as this read's other failures.\n\t\t\tif (page.metadata != null) {\n\t\t\t\t// Truncated too, as Jira Cloud's read is through `getPagedResult`: the account is missing that issue.\n\t\t\t\ttruncated ||= isIncompleteCollection(page.metadata);\n\t\t\t\tpages.push(withFailureScope(page.metadata, accountScope));\n\t\t\t}\n\n\t\t\tif (!page.hasMore) break;\n\n\t\t\t// More pages are claimed but no advancing cursor came back: the drain can't continue, so flag it\n\t\t\t// rather than stopping silently.\n\t\t\tif (page.nextCursor == null || page.nextCursor === cursor) {\n\t\t\t\tstopIncomplete('Jira Server returned no advancing issue continuation');\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcursor = page.nextCursor;\n\t\t\t// Pages remain but this was the last allowed iteration: the backstop stopped the drain.\n\t\t\tif (i === maxPagesPerRequest - 1) {\n\t\t\t\tstopIncomplete(\n\t\t\t\t\t`Jira Server account-wide issue drain stopped at the ${maxPagesPerRequest}-page backstop`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tvalues: results,\n\t\t\ttruncated: truncated,\n\t\t\tmetadata: mergeCollectionMetadata(summarizePageMetadata(pages), metadata),\n\t\t};\n\t}\n\n\tprotected override async getProviderLinkedIssueOrPullRequest(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resource: JiraServerResourceDescriptor,\n\t\t{ key }: { id: string; key: string },\n\t): Promise<IssueOrPullRequest | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst issue = await api.getJiraServerIssue(toTokenWithInfo(this.id, session), this.apiBaseUrlFor(session), key);\n\t\treturn issue != null ? toIssueShape(issue, this, { projection: 'point' }) : undefined;\n\t}\n\n\tprotected override async getProviderIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resource: JiraServerResourceDescriptor,\n\t\tid: string,\n\t): Promise<Issue | undefined> {\n\t\treturn this.readIssue(session, id, 'point');\n\t}\n\n\tprivate async readIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\tid: string,\n\t\tprojection: IssueProjection,\n\t): Promise<Issue | undefined> {\n\t\tconst api = await this.getProvidersApi();\n\t\tconst apiResult = await api.getJiraServerIssue(\n\t\t\ttoTokenWithInfo(this.id, session),\n\t\t\tthis.apiBaseUrlFor(session),\n\t\t\tid,\n\t\t);\n\t\tconst issue = apiResult != null ? toIssueShape(apiResult, this, { projection: projection }) : undefined;\n\t\treturn issue != null ? { ...issue, type: 'issue' } : undefined;\n\t}\n\n\t/**\n\t * The point read behind `getIssuesBatch`'s tracker targets (#5872). `resourceId` is this connection's single\n\t * synthetic resource — the host itself, see {@link JiraServerResourceDescriptor} — so it selects nothing further\n\t * here: the host was chosen upstream by `domain` (or the connection's configured domain) and the request is\n\t * addressed to the SESSION's base URL like every other read. No `resourceUrl` is needed either, unlike Jira\n\t * Cloud: its REST `self` link is an API endpoint, whereas `getJiraServerIssue` builds the browser link from\n\t * the base URL it was addressed to. A key that names no issue resolves to `undefined`, a proven absence.\n\t */\n\tprotected override getProviderIssueByResourceId(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resourceId: string,\n\t\tid: string,\n\t\t_resourceUrl: string | undefined,\n\t): Promise<Issue | undefined> {\n\t\treturn this.readIssue(session, id, 'batch');\n\t}\n\n\t/**\n\t * Caches are keyed by the ADDRESS the read was made against as well as the token: one integration instance\n\t * exists per configured host and two hosts can hold the same token, so a token-only key would serve one\n\t * host's projects for the other — and the host alone is not enough either, because a connection re-pointed\n\t * to another context path on the same host keeps its token and would otherwise be served the old path's\n\t * projects.\n\t */\n\tprivate projectsCacheKey(session: ProviderAuthenticationSession): string {\n\t\treturn `${this.apiBaseUrlFor(session)}:${session.accessToken}`;\n\t}\n\n\toverride invalidateDiscoveryCaches(): void {\n\t\tthis._projects.clear();\n\t}\n\n\tprotected override providerOnDisconnect(): void {\n\t\tthis._projects.clear();\n\t}\n}\n"],"mappings":"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"}
@@ -59,8 +59,16 @@ export declare class JiraIntegration extends IssuesIntegration<IssuesCloudHostIn
59
59
  /** When the caches were last dropped; a persisted discovery read before then is as stale as they were. */
60
60
  private _discoveryInvalidatedAt;
61
61
  protected getProviderResourcesForUser(session: ProviderAuthenticationSession, force?: boolean): Promise<JiraOrganizationDescriptor[] | undefined>;
62
+ protected getProviderUncachedResourcesForUser(session: ProviderAuthenticationSession): Promise<JiraOrganizationDescriptor[] | undefined>;
63
+ private readOrganizations;
62
64
  protected getProviderProjectsForResources(session: ProviderAuthenticationSession, resources: JiraOrganizationDescriptor[], force?: boolean): Promise<JiraProjectDescriptor[] | undefined>;
63
65
  protected getProviderProjectsForResourcesWithMetadata(session: ProviderAuthenticationSession, resources: JiraOrganizationDescriptor[], force?: boolean): Promise<ProviderApiCollectionResult<JiraProjectDescriptor>>;
66
+ protected getProviderUncachedProjectsForResourcesWithMetadata(session: ProviderAuthenticationSession, resources: JiraOrganizationDescriptor[]): Promise<ProviderApiCollectionResult<JiraProjectDescriptor>>;
67
+ /**
68
+ * Reads the projects of each resource from the provider, apart by whether a resource's list came back complete:
69
+ * only a complete one may be cached as the resource's project set.
70
+ */
71
+ private readProjects;
64
72
  protected getProviderIssuesForProject(session: ProviderAuthenticationSession, project: JiraProjectDescriptor, options?: IssuesForProjectOptions): Promise<IssueShape[] | undefined>;
65
73
  protected getProviderIssuesForProjectWithTruncation(session: ProviderAuthenticationSession, project: JiraProjectDescriptor, options?: IssuesForProjectOptions): Promise<ProjectIssuesDrain | undefined>;
66
74
  /**
@@ -157,10 +157,7 @@ var JiraIntegration = class extends IssuesIntegration {
157
157
  const cachedResources = this._organizations.get(accessToken);
158
158
  if (cachedResources != null && !force) return cachedResources;
159
159
  const generation = this._organizations.generation;
160
- const organizations = (await (await this.getProvidersApi()).getJiraResourcesForCurrentUser(toTokenWithInfo(this.id, session)))?.map((r) => ({
161
- ...r,
162
- key: r.id
163
- }));
160
+ const organizations = await this.readOrganizations(session);
164
161
  if (organizations == null) {
165
162
  this._organizations.delete(accessToken);
166
163
  return;
@@ -168,6 +165,15 @@ var JiraIntegration = class extends IssuesIntegration {
168
165
  this._organizations.set(accessToken, organizations, { generation });
169
166
  return organizations;
170
167
  }
168
+ getProviderUncachedResourcesForUser(session) {
169
+ return this.readOrganizations(session);
170
+ }
171
+ async readOrganizations(session) {
172
+ return (await (await this.getProvidersApi()).getJiraResourcesForCurrentUser(toTokenWithInfo(this.id, session)))?.map((r) => ({
173
+ ...r,
174
+ key: r.id
175
+ }));
176
+ }
171
177
  async getProviderProjectsForResources(session, resources, force = false) {
172
178
  return (await this.getProviderProjectsForResourcesWithMetadata(session, resources, force)).values;
173
179
  }
@@ -175,63 +181,78 @@ var JiraIntegration = class extends IssuesIntegration {
175
181
  const { accessToken } = session;
176
182
  const projectsCache = this._projects;
177
183
  const generation = projectsCache.generation;
178
- let resourcesWithoutProjects = [];
179
- if (force) resourcesWithoutProjects = resources;
180
- else for (const resource of resources) {
181
- const resourceKey = `${accessToken}:${resource.id}`;
182
- if (projectsCache.get(resourceKey) == null) resourcesWithoutProjects.push(resource);
183
- }
184
- let metadata;
185
- const partialProjects = [];
186
- if (resourcesWithoutProjects.length > 0) {
187
- const api = await this.getProvidersApi();
188
- const tokenWithInfo = toTokenWithInfo(this.id, session);
189
- (await Promise.allSettled(resourcesWithoutProjects.map(async (resource) => ({
190
- resource,
191
- result: await collectProviderPagedResult((cursor) => api.getJiraProjectsForResource(tokenWithInfo, resource.id, { cursor }), maxPagesPerRequest, {
192
- providerId: this.id,
193
- resourceId: resource.id
194
- })
195
- })))).forEach((drain, index) => {
196
- if (drain.status === "rejected") {
197
- const resource = resourcesWithoutProjects[index];
198
- if (resource == null) return;
199
- metadata = mergeCollectionMetadata(metadata, {
200
- completeness: "partial",
201
- failures: [toCollectionScopeFailure({
202
- providerId: this.id,
203
- resourceId: resource.id
204
- }, drain.reason)]
205
- });
206
- return;
207
- }
208
- const { resource, result } = drain.value;
209
- metadata = mergeCollectionMetadata(metadata, result.metadata);
210
- const projects = result.values.filter((project) => project.resourceId === resource.id).map((project) => ({ ...project }));
211
- if (result.truncated === true || result.metadata != null && result.metadata.completeness !== "complete") {
212
- partialProjects.push(...projects);
213
- if (result.truncated === true && (result.metadata == null || result.metadata.completeness === "complete")) metadata = mergeCollectionMetadata(metadata, { completeness: "partial" });
214
- return;
215
- }
216
- if (!projectsCache.set(`${accessToken}:${resource.id}`, projects, { generation })) {
217
- partialProjects.push(...projects);
218
- return;
219
- }
220
- this._autolinks.delete(accessToken);
221
- });
184
+ const cachedProjects = /* @__PURE__ */ new Map();
185
+ for (const resource of resources) {
186
+ const projects = projectsCache.get(`${accessToken}:${resource.id}`);
187
+ if (projects != null) cachedProjects.set(resource.id, projects);
222
188
  }
223
- const values = resources.reduce((projects, resource) => {
224
- const resourceProjects = projectsCache.get(`${accessToken}:${resource.id}`);
225
- if (resourceProjects != null) projects.push(...resourceProjects);
226
- return projects;
227
- }, partialProjects);
228
- const projectsByIdentity = /* @__PURE__ */ new Map();
229
- for (const project of values) {
230
- const identity = `${project.resourceId}:${project.id}`;
231
- if (!projectsByIdentity.has(identity)) projectsByIdentity.set(identity, project);
189
+ const resourcesWithoutProjects = force ? resources : resources.filter((resource) => !cachedProjects.has(resource.id));
190
+ const read = await this.readProjects(session, resourcesWithoutProjects);
191
+ const values = [...read.partial];
192
+ for (const [resourceId, projects] of read.complete) if (projectsCache.set(`${accessToken}:${resourceId}`, projects, { generation })) this._autolinks.delete(accessToken);
193
+ for (const resource of resources) {
194
+ const resourceProjects = read.complete.get(resource.id) ?? cachedProjects.get(resource.id);
195
+ if (resourceProjects != null) values.push(...resourceProjects);
232
196
  }
233
197
  return {
234
- values: [...projectsByIdentity.values()],
198
+ values: uniqueProjects(values),
199
+ metadata: read.metadata
200
+ };
201
+ }
202
+ async getProviderUncachedProjectsForResourcesWithMetadata(session, resources) {
203
+ const read = await this.readProjects(session, resources);
204
+ return {
205
+ values: uniqueProjects([...read.partial, ...[...read.complete.values()].flat()]),
206
+ metadata: read.metadata
207
+ };
208
+ }
209
+ /**
210
+ * Reads the projects of each resource from the provider, apart by whether a resource's list came back complete:
211
+ * only a complete one may be cached as the resource's project set.
212
+ */
213
+ async readProjects(session, resources) {
214
+ const complete = /* @__PURE__ */ new Map();
215
+ const partial = [];
216
+ let metadata;
217
+ if (resources.length === 0) return {
218
+ complete,
219
+ partial,
220
+ metadata
221
+ };
222
+ const api = await this.getProvidersApi();
223
+ const tokenWithInfo = toTokenWithInfo(this.id, session);
224
+ (await Promise.allSettled(resources.map(async (resource) => ({
225
+ resource,
226
+ result: await collectProviderPagedResult((cursor) => api.getJiraProjectsForResource(tokenWithInfo, resource.id, { cursor }), maxPagesPerRequest, {
227
+ providerId: this.id,
228
+ resourceId: resource.id
229
+ })
230
+ })))).forEach((drain, index) => {
231
+ if (drain.status === "rejected") {
232
+ const resource = resources[index];
233
+ if (resource == null) return;
234
+ metadata = mergeCollectionMetadata(metadata, {
235
+ completeness: "partial",
236
+ failures: [toCollectionScopeFailure({
237
+ providerId: this.id,
238
+ resourceId: resource.id
239
+ }, drain.reason)]
240
+ });
241
+ return;
242
+ }
243
+ const { resource, result } = drain.value;
244
+ metadata = mergeCollectionMetadata(metadata, result.metadata);
245
+ const projects = result.values.filter((project) => project.resourceId === resource.id).map((project) => ({ ...project }));
246
+ if (result.truncated === true || result.metadata != null && result.metadata.completeness !== "complete") {
247
+ partial.push(...projects);
248
+ if (result.truncated === true && (result.metadata == null || result.metadata.completeness === "complete")) metadata = mergeCollectionMetadata(metadata, { completeness: "partial" });
249
+ return;
250
+ }
251
+ complete.set(resource.id, projects);
252
+ });
253
+ return {
254
+ complete,
255
+ partial,
235
256
  metadata
236
257
  };
237
258
  }
@@ -567,6 +588,15 @@ var JiraIntegration = class extends IssuesIntegration {
567
588
  this._autolinks.clear();
568
589
  }
569
590
  };
591
+ /** The projects once each, by site and id. */
592
+ function uniqueProjects(projects) {
593
+ const projectsByIdentity = /* @__PURE__ */ new Map();
594
+ for (const project of projects) {
595
+ const identity = `${project.resourceId}:${project.id}`;
596
+ if (!projectsByIdentity.has(identity)) projectsByIdentity.set(identity, project);
597
+ }
598
+ return [...projectsByIdentity.values()];
599
+ }
570
600
  //#endregion
571
601
  export { JiraIntegration };
572
602