@gitkraken/core-gitlens 0.9.3 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -1
- package/dist/plus/integrations/integrationService.d.ts +6 -1
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/manager.d.ts +6 -1
- package/dist/plus/integrations/models/issueReads.d.ts +14 -0
- package/dist/plus/integrations/providers/bitbucket/models.d.ts +5 -5
- package/dist/plus/integrations/providers/bitbucket/models.js +2 -2
- package/dist/plus/integrations/providers/bitbucket/models.js.map +1 -1
- package/dist/plus/integrations/providers/issueSorts.d.ts +0 -1
- package/dist/plus/integrations/providers/jira-server.d.ts +2 -2
- package/dist/plus/integrations/providers/jira-server.js +9 -3
- package/dist/plus/integrations/providers/jira-server.js.map +1 -1
- package/dist/plus/integrations/providers/jira.d.ts +6 -1
- package/dist/plus/integrations/providers/jira.js +19 -3
- package/dist/plus/integrations/providers/jira.js.map +1 -1
- package/dist/plus/integrations/providers/linear.js +8 -2
- package/dist/plus/integrations/providers/linear.js.map +1 -1
- package/dist/plus/integrations/providers/models.d.ts +19 -1
- package/dist/plus/integrations/providers/models.js +14 -5
- package/dist/plus/integrations/providers/models.js.map +1 -1
- package/dist/plus/integrations/providers/providersApi.d.ts +8 -1
- package/dist/plus/integrations/providers/providersApi.js +3 -0
- package/dist/plus/integrations/providers/providersApi.js.map +1 -1
- package/dist/plus/integrations/providers/trello.js +1 -0
- package/dist/plus/integrations/providers/trello.js.map +1 -1
- package/dist/plus/integrations/reads/cursors.d.ts +8 -0
- package/dist/plus/integrations/reads/cursors.js +5 -3
- package/dist/plus/integrations/reads/cursors.js.map +1 -1
- package/dist/plus/integrations/reads/drains.d.ts +0 -1
- package/dist/plus/integrations/reads/filters.d.ts +8 -0
- package/dist/plus/integrations/reads/filters.js +2 -1
- package/dist/plus/integrations/reads/filters.js.map +1 -1
- package/dist/plus/integrations/reads/issueTracker.d.ts +9 -1
- package/dist/plus/integrations/reads/issueTracker.js +20 -3
- package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
- package/docs/integrations.md +4 -0
- package/package.json +2 -2
|
@@ -70,6 +70,7 @@ var TrelloIntegration = class extends IssuesIntegration {
|
|
|
70
70
|
}
|
|
71
71
|
async getProviderIssuesForProjectWithTruncation(session, project, options) {
|
|
72
72
|
if (!isIssueResourceDescriptor(project)) return void 0;
|
|
73
|
+
if (options?.state != null && options.state !== "open") throw new IntegrationReadUnavailableError(metadata.name, `issue state '${options.state}' is not supported`);
|
|
73
74
|
const appKey = this.requireAppKey(session);
|
|
74
75
|
const api = await this.getProvidersApi();
|
|
75
76
|
const tokenWithInfo = toTokenWithInfo(this.id, session);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trello.js","names":[],"sources":["../../../../../plus/integrations/src/providers/trello.ts"],"sourcesContent":["import type { Account } from '@gitlens/git/models/author.js';\nimport type { Issue, IssueShape } from '@gitlens/git/models/issue.js';\nimport type { IssueOrPullRequest, IssueOrPullRequestType } from '@gitlens/git/models/issueOrPullRequest.js';\nimport type { IssueResourceDescriptor, ResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport { isIssueResourceDescriptor } from '@gitlens/git/utils/resourceDescriptor.utils.js';\nimport type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider.js';\nimport type { ProviderAuthenticationSession } from '../authentication/models.js';\nimport { toTokenWithInfo } from '../authentication/models.js';\nimport { IssuesCloudHostIntegrationId } from '../constants.js';\nimport { IntegrationReadUnavailableError } from '../errors.js';\nimport type { IssuesForProjectOptions, ProjectIssuesDrain } from '../models/issueReads.js';\nimport { IssuesIntegration } from '../models/issuesIntegration.js';\nimport { fromProviderIssue, providersMetadata, toIssueShape } from './models.js';\n\nconst metadata = providersMetadata[IssuesCloudHostIntegrationId.Trello];\nconst authProvider = Object.freeze({ id: metadata.id, scopes: metadata.scopes });\n\n/** A Trello board, surfaced as both a \"resource\" (org analogue) and a \"project\" for issue reads. */\nexport interface TrelloBoardDescriptor extends IssueResourceDescriptor {}\n\nexport class TrelloIntegration extends IssuesIntegration<IssuesCloudHostIntegrationId.Trello> {\n\treadonly authProvider: IntegrationAuthenticationProviderDescriptor = authProvider;\n\n\toverride get id(): IssuesCloudHostIntegrationId.Trello {\n\t\treturn IssuesCloudHostIntegrationId.Trello;\n\t}\n\tprotected override get key(): 'trello' {\n\t\treturn 'trello';\n\t}\n\toverride get name(): string {\n\t\treturn metadata.name;\n\t}\n\toverride get domain(): string {\n\t\treturn metadata.domain;\n\t}\n\n\t/** The Trello client requires the app key paired with the token; a session without one can't read. */\n\tprivate appKeyFor(session: ProviderAuthenticationSession): string | undefined {\n\t\treturn session.appKey;\n\t}\n\n\t/**\n\t * Resolves the app key or throws — a session that authenticated but carries no app key can't read Trello,\n\t * and must be surfaced as a broken read (warning + fetchFailed) rather than an empty account.\n\t */\n\tprivate requireAppKey(session: ProviderAuthenticationSession): string {\n\t\tconst appKey = this.appKeyFor(session);\n\t\tif (appKey == null) {\n\t\t\tthrow new IntegrationReadUnavailableError(metadata.name, 'missing app key (session has no appKey)');\n\t\t}\n\t\treturn appKey;\n\t}\n\n\tprotected override async getProviderAccountForResource(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resource: ResourceDescriptor,\n\t): Promise<Account | undefined> {\n\t\tconst appKey = this.requireAppKey(session);\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst user = await api.getTrelloCurrentUser(toTokenWithInfo(this.id, session), appKey);\n\t\tif (user == null) return undefined;\n\n\t\treturn {\n\t\t\tprovider: this,\n\t\t\tid: user.id,\n\t\t\tname: user.name,\n\t\t\tusername: user.username,\n\t\t\temail: user.email,\n\t\t\tavatarUrl: user.avatarUrl ?? undefined,\n\t\t};\n\t}\n\n\tprotected override async getProviderResourcesForUser(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<TrelloBoardDescriptor[] | undefined> {\n\t\tconst appKey = this.requireAppKey(session);\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst boards = await api.getTrelloBoardsForCurrentUser(toTokenWithInfo(this.id, session), appKey);\n\t\treturn boards?.map(b => ({ key: b.id, id: b.id, name: b.name }));\n\t}\n\n\tprotected override getProviderProjectsForResources(\n\t\t_session: ProviderAuthenticationSession,\n\t\tresources: ResourceDescriptor[],\n\t): Promise<TrelloBoardDescriptor[] | undefined> {\n\t\t// Trello boards are both the resource and the project scope, so projects are the boards themselves.\n\t\treturn Promise.resolve(\n\t\t\tresources.filter(isIssueResourceDescriptor).map(r => ({ key: r.key, id: r.id, name: r.name })),\n\t\t);\n\t}\n\n\tprotected override async getProviderIssuesForProject(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: ResourceDescriptor,\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: ResourceDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<ProjectIssuesDrain | undefined> {\n\t\t// A non-issue descriptor genuinely has nothing to read (empty), but a missing app key is a broken read.\n\t\tif (!isIssueResourceDescriptor(project)) return undefined;\n\n\t\tconst appKey = this.requireAppKey(session);\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\n\t\t// Enrich issues with their board list names (card status), mirroring gitkraken.dev's Trello reads.\n\t\tconst lists = await api.getTrelloListsForBoard(tokenWithInfo, appKey, project.id);\n\t\tconst trelloBoardListsById = lists?.reduce<Record<string, { name: string }>>((acc, list) => {\n\t\t\tacc[list.id] = { name: list.name };\n\t\t\treturn acc;\n\t\t}, {});\n\t\tconst boardProject = {\n\t\t\tid: project.id,\n\t\t\tname: project.name,\n\t\t\tresourceId: project.id,\n\t\t\tresourceName: project.name,\n\t\t};\n\n\t\tconst result = await api.getTrelloIssuesForBoard(tokenWithInfo, appKey, project.id, {\n\t\t\tassigneeLogins: options?.user != null ? [options.user] : undefined,\n\t\t\ttrelloBoardListsById: trelloBoardListsById,\n\t\t\tsort: options?.sort,\n\t\t});\n\n\t\tconst values = result.values.flatMap(issue => {\n\t\t\tconst mapped = toIssueShape(issue, this);\n\t\t\treturn mapped == null ? [] : [{ ...mapped, project: boardProject } satisfies IssueShape];\n\t\t});\n\n\t\t// Trello's search caps results and reports the cap through `metadata.completeness` (partial/unknown),\n\t\t// never a cursor. Surface that as terminal truncation; there is no next page to fetch, so retrying the\n\t\t// same read cannot recover the omitted cards (D11).\n\t\tconst truncated = result.metadata != null && result.metadata.completeness !== 'complete';\n\t\treturn truncated\n\t\t\t? { values: values, truncated: true, recovery: 'none', metadata: result.metadata }\n\t\t\t: { values: values, truncated: false, metadata: result.metadata };\n\t}\n\n\tprotected override searchProviderMyIssues(\n\t\t_session: ProviderAuthenticationSession,\n\t\t_resources?: ResourceDescriptor[],\n\t\t_cancellation?: AbortSignal,\n\t): Promise<IssueShape[] | undefined> {\n\t\t// Trello has no cross-board \"issues assigned to me\" endpoint; issues are read per board via\n\t\t// getIssuesForProject. Callers that need them enumerate boards first.\n\t\treturn Promise.resolve(undefined);\n\t}\n\n\tprotected override getProviderLinkedIssueOrPullRequest(\n\t\t_session: ProviderAuthenticationSession,\n\t\t_resource: ResourceDescriptor,\n\t\t_id: { id: string; key: string },\n\t\t_type: undefined | IssueOrPullRequestType,\n\t): Promise<IssueOrPullRequest | undefined> {\n\t\treturn Promise.resolve(undefined);\n\t}\n\n\tprotected override async getProviderIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: ResourceDescriptor,\n\t\tid: string,\n\t): Promise<Issue | undefined> {\n\t\tif (!isIssueResourceDescriptor(resource)) return undefined;\n\n\t\tconst appKey = this.requireAppKey(session);\n\t\tconst api = await this.getProvidersApi();\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\tconst lists = await api.getTrelloListsForBoard(tokenWithInfo, appKey, resource.id);\n\t\tconst trelloBoardListsById = lists?.reduce<Record<string, { name: string }>>((acc, list) => {\n\t\t\tacc[list.id] = { name: list.name };\n\t\t\treturn acc;\n\t\t}, {});\n\n\t\t// Branch-association round-trips use the encoded entity identifier's stable Trello card id. Resolve that\n\t\t// directly so large boards don't require a capped whole-board scan; keep a numeric idShort fallback so\n\t\t// legacy/manual callers that still pass the board-local display number continue to resolve.\n\t\tlet issue = await api.getTrelloCard(tokenWithInfo, appKey, id, {\n\t\t\ttrelloBoardListsById: trelloBoardListsById,\n\t\t});\n\t\tif (issue == null && /^[0-9]+$/.test(id)) {\n\t\t\tissue = (\n\t\t\t\tawait api.getTrelloIssuesForBoard(tokenWithInfo, appKey, resource.id, {\n\t\t\t\t\ttrelloBoardListsById: trelloBoardListsById,\n\t\t\t\t})\n\t\t\t).values.find(issue => issue.number === id);\n\t\t}\n\n\t\treturn issue != null\n\t\t\t? fromProviderIssue(issue, this, {\n\t\t\t\t\tproject: {\n\t\t\t\t\t\tid: resource.id,\n\t\t\t\t\t\tname: resource.name,\n\t\t\t\t\t\tresourceId: resource.id,\n\t\t\t\t\t\tresourceName: resource.name,\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t: undefined;\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"trello.js","names":[],"sources":["../../../../../plus/integrations/src/providers/trello.ts"],"sourcesContent":["import type { Account } from '@gitlens/git/models/author.js';\nimport type { Issue, IssueShape } from '@gitlens/git/models/issue.js';\nimport type { IssueOrPullRequest, IssueOrPullRequestType } from '@gitlens/git/models/issueOrPullRequest.js';\nimport type { IssueResourceDescriptor, ResourceDescriptor } from '@gitlens/git/models/resourceDescriptor.js';\nimport { isIssueResourceDescriptor } from '@gitlens/git/utils/resourceDescriptor.utils.js';\nimport type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthenticationProvider.js';\nimport type { ProviderAuthenticationSession } from '../authentication/models.js';\nimport { toTokenWithInfo } from '../authentication/models.js';\nimport { IssuesCloudHostIntegrationId } from '../constants.js';\nimport { IntegrationReadUnavailableError } from '../errors.js';\nimport type { IssuesForProjectOptions, ProjectIssuesDrain } from '../models/issueReads.js';\nimport { IssuesIntegration } from '../models/issuesIntegration.js';\nimport { fromProviderIssue, providersMetadata, toIssueShape } from './models.js';\n\nconst metadata = providersMetadata[IssuesCloudHostIntegrationId.Trello];\nconst authProvider = Object.freeze({ id: metadata.id, scopes: metadata.scopes });\n\n/** A Trello board, surfaced as both a \"resource\" (org analogue) and a \"project\" for issue reads. */\nexport interface TrelloBoardDescriptor extends IssueResourceDescriptor {}\n\nexport class TrelloIntegration extends IssuesIntegration<IssuesCloudHostIntegrationId.Trello> {\n\treadonly authProvider: IntegrationAuthenticationProviderDescriptor = authProvider;\n\n\toverride get id(): IssuesCloudHostIntegrationId.Trello {\n\t\treturn IssuesCloudHostIntegrationId.Trello;\n\t}\n\tprotected override get key(): 'trello' {\n\t\treturn 'trello';\n\t}\n\toverride get name(): string {\n\t\treturn metadata.name;\n\t}\n\toverride get domain(): string {\n\t\treturn metadata.domain;\n\t}\n\n\t/** The Trello client requires the app key paired with the token; a session without one can't read. */\n\tprivate appKeyFor(session: ProviderAuthenticationSession): string | undefined {\n\t\treturn session.appKey;\n\t}\n\n\t/**\n\t * Resolves the app key or throws — a session that authenticated but carries no app key can't read Trello,\n\t * and must be surfaced as a broken read (warning + fetchFailed) rather than an empty account.\n\t */\n\tprivate requireAppKey(session: ProviderAuthenticationSession): string {\n\t\tconst appKey = this.appKeyFor(session);\n\t\tif (appKey == null) {\n\t\t\tthrow new IntegrationReadUnavailableError(metadata.name, 'missing app key (session has no appKey)');\n\t\t}\n\t\treturn appKey;\n\t}\n\n\tprotected override async getProviderAccountForResource(\n\t\tsession: ProviderAuthenticationSession,\n\t\t_resource: ResourceDescriptor,\n\t): Promise<Account | undefined> {\n\t\tconst appKey = this.requireAppKey(session);\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst user = await api.getTrelloCurrentUser(toTokenWithInfo(this.id, session), appKey);\n\t\tif (user == null) return undefined;\n\n\t\treturn {\n\t\t\tprovider: this,\n\t\t\tid: user.id,\n\t\t\tname: user.name,\n\t\t\tusername: user.username,\n\t\t\temail: user.email,\n\t\t\tavatarUrl: user.avatarUrl ?? undefined,\n\t\t};\n\t}\n\n\tprotected override async getProviderResourcesForUser(\n\t\tsession: ProviderAuthenticationSession,\n\t): Promise<TrelloBoardDescriptor[] | undefined> {\n\t\tconst appKey = this.requireAppKey(session);\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst boards = await api.getTrelloBoardsForCurrentUser(toTokenWithInfo(this.id, session), appKey);\n\t\treturn boards?.map(b => ({ key: b.id, id: b.id, name: b.name }));\n\t}\n\n\tprotected override getProviderProjectsForResources(\n\t\t_session: ProviderAuthenticationSession,\n\t\tresources: ResourceDescriptor[],\n\t): Promise<TrelloBoardDescriptor[] | undefined> {\n\t\t// Trello boards are both the resource and the project scope, so projects are the boards themselves.\n\t\treturn Promise.resolve(\n\t\t\tresources.filter(isIssueResourceDescriptor).map(r => ({ key: r.key, id: r.id, name: r.name })),\n\t\t);\n\t}\n\n\tprotected override async getProviderIssuesForProject(\n\t\tsession: ProviderAuthenticationSession,\n\t\tproject: ResourceDescriptor,\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: ResourceDescriptor,\n\t\toptions?: IssuesForProjectOptions,\n\t): Promise<ProjectIssuesDrain | undefined> {\n\t\t// A non-issue descriptor genuinely has nothing to read (empty), but a missing app key is a broken read.\n\t\tif (!isIssueResourceDescriptor(project)) return undefined;\n\n\t\t// The board read always excludes archived cards, and a card has no other state, so only open cards can be\n\t\t// read. Refused rather than ignored: answering `closed` with the open cards would publish them as closed.\n\t\tif (options?.state != null && options.state !== 'open') {\n\t\t\tthrow new IntegrationReadUnavailableError(metadata.name, `issue state '${options.state}' is not supported`);\n\t\t}\n\n\t\tconst appKey = this.requireAppKey(session);\n\n\t\tconst api = await this.getProvidersApi();\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\n\t\t// Enrich issues with their board list names (card status), mirroring gitkraken.dev's Trello reads.\n\t\tconst lists = await api.getTrelloListsForBoard(tokenWithInfo, appKey, project.id);\n\t\tconst trelloBoardListsById = lists?.reduce<Record<string, { name: string }>>((acc, list) => {\n\t\t\tacc[list.id] = { name: list.name };\n\t\t\treturn acc;\n\t\t}, {});\n\t\tconst boardProject = {\n\t\t\tid: project.id,\n\t\t\tname: project.name,\n\t\t\tresourceId: project.id,\n\t\t\tresourceName: project.name,\n\t\t};\n\n\t\tconst result = await api.getTrelloIssuesForBoard(tokenWithInfo, appKey, project.id, {\n\t\t\tassigneeLogins: options?.user != null ? [options.user] : undefined,\n\t\t\ttrelloBoardListsById: trelloBoardListsById,\n\t\t\tsort: options?.sort,\n\t\t});\n\n\t\tconst values = result.values.flatMap(issue => {\n\t\t\tconst mapped = toIssueShape(issue, this);\n\t\t\treturn mapped == null ? [] : [{ ...mapped, project: boardProject } satisfies IssueShape];\n\t\t});\n\n\t\t// Trello's search caps results and reports the cap through `metadata.completeness` (partial/unknown),\n\t\t// never a cursor. Surface that as terminal truncation; there is no next page to fetch, so retrying the\n\t\t// same read cannot recover the omitted cards (D11).\n\t\tconst truncated = result.metadata != null && result.metadata.completeness !== 'complete';\n\t\treturn truncated\n\t\t\t? { values: values, truncated: true, recovery: 'none', metadata: result.metadata }\n\t\t\t: { values: values, truncated: false, metadata: result.metadata };\n\t}\n\n\tprotected override searchProviderMyIssues(\n\t\t_session: ProviderAuthenticationSession,\n\t\t_resources?: ResourceDescriptor[],\n\t\t_cancellation?: AbortSignal,\n\t): Promise<IssueShape[] | undefined> {\n\t\t// Trello has no cross-board \"issues assigned to me\" endpoint; issues are read per board via\n\t\t// getIssuesForProject. Callers that need them enumerate boards first.\n\t\treturn Promise.resolve(undefined);\n\t}\n\n\tprotected override getProviderLinkedIssueOrPullRequest(\n\t\t_session: ProviderAuthenticationSession,\n\t\t_resource: ResourceDescriptor,\n\t\t_id: { id: string; key: string },\n\t\t_type: undefined | IssueOrPullRequestType,\n\t): Promise<IssueOrPullRequest | undefined> {\n\t\treturn Promise.resolve(undefined);\n\t}\n\n\tprotected override async getProviderIssue(\n\t\tsession: ProviderAuthenticationSession,\n\t\tresource: ResourceDescriptor,\n\t\tid: string,\n\t): Promise<Issue | undefined> {\n\t\tif (!isIssueResourceDescriptor(resource)) return undefined;\n\n\t\tconst appKey = this.requireAppKey(session);\n\t\tconst api = await this.getProvidersApi();\n\t\tconst tokenWithInfo = toTokenWithInfo(this.id, session);\n\t\tconst lists = await api.getTrelloListsForBoard(tokenWithInfo, appKey, resource.id);\n\t\tconst trelloBoardListsById = lists?.reduce<Record<string, { name: string }>>((acc, list) => {\n\t\t\tacc[list.id] = { name: list.name };\n\t\t\treturn acc;\n\t\t}, {});\n\n\t\t// Branch-association round-trips use the encoded entity identifier's stable Trello card id. Resolve that\n\t\t// directly so large boards don't require a capped whole-board scan; keep a numeric idShort fallback so\n\t\t// legacy/manual callers that still pass the board-local display number continue to resolve.\n\t\tlet issue = await api.getTrelloCard(tokenWithInfo, appKey, id, {\n\t\t\ttrelloBoardListsById: trelloBoardListsById,\n\t\t});\n\t\tif (issue == null && /^[0-9]+$/.test(id)) {\n\t\t\tissue = (\n\t\t\t\tawait api.getTrelloIssuesForBoard(tokenWithInfo, appKey, resource.id, {\n\t\t\t\t\ttrelloBoardListsById: trelloBoardListsById,\n\t\t\t\t})\n\t\t\t).values.find(issue => issue.number === id);\n\t\t}\n\n\t\treturn issue != null\n\t\t\t? fromProviderIssue(issue, this, {\n\t\t\t\t\tproject: {\n\t\t\t\t\t\tid: resource.id,\n\t\t\t\t\t\tname: resource.name,\n\t\t\t\t\t\tresourceId: resource.id,\n\t\t\t\t\t\tresourceName: resource.name,\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t: undefined;\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { IssueStateFilter } from "../../../git/models/issue.js";
|
|
1
2
|
import { IntegrationIds } from "../constants.js";
|
|
2
3
|
//#region ../plus/integrations/src/reads/cursors.d.ts
|
|
3
4
|
/**
|
|
@@ -21,6 +22,12 @@ export interface IssueTrackerPageCursor {
|
|
|
21
22
|
retryPages?: number[];
|
|
22
23
|
retryProjects?: string[];
|
|
23
24
|
completedProjects?: string[];
|
|
25
|
+
/**
|
|
26
|
+
* The `state` the read that minted this cursor asked for, omitted for open. `completedProjects` records what was
|
|
27
|
+
* already drained UNDER that state, so resuming it under another would skip projects whose other-state issues
|
|
28
|
+
* were never read.
|
|
29
|
+
*/
|
|
30
|
+
state?: 'closed' | 'all';
|
|
24
31
|
}
|
|
25
32
|
export declare function parseIssueTrackerPageCursor(cursor: string | undefined): IssueTrackerPageCursor | undefined;
|
|
26
33
|
export declare function toIssueTrackerPageCursor(options: {
|
|
@@ -30,6 +37,7 @@ export declare function toIssueTrackerPageCursor(options: {
|
|
|
30
37
|
retryPages: readonly number[];
|
|
31
38
|
retryProjects: readonly string[];
|
|
32
39
|
completedProjects?: readonly string[];
|
|
40
|
+
state?: IssueStateFilter;
|
|
33
41
|
}): string | undefined;
|
|
34
42
|
/** Identifies one org slice of the fan-out. Connection and domain are part of the key: two accounts, or two
|
|
35
43
|
* self-managed hosts, can each have an org of the same name. */
|
|
@@ -29,7 +29,8 @@ function parseIssueTrackerPageCursor(cursor) {
|
|
|
29
29
|
...typeof parsed.nextPage === "number" && Number.isSafeInteger(parsed.nextPage) && parsed.nextPage > 0 ? { nextPage: parsed.nextPage } : {},
|
|
30
30
|
...retryPages != null ? { retryPages } : {},
|
|
31
31
|
...retryProjects != null ? { retryProjects } : {},
|
|
32
|
-
...completedProjects != null ? { completedProjects } : {}
|
|
32
|
+
...completedProjects != null ? { completedProjects } : {},
|
|
33
|
+
...parsed.state === "closed" || parsed.state === "all" ? { state: parsed.state } : {}
|
|
33
34
|
};
|
|
34
35
|
} catch {
|
|
35
36
|
return;
|
|
@@ -40,7 +41,7 @@ function toIssueTrackerPageCursor(options) {
|
|
|
40
41
|
const retryProjects = [...new Set(options.retryProjects)].sort();
|
|
41
42
|
const completedProjects = retryPages.length > 0 || retryProjects.length > 0 || options.nextPage != null ? [...new Set(options.completedProjects ?? [])].sort() : [];
|
|
42
43
|
if (retryPages.length === 0 && retryProjects.length === 0 && options.nextPage == null) return;
|
|
43
|
-
if (retryPages.length === 0 && retryProjects.length === 0 && completedProjects.length === 0 && options.nextPage != null && options.unpaged !== true) return toPageCursor(options.nextPage);
|
|
44
|
+
if (retryPages.length === 0 && retryProjects.length === 0 && completedProjects.length === 0 && options.nextPage != null && options.unpaged !== true && (options.state == null || options.state === "open")) return toPageCursor(options.nextPage);
|
|
44
45
|
return JSON.stringify({
|
|
45
46
|
type: "issue-tracker-page",
|
|
46
47
|
currentPage: options.currentPage,
|
|
@@ -48,7 +49,8 @@ function toIssueTrackerPageCursor(options) {
|
|
|
48
49
|
...options.nextPage != null ? { nextPage: options.nextPage } : {},
|
|
49
50
|
...retryPages.length > 0 ? { retryPages } : {},
|
|
50
51
|
...retryProjects.length > 0 ? { retryProjects } : {},
|
|
51
|
-
...completedProjects.length > 0 ? { completedProjects } : {}
|
|
52
|
+
...completedProjects.length > 0 ? { completedProjects } : {},
|
|
53
|
+
...options.state === "closed" || options.state === "all" ? { state: options.state } : {}
|
|
52
54
|
});
|
|
53
55
|
}
|
|
54
56
|
/** Whether a bundle entry addresses the same org slice as `org`, comparing domains by host. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cursors.js","names":[],"sources":["../../../../../plus/integrations/src/reads/cursors.ts"],"sourcesContent":["import type { IntegrationIds } from '../constants.js';\nimport { toPageCursor } from '../providers/utils/providerPaging.js';\nimport { hostFromDomain } from '../utils/domain.utils.js';\nimport { pageToCursor } from './paging.js';\n\n/**\n * The COMPOSITE cursors, for the two reads a single provider cursor can't address.\n *\n * A plain paged read threads the provider's own opaque continuation. These two fan out instead — the\n * issue-tracker read over a window of projects, the broaden read over a set of orgs — so one round produces\n * several provider positions plus the bookkeeping needed to resume: which scopes failed and should be retried,\n * and which are already drained and must NOT be re-read (a cursor-only provider handed a fresh page-1 request\n * answers with its first page again, duplicating items).\n *\n * Both encodings are opaque to consumers and are parsed defensively: a cursor is caller-supplied data that may\n * be stale, truncated, or from another read entirely, so anything unrecognized degrades to \"no cursor\" (which\n * falls back to page paging) rather than throwing or being forwarded to a provider as a foreign string.\n */\n\n// ---------------------------------------------------------------------------------------------------------\n// Issue-tracker page cursor (Jira/Linear/Trello: resource → project, paginated by a window of PROJECTS)\n// ---------------------------------------------------------------------------------------------------------\n\nexport interface IssueTrackerPageCursor {\n\ttype: 'issue-tracker-page';\n\tcurrentPage: number;\n\tunpaged?: boolean;\n\tnextPage?: number;\n\tretryPages?: number[];\n\tretryProjects?: string[];\n\tcompletedProjects?: string[];\n}\n\n/** Positive safe integers only, deduped; `undefined` when nothing survives (so the field is omitted). */\nfunction positiveIntegers(values: unknown): number[] | undefined {\n\tif (!Array.isArray(values)) return undefined;\n\n\tconst valid = values.filter(\n\t\t(value): value is number => typeof value === 'number' && Number.isSafeInteger(value) && value > 0,\n\t);\n\treturn valid.length > 0 ? [...new Set(valid)] : undefined;\n}\n\n/** Non-empty strings only, deduped; `undefined` when nothing survives (so the field is omitted). */\nfunction nonEmptyStrings(values: unknown): string[] | undefined {\n\tif (!Array.isArray(values)) return undefined;\n\n\tconst valid = values.filter((value): value is string => typeof value === 'string' && value.length > 0);\n\treturn valid.length > 0 ? [...new Set(valid)] : undefined;\n}\n\nexport function parseIssueTrackerPageCursor(cursor: string | undefined): IssueTrackerPageCursor | undefined {\n\tif (cursor == null) return undefined;\n\n\ttry {\n\t\tconst parsed = JSON.parse(cursor) as Partial<IssueTrackerPageCursor>;\n\t\tif (\n\t\t\tparsed.type !== 'issue-tracker-page' ||\n\t\t\ttypeof parsed.currentPage !== 'number' ||\n\t\t\t!Number.isSafeInteger(parsed.currentPage) ||\n\t\t\tparsed.currentPage < 1\n\t\t) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst retryPages = positiveIntegers(parsed.retryPages);\n\t\tconst retryProjects = nonEmptyStrings(parsed.retryProjects);\n\t\tconst completedProjects = nonEmptyStrings(parsed.completedProjects);\n\t\treturn {\n\t\t\ttype: 'issue-tracker-page',\n\t\t\tcurrentPage: parsed.currentPage,\n\t\t\t...(parsed.unpaged === true ? { unpaged: true } : {}),\n\t\t\t...(typeof parsed.nextPage === 'number' && Number.isSafeInteger(parsed.nextPage) && parsed.nextPage > 0\n\t\t\t\t? { nextPage: parsed.nextPage }\n\t\t\t\t: {}),\n\t\t\t...(retryPages != null ? { retryPages: retryPages } : {}),\n\t\t\t...(retryProjects != null ? { retryProjects: retryProjects } : {}),\n\t\t\t...(completedProjects != null ? { completedProjects: completedProjects } : {}),\n\t\t};\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport function toIssueTrackerPageCursor(options: {\n\tcurrentPage: number;\n\tunpaged?: boolean;\n\tnextPage?: number;\n\tretryPages: readonly number[];\n\tretryProjects: readonly string[];\n\tcompletedProjects?: readonly string[];\n}): string | undefined {\n\tconst retryPages = [...new Set(options.retryPages)].sort((a, b) => a - b);\n\tconst retryProjects = [...new Set(options.retryProjects)].sort();\n\tconst completedProjects =\n\t\tretryPages.length > 0 || retryProjects.length > 0 || options.nextPage != null\n\t\t\t? [...new Set(options.completedProjects ?? [])].sort()\n\t\t\t: [];\n\tif (retryPages.length === 0 && retryProjects.length === 0 && options.nextPage == null) {\n\t\treturn undefined;\n\t}\n\t// Nothing to carry beyond the next window: emit the plain page cursor instead of a composite one, so an\n\t// ordinary forward read round-trips through the same encoding every other paged read uses.\n\tif (\n\t\tretryPages.length === 0 &&\n\t\tretryProjects.length === 0 &&\n\t\tcompletedProjects.length === 0 &&\n\t\toptions.nextPage != null &&\n\t\toptions.unpaged !== true\n\t) {\n\t\treturn toPageCursor(options.nextPage);\n\t}\n\treturn JSON.stringify({\n\t\ttype: 'issue-tracker-page',\n\t\tcurrentPage: options.currentPage,\n\t\t...(options.unpaged === true ? { unpaged: true } : {}),\n\t\t...(options.nextPage != null ? { nextPage: options.nextPage } : {}),\n\t\t...(retryPages.length > 0 ? { retryPages: retryPages } : {}),\n\t\t...(retryProjects.length > 0 ? { retryProjects: retryProjects } : {}),\n\t\t...(completedProjects.length > 0 ? { completedProjects: completedProjects } : {}),\n\t} satisfies IssueTrackerPageCursor);\n}\n\n// ---------------------------------------------------------------------------------------------------------\n// Broaden-issues cursor bundle (one provider position per org in the fan-out)\n// ---------------------------------------------------------------------------------------------------------\n\n/** Identifies one org slice of the fan-out. Connection and domain are part of the key: two accounts, or two\n * self-managed hosts, can each have an org of the same name. */\nexport interface BroadenIssuesOrgKey {\n\tproviderId: IntegrationIds;\n\tname: string;\n\tconnectionId?: string;\n\tdomain?: string;\n}\n\n/** One org's position in the bundle: either the provider's own cursor, or a page to retry after a failure. */\nexport interface BroadenIssuesOrgCursor {\n\tproviderId: IntegrationIds;\n\torg: string;\n\tconnectionId?: string;\n\tdomain?: string;\n\tcursor?: string;\n\tretryPage?: number;\n}\n\nexport type BroadenIssuesExhaustedOrg = Omit<BroadenIssuesOrgCursor, 'cursor' | 'retryPage'>;\n\n/** Whether a bundle entry addresses the same org slice as `org`, comparing domains by host. */\nfunction matchesOrg(\n\tentry: { providerId?: IntegrationIds; org?: string; connectionId?: string; domain?: string },\n\torg: BroadenIssuesOrgKey,\n): boolean {\n\treturn (\n\t\tentry.providerId === org.providerId &&\n\t\tentry.org === org.name &&\n\t\tentry.connectionId === org.connectionId &&\n\t\t(hostFromDomain(entry.domain) ?? entry.domain) === (hostFromDomain(org.domain) ?? org.domain)\n\t);\n}\n\n/**\n * The cursor to send for one org in this round: its entry in the bundle, or a synthesized page cursor.\n *\n * A single-org fan-out has no bundle to key into — its cursor IS the provider's, threaded straight through.\n */\nexport function getBroadenIssuesCursor(\n\tcursor: string | undefined,\n\torg: BroadenIssuesOrgKey,\n\tpage: number,\n\torgCount: number,\n): string | undefined {\n\tif (orgCount === 1) return cursor ?? pageToCursor(page);\n\n\tif (cursor != null) {\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(cursor) as { cursors?: BroadenIssuesOrgCursor[] };\n\t\t\tconst match = parsed.cursors?.find(c => matchesOrg(c, org));\n\t\t\tif (match?.cursor != null) return match.cursor;\n\t\t\tif (typeof match?.retryPage === 'number' && Number.isSafeInteger(match.retryPage) && match.retryPage > 0) {\n\t\t\t\treturn toPageCursor(match.retryPage);\n\t\t\t}\n\t\t} catch {}\n\t}\n\n\treturn pageToCursor(page);\n}\n\n/**\n * Whether a prior round already drained this org (multi-org fan-out only). Once an org runs out of pages while\n * another org keeps paging, the bundle records it as exhausted so the next round skips it instead of re-issuing\n * a page-1 read — which cursor-only providers (having no page-number cursor to honor) would answer with their\n * first page again, duplicating results.\n */\nexport function isBroadenIssuesOrgExhausted(\n\tcursor: string | undefined,\n\torg: BroadenIssuesOrgKey,\n\torgCount: number,\n): boolean {\n\tif (orgCount === 1 || cursor == null) return false;\n\n\ttry {\n\t\tconst parsed = JSON.parse(cursor) as { exhausted?: BroadenIssuesExhaustedOrg[] };\n\t\treturn parsed.exhausted?.some(e => matchesOrg(e, org)) ?? false;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/** Encodes the round's per-org positions. A single-org fan-out emits that org's cursor directly, unwrapped. */\nexport function toBroadenIssuesCursor(\n\tcursors: BroadenIssuesOrgCursor[],\n\texhausted: BroadenIssuesExhaustedOrg[],\n\torgCount: number,\n): string | undefined {\n\tif (cursors.length === 0) return undefined;\n\tif (orgCount === 1) {\n\t\treturn cursors[0].cursor ?? (cursors[0].retryPage != null ? toPageCursor(cursors[0].retryPage) : undefined);\n\t}\n\n\t// Carry the exhausted orgs alongside the still-active cursors so the next round can skip them (see\n\t// isBroadenIssuesOrgExhausted). Only meaningful while at least one org still has more to read.\n\treturn JSON.stringify({ cursors: cursors, exhausted: exhausted });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cursors.js","names":[],"sources":["../../../../../plus/integrations/src/reads/cursors.ts"],"sourcesContent":["import type { IssueStateFilter } from '@gitlens/git/models/issue.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { toPageCursor } from '../providers/utils/providerPaging.js';\nimport { hostFromDomain } from '../utils/domain.utils.js';\nimport { pageToCursor } from './paging.js';\n\n/**\n * The COMPOSITE cursors, for the two reads a single provider cursor can't address.\n *\n * A plain paged read threads the provider's own opaque continuation. These two fan out instead — the\n * issue-tracker read over a window of projects, the broaden read over a set of orgs — so one round produces\n * several provider positions plus the bookkeeping needed to resume: which scopes failed and should be retried,\n * and which are already drained and must NOT be re-read (a cursor-only provider handed a fresh page-1 request\n * answers with its first page again, duplicating items).\n *\n * Both encodings are opaque to consumers and are parsed defensively: a cursor is caller-supplied data that may\n * be stale, truncated, or from another read entirely, so anything unrecognized degrades to \"no cursor\" (which\n * falls back to page paging) rather than throwing or being forwarded to a provider as a foreign string.\n */\n\n// ---------------------------------------------------------------------------------------------------------\n// Issue-tracker page cursor (Jira/Linear/Trello: resource → project, paginated by a window of PROJECTS)\n// ---------------------------------------------------------------------------------------------------------\n\nexport interface IssueTrackerPageCursor {\n\ttype: 'issue-tracker-page';\n\tcurrentPage: number;\n\tunpaged?: boolean;\n\tnextPage?: number;\n\tretryPages?: number[];\n\tretryProjects?: string[];\n\tcompletedProjects?: string[];\n\t/**\n\t * The `state` the read that minted this cursor asked for, omitted for open. `completedProjects` records what was\n\t * already drained UNDER that state, so resuming it under another would skip projects whose other-state issues\n\t * were never read.\n\t */\n\tstate?: 'closed' | 'all';\n}\n\n/** Positive safe integers only, deduped; `undefined` when nothing survives (so the field is omitted). */\nfunction positiveIntegers(values: unknown): number[] | undefined {\n\tif (!Array.isArray(values)) return undefined;\n\n\tconst valid = values.filter(\n\t\t(value): value is number => typeof value === 'number' && Number.isSafeInteger(value) && value > 0,\n\t);\n\treturn valid.length > 0 ? [...new Set(valid)] : undefined;\n}\n\n/** Non-empty strings only, deduped; `undefined` when nothing survives (so the field is omitted). */\nfunction nonEmptyStrings(values: unknown): string[] | undefined {\n\tif (!Array.isArray(values)) return undefined;\n\n\tconst valid = values.filter((value): value is string => typeof value === 'string' && value.length > 0);\n\treturn valid.length > 0 ? [...new Set(valid)] : undefined;\n}\n\nexport function parseIssueTrackerPageCursor(cursor: string | undefined): IssueTrackerPageCursor | undefined {\n\tif (cursor == null) return undefined;\n\n\ttry {\n\t\tconst parsed = JSON.parse(cursor) as Partial<IssueTrackerPageCursor>;\n\t\tif (\n\t\t\tparsed.type !== 'issue-tracker-page' ||\n\t\t\ttypeof parsed.currentPage !== 'number' ||\n\t\t\t!Number.isSafeInteger(parsed.currentPage) ||\n\t\t\tparsed.currentPage < 1\n\t\t) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst retryPages = positiveIntegers(parsed.retryPages);\n\t\tconst retryProjects = nonEmptyStrings(parsed.retryProjects);\n\t\tconst completedProjects = nonEmptyStrings(parsed.completedProjects);\n\t\treturn {\n\t\t\ttype: 'issue-tracker-page',\n\t\t\tcurrentPage: parsed.currentPage,\n\t\t\t...(parsed.unpaged === true ? { unpaged: true } : {}),\n\t\t\t...(typeof parsed.nextPage === 'number' && Number.isSafeInteger(parsed.nextPage) && parsed.nextPage > 0\n\t\t\t\t? { nextPage: parsed.nextPage }\n\t\t\t\t: {}),\n\t\t\t...(retryPages != null ? { retryPages: retryPages } : {}),\n\t\t\t...(retryProjects != null ? { retryProjects: retryProjects } : {}),\n\t\t\t...(completedProjects != null ? { completedProjects: completedProjects } : {}),\n\t\t\t...(parsed.state === 'closed' || parsed.state === 'all' ? { state: parsed.state } : {}),\n\t\t};\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport function toIssueTrackerPageCursor(options: {\n\tcurrentPage: number;\n\tunpaged?: boolean;\n\tnextPage?: number;\n\tretryPages: readonly number[];\n\tretryProjects: readonly string[];\n\tcompletedProjects?: readonly string[];\n\tstate?: IssueStateFilter;\n}): string | undefined {\n\tconst retryPages = [...new Set(options.retryPages)].sort((a, b) => a - b);\n\tconst retryProjects = [...new Set(options.retryProjects)].sort();\n\tconst completedProjects =\n\t\tretryPages.length > 0 || retryProjects.length > 0 || options.nextPage != null\n\t\t\t? [...new Set(options.completedProjects ?? [])].sort()\n\t\t\t: [];\n\tif (retryPages.length === 0 && retryProjects.length === 0 && options.nextPage == null) {\n\t\treturn undefined;\n\t}\n\t// Nothing to carry beyond the next window: emit the plain page cursor instead of a composite one, so an\n\t// ordinary forward read round-trips through the same encoding every other paged read uses.\n\tif (\n\t\tretryPages.length === 0 &&\n\t\tretryProjects.length === 0 &&\n\t\tcompletedProjects.length === 0 &&\n\t\toptions.nextPage != null &&\n\t\toptions.unpaged !== true &&\n\t\t// A page cursor carries no state, so a narrowed read keeps the composite one: resumed under another state, a\n\t\t// plain cursor would start at its window and skip the earlier projects' issues of that state.\n\t\t(options.state == null || options.state === 'open')\n\t) {\n\t\treturn toPageCursor(options.nextPage);\n\t}\n\treturn JSON.stringify({\n\t\ttype: 'issue-tracker-page',\n\t\tcurrentPage: options.currentPage,\n\t\t...(options.unpaged === true ? { unpaged: true } : {}),\n\t\t...(options.nextPage != null ? { nextPage: options.nextPage } : {}),\n\t\t...(retryPages.length > 0 ? { retryPages: retryPages } : {}),\n\t\t...(retryProjects.length > 0 ? { retryProjects: retryProjects } : {}),\n\t\t...(completedProjects.length > 0 ? { completedProjects: completedProjects } : {}),\n\t\t...(options.state === 'closed' || options.state === 'all' ? { state: options.state } : {}),\n\t} satisfies IssueTrackerPageCursor);\n}\n\n// ---------------------------------------------------------------------------------------------------------\n// Broaden-issues cursor bundle (one provider position per org in the fan-out)\n// ---------------------------------------------------------------------------------------------------------\n\n/** Identifies one org slice of the fan-out. Connection and domain are part of the key: two accounts, or two\n * self-managed hosts, can each have an org of the same name. */\nexport interface BroadenIssuesOrgKey {\n\tproviderId: IntegrationIds;\n\tname: string;\n\tconnectionId?: string;\n\tdomain?: string;\n}\n\n/** One org's position in the bundle: either the provider's own cursor, or a page to retry after a failure. */\nexport interface BroadenIssuesOrgCursor {\n\tproviderId: IntegrationIds;\n\torg: string;\n\tconnectionId?: string;\n\tdomain?: string;\n\tcursor?: string;\n\tretryPage?: number;\n}\n\nexport type BroadenIssuesExhaustedOrg = Omit<BroadenIssuesOrgCursor, 'cursor' | 'retryPage'>;\n\n/** Whether a bundle entry addresses the same org slice as `org`, comparing domains by host. */\nfunction matchesOrg(\n\tentry: { providerId?: IntegrationIds; org?: string; connectionId?: string; domain?: string },\n\torg: BroadenIssuesOrgKey,\n): boolean {\n\treturn (\n\t\tentry.providerId === org.providerId &&\n\t\tentry.org === org.name &&\n\t\tentry.connectionId === org.connectionId &&\n\t\t(hostFromDomain(entry.domain) ?? entry.domain) === (hostFromDomain(org.domain) ?? org.domain)\n\t);\n}\n\n/**\n * The cursor to send for one org in this round: its entry in the bundle, or a synthesized page cursor.\n *\n * A single-org fan-out has no bundle to key into — its cursor IS the provider's, threaded straight through.\n */\nexport function getBroadenIssuesCursor(\n\tcursor: string | undefined,\n\torg: BroadenIssuesOrgKey,\n\tpage: number,\n\torgCount: number,\n): string | undefined {\n\tif (orgCount === 1) return cursor ?? pageToCursor(page);\n\n\tif (cursor != null) {\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(cursor) as { cursors?: BroadenIssuesOrgCursor[] };\n\t\t\tconst match = parsed.cursors?.find(c => matchesOrg(c, org));\n\t\t\tif (match?.cursor != null) return match.cursor;\n\t\t\tif (typeof match?.retryPage === 'number' && Number.isSafeInteger(match.retryPage) && match.retryPage > 0) {\n\t\t\t\treturn toPageCursor(match.retryPage);\n\t\t\t}\n\t\t} catch {}\n\t}\n\n\treturn pageToCursor(page);\n}\n\n/**\n * Whether a prior round already drained this org (multi-org fan-out only). Once an org runs out of pages while\n * another org keeps paging, the bundle records it as exhausted so the next round skips it instead of re-issuing\n * a page-1 read — which cursor-only providers (having no page-number cursor to honor) would answer with their\n * first page again, duplicating results.\n */\nexport function isBroadenIssuesOrgExhausted(\n\tcursor: string | undefined,\n\torg: BroadenIssuesOrgKey,\n\torgCount: number,\n): boolean {\n\tif (orgCount === 1 || cursor == null) return false;\n\n\ttry {\n\t\tconst parsed = JSON.parse(cursor) as { exhausted?: BroadenIssuesExhaustedOrg[] };\n\t\treturn parsed.exhausted?.some(e => matchesOrg(e, org)) ?? false;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/** Encodes the round's per-org positions. A single-org fan-out emits that org's cursor directly, unwrapped. */\nexport function toBroadenIssuesCursor(\n\tcursors: BroadenIssuesOrgCursor[],\n\texhausted: BroadenIssuesExhaustedOrg[],\n\torgCount: number,\n): string | undefined {\n\tif (cursors.length === 0) return undefined;\n\tif (orgCount === 1) {\n\t\treturn cursors[0].cursor ?? (cursors[0].retryPage != null ? toPageCursor(cursors[0].retryPage) : undefined);\n\t}\n\n\t// Carry the exhausted orgs alongside the still-active cursors so the next round can skip them (see\n\t// isBroadenIssuesOrgExhausted). Only meaningful while at least one org still has more to read.\n\treturn JSON.stringify({ cursors: cursors, exhausted: exhausted });\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { PullRequestFilter, PullRequestStateFilter } from "../../../git/models/pullRequest.js";
|
|
2
2
|
import { IntegrationIds } from "../constants.js";
|
|
3
|
-
import "../providerFilters.js";
|
|
4
3
|
import { ProviderWarning } from "../results.js";
|
|
5
4
|
import { GitHostIntegration } from "../models/gitHostIntegration.js";
|
|
6
5
|
import { IntegrationResult } from "../models/integration.js";
|
|
@@ -220,6 +220,14 @@ export type SupportedFilters = {
|
|
|
220
220
|
* `issueSorts`.
|
|
221
221
|
*/
|
|
222
222
|
issueSortsAccountWide: IssueSorting[];
|
|
223
|
+
/**
|
|
224
|
+
* Whether an issue tracker's reads can be asked for a `state` other than open (`closed`, `all`). False means
|
|
225
|
+
* only open issues are readable, and the read refuses the other two rather than serving the open list.
|
|
226
|
+
*
|
|
227
|
+
* Tracker-only, like `issues`: the git hosts' state support belongs to the filtered search, under
|
|
228
|
+
* `issueSearch.states`.
|
|
229
|
+
*/
|
|
230
|
+
issueStates: boolean;
|
|
223
231
|
};
|
|
224
232
|
/**
|
|
225
233
|
* The filters `listPullRequestsPage`/`listIssuesPage` (and the sweeps) accept for a provider, so a caller can
|
|
@@ -357,7 +357,8 @@ function getSupportedFilters(providerId) {
|
|
|
357
357
|
sorts: [...issueSearch?.sorts ?? []]
|
|
358
358
|
},
|
|
359
359
|
issueSorts: [...metadata?.supportedIssueSorts ?? []],
|
|
360
|
-
issueSortsAccountWide: [...metadata?.supportedAccountWideIssueSorts ?? []]
|
|
360
|
+
issueSortsAccountWide: [...metadata?.supportedAccountWideIssueSorts ?? []],
|
|
361
|
+
issueStates: metadata?.supportsIssueStates === true
|
|
361
362
|
};
|
|
362
363
|
}
|
|
363
364
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filters.js","names":[],"sources":["../../../../../plus/integrations/src/reads/filters.ts"],"sourcesContent":["import type { IntegrationIds } from '../constants.js';\nimport type {\n\tIssueFilter,\n\tIssueSearchCapabilities,\n\tIssueSearchCriteria,\n\tIssueSearchRelationship,\n\tIssueSorting,\n\tPullRequestFilter,\n\tPullRequestSearchCapabilities,\n\tPullRequestSearchCriteria,\n} from '../providerFilters.js';\nimport type { ProviderRepoInput, ProviderReposInput } from '../providers/models.js';\nimport { providersMetadata } from '../providers/models.js';\n\n/**\n * Validation of a caller-supplied relationship filter set against what a provider can express server-side.\n *\n * Every one of these is ALL-OR-NOTHING, and deliberately so: dropping the members a provider can't express\n * would silently widen the read past what was asked for (requesting Author+Mention on a host without Mention\n * would return every authored item rather than the requested set), and narrowing the returned page afterward\n * would leave `items` describing a different result set than the `hasMore`/`cursor` the provider produced with\n * it. So an inexpressible set is refused whole, and the caller surfaces a warning instead of reading unfiltered.\n *\n * The three differ only in WHICH capability they check, because they govern three different provider queries —\n * see `IntegrationManager.getSupportedFilters`, which exposes the same capability table so a consumer can\n * intersect against it and never reach a refusal.\n */\nexport interface ResolvedFilters<T> {\n\tfilters?: T[];\n\tunsupported: boolean;\n}\n\n/**\n * Validates a repo-scoped PR filter set, so an unsupported filter never trips the read core's \"Unsupported\n * filters\" guard. `unsupported: true` when the caller DID request filters and the provider can't express even\n * ONE of them — the exact negation of the read core's `providerSupportsPullRequestFilters` (`every`), so this\n * can only ever pre-empt that guard, never disagree with it.\n *\n * Returns `{ filters }` (possibly undefined when none were requested — an unfiltered read is intended). Genuine\n * \"my pull requests\" self-scoping is delivered by the account-wide path\n * (`GitHostIntegration.getMyPullRequestsForUserResult`); this only governs the optional repo-scoped narrowing.\n */\nexport function resolvePullRequestFilters(\n\tid: IntegrationIds,\n\tfilters: PullRequestFilter[] | undefined,\n): ResolvedFilters<PullRequestFilter> {\n\tif (filters == null || filters.length === 0) return { unsupported: false };\n\n\tconst supported = providersMetadata[id]?.supportedPullRequestFilters;\n\tif (supported == null || filters.some(f => !supported.includes(f))) return { unsupported: true };\n\n\treturn { filters: filters, unsupported: false };\n}\n\n/**\n * Validates an account-wide PR relationship union independently from the repo-scoped capability. Dropping an\n * unsupported member would change the requested OR set, so validation is all-or-nothing.\n */\nexport function resolveAccountWidePullRequestFilters(\n\tid: IntegrationIds,\n\tfilters: PullRequestFilter[] | undefined,\n): ResolvedFilters<PullRequestFilter> {\n\tif (filters == null || filters.length === 0) return { unsupported: false };\n\n\tconst supported = providersMetadata[id]?.supportedAccountWidePullRequestFilters;\n\tif (supported == null || filters.some(f => !supported.includes(f))) return { unsupported: true };\n\n\treturn { filters: [...new Set(filters)], unsupported: false };\n}\n\n/** Why {@link resolvePullRequestSearchCriteria} refused a criteria set. */\nexport type PullRequestSearchCriteriaRejection =\n\t/** The provider has no filtered pull-request search. */\n\t| { reason: 'unsupported-search' }\n\t/** The provider has one, but cannot express every requested criterion server-side. */\n\t| { reason: 'unsupported-criteria'; criteria: string[] };\n\n/**\n * Validates filtered pull-request search criteria all-or-nothing. Dropping an unsupported criterion would\n * widen the provider query while leaving its page and cursor describing the wider set, so the facade refuses the\n * request before any upstream call instead.\n */\nexport function resolvePullRequestSearchCriteria(\n\tid: IntegrationIds,\n\tcriteria: PullRequestSearchCriteria | undefined,\n): { rejection?: PullRequestSearchCriteriaRejection } {\n\tconst supported = providersMetadata[id]?.supportedPullRequestSearch;\n\tif (supported == null || supported.relationships.length === 0) {\n\t\treturn { rejection: { reason: 'unsupported-search' } };\n\t}\n\tif (criteria == null) return {};\n\n\tconst unsupported: string[] = [];\n\tfor (const relationship of criteria.relationships ?? []) {\n\t\tif (!supported.relationships.includes(relationship)) {\n\t\t\tunsupported.push(`relationships:${relationship}`);\n\t\t}\n\t}\n\tfor (const state of criteria.states ?? []) {\n\t\tif (!supported.states.includes(state)) {\n\t\t\tunsupported.push(`states:${state}`);\n\t\t}\n\t}\n\tif (criteria.text != null && criteria.text.trim().length > 0 && !supported.text) {\n\t\tunsupported.push('text');\n\t}\n\tif (criteria.updatedAfter != null && !supported.updatedAfter) {\n\t\tunsupported.push('updatedAfter');\n\t}\n\tif (criteria.createdAfter != null && !supported.createdAfter) {\n\t\tunsupported.push('createdAfter');\n\t}\n\tif (criteria.includeArchived === true && !supported.includeArchived) {\n\t\tunsupported.push('includeArchived');\n\t}\n\t// `!= null`, not truthy: `draft: false` (only ready-for-review) is as much a request as `draft: true`.\n\tif (criteria.draft != null && !supported.draft) {\n\t\tunsupported.push('draft');\n\t}\n\t// Folded into the same `unsupported-criteria` rejection rather than its own reason, so a caller asking for one\n\t// inexpressible filter and one inexpressible sort learns about both at once. `updated:desc` is always in\n\t// `sorts` for a usable search, so the omitted (default) case never rejects.\n\tif (criteria.sort != null && !supported.sorts.includes(criteria.sort)) {\n\t\tunsupported.push(`sort:${criteria.sort}`);\n\t}\n\n\treturn unsupported.length > 0 ? { rejection: { reason: 'unsupported-criteria', criteria: unsupported } } : {};\n}\n\n/** Why a filtered pull-request search's repository/organization boundary was refused. */\nexport type PullRequestSearchScopeRejection =\n\t| { reason: 'unscoped' }\n\t| { reason: 'repo-ids' }\n\t| { reason: 'unsupported-repository-scope' }\n\t| { reason: 'unsupported-organization-scope' }\n\t/** Scope names a query cannot carry AS GIVEN; see {@link unusableSearchScopeNames}. */\n\t| { reason: 'unusable-scope'; scopes: string[] };\n\n/**\n * Validates the search boundary independently from its criteria. A current-user relationship is itself a safe\n * account-wide boundary; without one, a repository or organization scope is mandatory.\n *\n * Scope names go through the same {@link unusableSearchScopeNames} rule the issue search uses — shared rather\n * than re-derived, because the defect is identical on both reads.\n */\nexport function resolvePullRequestSearchScope(\n\tid: IntegrationIds,\n\trepos: ProviderReposInput | undefined,\n\torg: string | undefined,\n\tcriteria: PullRequestSearchCriteria | undefined,\n): { rejection?: PullRequestSearchScopeRejection; repos?: ProviderRepoInput[] } {\n\tconst supported = providersMetadata[id]?.supportedPullRequestSearch;\n\tlet resolvedRepos: ProviderRepoInput[] | undefined;\n\n\tif (repos?.length) {\n\t\tif (repos.some(r => typeof r === 'string' || typeof r === 'number')) {\n\t\t\treturn { rejection: { reason: 'repo-ids' } };\n\t\t}\n\t\tif (supported?.repositoryScope !== true) return { rejection: { reason: 'unsupported-repository-scope' } };\n\n\t\tresolvedRepos = repos as ProviderRepoInput[];\n\t}\n\n\tconst hasOrganizationScope = org != null && org.length > 0;\n\tif (hasOrganizationScope && supported?.organizationScope !== true) {\n\t\treturn { rejection: { reason: 'unsupported-organization-scope' } };\n\t}\n\n\t// After the capability checks and before the \"is it scoped at all\" rule: a scope the provider cannot express\n\t// at all is the more fundamental refusal, and an unusable value that WAS supplied must not be reported as a\n\t// missing one. `org` is passed bare, matching the issue twin — the helper applies its own empty-means-\n\t// unsupplied guard, so re-testing `hasOrganizationScope` here would only make the two resolvers look like\n\t// they differ.\n\tconst unusable = unusableSearchScopeNames(id, org, resolvedRepos);\n\tif (unusable.length > 0) return { rejection: { reason: 'unusable-scope', scopes: unusable } };\n\n\tif (resolvedRepos != null || hasOrganizationScope || (criteria?.relationships?.length ?? 0) > 0) {\n\t\treturn { repos: resolvedRepos };\n\t}\n\n\treturn { rejection: { reason: 'unscoped' } };\n}\n\n/**\n * Validates a caller-provided issue filter set against what the provider's ACCOUNT-WIDE issue read can express\n * server-side (`ProviderMetadata.supportedAccountWideIssueFilters`), which is a different — usually narrower —\n * set than the repo-scoped `ProviderMetadata.supportedIssueFilters`.\n *\n * Dropping the unexpressible members would silently widen the read back toward the provider's own union\n * (authored ∪ assigned ∪ mentioned for GitHub), which is the opposite of what a caller narrowing to\n * `[Assignee]` asked for.\n */\nexport function resolveAccountWideIssueFilters(\n\tid: IntegrationIds,\n\tfilters: IssueFilter[] | undefined,\n): ResolvedFilters<IssueFilter> {\n\tif (filters == null || filters.length === 0) return { unsupported: false };\n\n\tconst supported = providersMetadata[id]?.supportedAccountWideIssueFilters;\n\tif (supported == null || filters.some(f => !supported.includes(f))) return { unsupported: true };\n\n\treturn { filters: filters, unsupported: false };\n}\n\n/** Why {@link resolveIssueSearchCriteria} refused a criteria set, so the caller can word the refusal exactly. */\nexport type IssueSearchCriteriaRejection =\n\t/** The provider has no filtered issue search at all. */\n\t| { reason: 'unsupported-search' }\n\t/** The provider has one, but can't express these criteria server-side. */\n\t| { reason: 'unsupported-criteria'; criteria: string[] }\n\t/** `any-assignee` and `unassigned` partition the scope between them; asking for both asks for nothing. */\n\t| { reason: 'contradictory-relationships' };\n\n/**\n * Whether a provider exposes the FILTERED issue search at all — the one predicate a caller can check BEFORE\n * building criteria, so a read that would only be refused is never issued.\n *\n * The same test {@link resolveIssueSearchCriteria} makes for its `unsupported-search` rejection, named rather\n * than re-derived at each call site: a caller writing `supportedIssueSearch != null` itself is a copy free to\n * disagree with the validator about what \"has a search\" means. `broadenIssues` reads it to pick its engine —\n * the org-scoped search where there is one, the repository drain where there isn't — which is a CHOICE rather\n * than a refusal, so it needs the predicate without the rejection.\n */\nexport function supportsFilteredIssueSearch(id: IntegrationIds): boolean {\n\treturn providersMetadata[id]?.supportedIssueSearch != null;\n}\n\n/**\n * Validates a filtered issue search's criteria against {@link ProviderMetadata.supportedIssueSearch}.\n *\n * All-or-nothing like its three siblings above, and for the same reason: a criterion dropped because the\n * provider can't express it would serve a WIDER result than was asked for, and narrowing the returned page\n * afterward would leave `items` describing a different result set than the `hasMore`/`cursor` the provider\n * produced with it. So an inexpressible set is refused whole and the caller surfaces a warning.\n *\n * Unlike the sibling validators this also rejects a set that is internally contradictory, because this criteria\n * model is the only one with two members that partition the scope between them (`any-assignee` ∪ `unassigned` =\n * everything, ∩ = nothing). Silently keeping one would answer a question the caller didn't ask.\n *\n * `undefined` (or an empty relationship list plus no other criterion) is a valid UNNARROWED search of the given\n * scope, not an error — the caller's `repos`/`org` is what bounds it.\n */\nexport function resolveIssueSearchCriteria(\n\tid: IntegrationIds,\n\tcriteria: IssueSearchCriteria | undefined,\n): { rejection?: IssueSearchCriteriaRejection } {\n\tconst supported = providersMetadata[id]?.supportedIssueSearch;\n\tif (supported == null) return { rejection: { reason: 'unsupported-search' } };\n\tif (criteria == null) return {};\n\n\tconst relationships = criteria.relationships;\n\tif (relationships?.includes('any-assignee') && relationships.includes('unassigned')) {\n\t\treturn { rejection: { reason: 'contradictory-relationships' } };\n\t}\n\n\tconst unsupported: string[] = [];\n\tfor (const relationship of relationships ?? []) {\n\t\tif (!supported.relationships.includes(relationship)) {\n\t\t\tunsupported.push(`relationships:${relationship}`);\n\t\t}\n\t}\n\t// Only a criterion the caller actually SET can be unsupported. `false`/empty means \"don't narrow on this\",\n\t// which every provider can honor by doing nothing, so it must not be validated as a request.\n\tif (criteria.text != null && criteria.text.trim().length > 0 && !supported.text) {\n\t\tunsupported.push('text');\n\t}\n\tif (criteria.labels?.length && !supported.labels) {\n\t\tunsupported.push('labels');\n\t}\n\tif (criteria.milestone != null && !supported.milestone) {\n\t\tunsupported.push('milestone');\n\t}\n\tif (criteria.updatedAfter != null && !supported.updatedAfter) {\n\t\tunsupported.push('updatedAfter');\n\t}\n\tif (criteria.createdAfter != null && !supported.createdAfter) {\n\t\tunsupported.push('createdAfter');\n\t}\n\tif (criteria.withoutLinkedPullRequest === true && !supported.withoutLinkedPullRequest) {\n\t\tunsupported.push('withoutLinkedPullRequest');\n\t}\n\t// `'open'` is every provider's own default, so asking for it needs no state capability.\n\tif (criteria.state != null && criteria.state !== 'open' && !supported.states) {\n\t\tunsupported.push('state');\n\t}\n\t// Folded into the same `unsupported-criteria` rejection rather than given its own reason, so a caller asking\n\t// for one inexpressible filter and one inexpressible sort learns about both at once instead of fixing them one\n\t// refusal at a time. The `sorts` list is `supported`'s own, so this needs no branch on which surface it is.\n\tif (criteria.sort != null && !supported.sorts.includes(criteria.sort)) {\n\t\tunsupported.push(`sort:${criteria.sort}`);\n\t}\n\n\tif (unsupported.length > 0) return { rejection: { reason: 'unsupported-criteria', criteria: unsupported } };\n\n\treturn {};\n}\n\n/**\n * The relationships that bound a filtered issue search to the CURRENT USER, and so can stand in for a\n * repository/org scope.\n *\n * Derived from the union rather than spelled out per call site, because the distinction it encodes is the whole\n * reason the scope rule exists: `any-assignee` and `unassigned` are deliberately absent — they describe the\n * ISSUE, not the caller, so neither reduces the search to anyone's own world (measured: unscoped `no:assignee`\n * matches ~45 million issues on GitHub).\n */\nconst userScopingIssueSearchRelationships: readonly IssueSearchRelationship[] = ['authored', 'assigned', 'mentioned'];\n\n/** What a provider with NO filtered issue search reports: nothing expressible, spelled out rather than absent. */\nconst unsupportedIssueSearchCapabilities: IssueSearchCapabilities = {\n\trelationships: [],\n\ttext: false,\n\tlabels: false,\n\tmilestone: false,\n\tupdatedAfter: false,\n\tcreatedAfter: false,\n\twithoutLinkedPullRequest: false,\n\tstates: false,\n\tsorts: [],\n};\n\n/** What a provider with no filtered pull-request search reports. */\nconst unsupportedPullRequestSearchCapabilities: PullRequestSearchCapabilities = {\n\trelationships: [],\n\tstates: [],\n\ttext: false,\n\tupdatedAfter: false,\n\tcreatedAfter: false,\n\tincludeArchived: false,\n\tdraft: false,\n\trepositoryScope: false,\n\torganizationScope: false,\n\tsorts: [],\n};\n\n/** Why a filtered issue search's scope was refused, or `undefined` when it is usable. */\nexport type IssueSearchScopeRejection =\n\t/** No repositories, no org, and no user-relative relationship: a search of the whole host. */\n\t| { reason: 'unscoped' }\n\t/** Repositories given as ids. A search names repositories by PATH, so ids can't express a scope. */\n\t| { reason: 'repo-ids' }\n\t/** Scope names a query cannot carry AS GIVEN; see {@link unusableSearchScopeNames}. */\n\t| { reason: 'unusable-scope'; scopes: string[] };\n\n/**\n * Whether a scope name reaches the provider NAMING THE SAME SCOPE — the one rule behind every `unusable-scope`\n * refusal, and the canonical home for its reasoning: the call sites point here rather than restating it.\n *\n * The defect it exists to prevent: the boundary is checked against the value AS SUPPLIED while the request is\n * built from the value AFTER the provider sanitizes it, so a name a query cannot spell produces a read that is no\n * longer the read that was authorized. Three outcomes, all of which LOOK LIKE SUCCESS, so there is nothing for a\n * consumer to branch on:\n * - **emptied** — a value of only quotes/whitespace/control characters emits no scope qualifier at all. A scope is\n * also what makes a relationship-less read legal, so the request carries neither and every item on the host\n * matches: measured at 52 million issues across unrelated accounts.\n * - **altered** — a quote inside a real name sanitizes to a real but DIFFERENT scope (`git\"kraken` -> `gitkraken`),\n * whose answer looks entirely normal.\n * - **split** — whitespace delimits qualifiers, so `my org` emits `org:my org`: a search of `my` additionally\n * filtered by the free text `org`. Measured against the live API this is a wrong NARROWING rather than a\n * widening — `org:gitkraken bar` returns 12 where `org:gitkraken` returns 379.\n *\n * Refusing beats sanitizing, and the count probes are why it matters most: they deliberately apply exactly the\n * qualifiers their search would, so a sanitized scope makes the count AGREE with the wrong search rather than\n * disagree with it, and a consumer cross-checking \"N matched\" cannot detect it by construction. Nor can it\n * pre-empt the rule — the free-text sanitizing rules are published precisely so a caller can mirror them, the\n * scope rules are not — so only the caller knows which scope it meant, and the refusal goes back to it NAMING the\n * offending value.\n *\n * Provider-NEUTRAL by design, and deliberately not an import of GitHub's `sanitizeGitHubQualifierValue`: this\n * module validates for every provider, and a GitHub-specific rule reaching in here would be wrong for the next\n * one that declares a search. The character class below is stated as the common part of any query language,\n * because each class breaks a query on its own terms — a quote closes its own qualifier, a control character\n * cannot appear at all, whitespace delimits the next qualifier.\n *\n * A provider that never puts a scope name into a query string declares\n * {@link ProviderMetadata.exactSearchScopeNames}, and only blank names and control characters are refused for it:\n * Azure DevOps Server addresses a collection or repository as an encoded URL segment and matches a project against\n * the names discovery reported, so a space or a quote reaches it naming the same scope — and its names legitimately\n * contain both.\n *\n * EDGES ARE STRIPPED before the test, which is what keeps the predicate no stricter than the provider's own\n * sanitizing — the invariant that makes refusing safe to add, since it means this can only reject a name the\n * provider would have altered, never one it would have resolved correctly. Leading and trailing whitespace AND\n * control characters both qualify: a sanitizer maps a control character to a space, then collapses and trims, so\n * `'gitkraken\\n'` and `'gitkraken\\u0000'` alike emit `org:gitkraken` — the scope that was asked for. Note this\n * is wider than `String.trim()`, which leaves control characters in place.\n *\n * None of the three outcomes above survives the stripping, so the rule loses nothing: an all-edge value still\n * empties, an INNER space or control character still splits (`'git\\u0000kraken'` emits `git kraken`, two\n * tokens), and a quote still alters wherever it sits.\n */\nfunction isUsableSearchScopeName(id: IntegrationIds, name: string): boolean {\n\tconst stripped = stripSearchScopeEdges(name);\n\tif (providersMetadata[id]?.exactSearchScopeNames === true) {\n\t\t// eslint-disable-next-line no-control-regex\n\t\treturn stripped.length > 0 && !/[\\u0000-\\u001f\\u007f]/.test(stripped);\n\t}\n\n\t// eslint-disable-next-line no-control-regex\n\treturn stripped.length > 0 && !/[\"\\u0000-\\u001f\\u007f\\s]/.test(stripped);\n}\n\n/** The leading/trailing run a provider's sanitizing removes — see {@link isUsableSearchScopeName}. */\nfunction stripSearchScopeEdges(value: string): string {\n\t// eslint-disable-next-line no-control-regex\n\treturn value.replace(/^[\\s\\u0000-\\u001f\\u007f]+|[\\s\\u0000-\\u001f\\u007f]+$/g, '');\n}\n\n/**\n * The scope names a search cannot carry as given, as the strings to name in the refusal — empty when every one is\n * usable. See {@link isUsableSearchScopeName} for the rule and why it refuses rather than sanitizes.\n */\nfunction unusableSearchScopeNames(\n\tid: IntegrationIds,\n\torg: string | undefined,\n\trepos: readonly ProviderRepoInput[] | undefined,\n): string[] {\n\tconst unusable: string[] = [];\n\n\t// An EMPTY org is \"no org supplied\" and falls through to the remaining scopes; any other unusable value WAS\n\t// supplied, so it is refused rather than dropped.\n\tif (org != null && org.length > 0 && !isUsableSearchScopeName(id, org)) {\n\t\tunusable.push(org);\n\t}\n\n\t// A `repo:` qualifier names a repository by its JOINED `namespace/name` path, which is the value the rule\n\t// below is applied to. Both halves are read defensively, through the SAME locals the label is built from: the\n\t// descriptor form is only narrowed from a union by an element-type check, so a half-built descriptor reaches\n\t// here as `undefined` and must refuse rather than throw out of a facade that reports refusals as warnings —\n\t// and reporting it as `undefined/a` would name a value the caller never passed.\n\tfor (const repo of repos ?? []) {\n\t\tconst namespace = repo.namespace ?? '';\n\t\tconst name = repo.name ?? '';\n\t\tconst path = `${namespace}/${name}`;\n\t\t// BOTH the composite and each half, because neither sees what the other does:\n\t\t// - the composite catches an offender the halves cannot, since an edge of a half is an INTERIOR character\n\t\t// of the path — `{ 'git ', 'kraken' }` has two usable-looking halves and emits `repo:git /kraken`.\n\t\t// - the halves catch a BLANK one the composite cannot, since that offender sits at a composite EDGE where\n\t\t// stripping removes it — `' /a'` strips to `'/a'`, a perfectly spellable qualifier naming no repository.\n\t\t// Each half is measured after the same stripping, so `' '` and `''` are one case rather than two.\n\t\tif (\n\t\t\t!isUsableSearchScopeName(id, path) ||\n\t\t\tstripSearchScopeEdges(namespace).length === 0 ||\n\t\t\tstripSearchScopeEdges(name).length === 0\n\t\t) {\n\t\t\tunusable.push(path);\n\t\t}\n\t}\n\n\treturn unusable;\n}\n\n/**\n * Validates that a filtered issue search is scoped at all, and narrows `repos` to the descriptor form the\n * provider query needs.\n *\n * Shared by `searchIssuesPage` and `countIssues` because a count computed under different constraints than the\n * read it previews is a WRONG number rather than a missing one. The rejection is returned as a reason code, not a\n * warning: the two callers word it differently (whole-read vs naming the offending scope's key), and wording is\n * the warning layer's business.\n *\n * `repo-ids` and `unscoped` are mutually exclusive — one requires repositories and the other requires none — so\n * their relative order is free. `unusable-scope` is NOT: it must precede `unscoped`, so a value that was supplied\n * but cannot be used is never reported as a missing one.\n *\n * Every scope name is checked for naming the same scope after the provider sanitizes it, not merely for being\n * non-empty, and that distinction is a SECURITY one rather than a nicety — see {@link isUsableSearchScopeName}.\n */\nexport function resolveIssueSearchScope(\n\tid: IntegrationIds,\n\trepos: ProviderReposInput | undefined,\n\torg: string | undefined,\n\tcriteria: IssueSearchCriteria | undefined,\n): { rejection?: IssueSearchScopeRejection; repos?: ProviderRepoInput[] } {\n\tlet resolvedRepos: ProviderRepoInput[] | undefined;\n\n\tif (repos?.length) {\n\t\t// `ProviderReposInput` is a union of descriptor and id arrays; only the descriptor form is usable here.\n\t\tif (repos.some(r => typeof r === 'string' || typeof r === 'number')) {\n\t\t\treturn { rejection: { reason: 'repo-ids' } };\n\t\t}\n\n\t\tresolvedRepos = repos as ProviderRepoInput[];\n\t}\n\n\t// Checked BEFORE the \"is it scoped at all\" rule below, so an unusable value is never reported as a missing\n\t// one: it was supplied, and telling the caller to pass a scope it already passed names the wrong defect.\n\tconst unusable = unusableSearchScopeNames(id, org, resolvedRepos);\n\tif (unusable.length > 0) return { rejection: { reason: 'unusable-scope', scopes: unusable } };\n\n\tif (resolvedRepos != null) return { repos: resolvedRepos };\n\tif (org != null && org.length > 0) return {};\n\tif (criteria?.relationships?.some(r => userScopingIssueSearchRelationships.includes(r)) === true) return {};\n\n\treturn { rejection: { reason: 'unscoped' } };\n}\n\n/**\n * What {@link getSupportedFilters} reports: every vocabulary a provider's reads can express, so a consumer can\n * narrow to it BEFORE issuing a read. See that function for the capability table itself and why intersecting\n * against this is what keeps a filtered read from being refused.\n *\n * Named rather than written inline, because the same shape is the declared return type of THREE declarations that\n * all describe one value — this function, `IntegrationService.getSupportedFilters`, which only forwards to it, and\n * `IntegrationManager.getSupportedFilters`, which publishes it. Spelled out three times, a member added here\n * reached one of them and left the others a compile error away from the truth; that is exactly how the two sort\n * members below came to be written out three times.\n */\nexport type SupportedFilters = {\n\tpullRequests: PullRequestFilter[];\n\tpullRequestsAccountWide: PullRequestFilter[];\n\t/**\n\t * Criteria and scopes the filtered pull-request search can express. Always present; an empty relationship list\n\t * means the provider exposes no filtered pull-request search.\n\t */\n\tpullRequestSearch: PullRequestSearchCapabilities;\n\t/**\n\t * Filters the repo-scoped issue read accepts — and, for an issue tracker (Jira/Linear/Trello), its\n\t * project-scoped read, that being its only issue surface. A tracker therefore reports here and leaves\n\t * `issuesAccountWide` empty, so intersecting a tracker against that field would read \"cannot filter\" for a\n\t * provider that filters fine.\n\t */\n\tissues: IssueFilter[];\n\tissuesAccountWide: IssueFilter[];\n\t/**\n\t * What the filtered issue search (and the count probe over the same criteria) can express. Always present: a\n\t * provider with no filtered issue search reports an empty `relationships` and all-false flags, which is the\n\t * signal to hide that surface rather than to hide individual chips.\n\t */\n\tissueSearch: IssueSearchCapabilities;\n\t/**\n\t * Sort keys the repo-scoped issue read can express — and, for an issue tracker, its project-scoped read. Empty\n\t * means the read can't be ordered, so pass no `sort`.\n\t *\n\t * A key here is expressible on ONE provider query. A page spanning several repositories or projects is a merge,\n\t * and a merge can only order by what a normalized issue carries, so `priority`/`dueDate`/`resolved` are refused\n\t * there even where they are listed. Read one scope at a time to use them.\n\t */\n\tissueSorts: IssueSorting[];\n\t/**\n\t * Sort keys the account-wide issue read can express — a different vocabulary, not a subset: for GitLab the two\n\t * reads are different APIs. Needs no mergeability caveat, because every account-wide read is a union of several\n\t * queries, so only keys a merge can honor are listed at all. Empty for an issue tracker, which reports under\n\t * `issueSorts`.\n\t */\n\tissueSortsAccountWide: IssueSorting[];\n};\n\n/**\n * The filters `listPullRequestsPage`/`listIssuesPage` (and the sweeps) accept for a provider, so a caller can\n * narrow to what the provider can express BEFORE issuing the read.\n *\n * This matters because the filter contract is all-or-nothing: a set containing even one unsupported filter is\n * refused outright ({@link resolvePullRequestFilters}) rather than silently narrowed, since falling through to\n * an unfiltered fetch would return every PR instead of the user's. Without this accessor a consumer had to\n * hardcode its own copy of the table — reachable only by importing the internal `providers/models.js`\n * subpath — and a copy that drifts turns a supported read into an empty page with `fetchFailed`.\n *\n * Empty means \"no filter of that kind is expressible\": either the provider has no such surface (issue trackers\n * have no pull requests; Bitbucket exposes no issues) or its metadata declares none. Callers should treat it as\n * \"don't pass filters\", not as an error. Returns copies, so mutating the result can't corrupt the metadata.\n *\n * `issues` and `issuesAccountWide` are separate because the repo-scoped and account-wide issue reads are\n * different provider queries with different filter surfaces, and the same `filters` input is validated against\n * whichever one the read uses (`repos` present or not). `issuesAccountWide` is generally the narrower of the\n * two: GitLab, for instance, can express `Assignee` and `Author` account-wide, but not `Mention`.\n *\n * That split describes the GIT-HOST reads only. An issue tracker (Jira/Linear/Trello) has neither — its issues\n * live under resource → project — so it reports its filters under `issues`, which is what\n * {@link IntegrationService.listIssueTrackerIssuesPage} validates against, and leaves `issuesAccountWide`\n * empty. Reading a tracker's capability off `issuesAccountWide` therefore under-reports it.\n *\n * `issueSearch` is a third, wider surface: the FILTERED issue search (`searchIssuesPage`, and the `countIssues`\n * probe over the same criteria), which is not bound to the user at all. It is a record of per-criterion flags\n * rather than a list, because its criteria aren't a single kind. An empty `relationships` means the provider has\n * no filtered issue search — hide the surface, not just its chips.\n *\n * `pullRequestSearch` describes the separate filtered PR search. Its table declares each criteria vocabulary\n * plus repository/organization scope support; an empty relationship list means the search itself is absent.\n *\n * `issueSorts` / `issueSortsAccountWide` / `issueSearch.sorts` are the ORDERING vocabulary of those same three\n * issue reads, split the same way and for the same reason (GitLab's repo-scoped read is GraphQL and its\n * account-wide read is REST, with genuinely different sort vocabularies). Empty means the read can't be ordered,\n * so pass no `sort`; a key not listed refuses the whole read exactly like an inexpressible filter. Note a key\n * listed here is expressible on ONE provider query: a read that fans out across projects can only honor a key\n * derivable from a normalized issue, so it refuses `priority`/`dueDate`/`resolved` on top of this table.\n *\n * Note this is a CAPABILITY table — \"what the provider can express\" — not a recommendation. A consumer\n * matching another tool's behavior may deliberately pass fewer filters than are listed here (or none, where an\n * already-scoped read would only be narrowed by them). Intersecting against this table is what keeps a\n * filtered read from being refused; it isn't a directive to use every filter in it.\n */\nexport function getSupportedFilters(providerId: IntegrationIds): SupportedFilters {\n\tconst metadata = providersMetadata[providerId];\n\tconst issueSearch = metadata?.supportedIssueSearch;\n\tconst pullRequestSearch = metadata?.supportedPullRequestSearch;\n\treturn {\n\t\tpullRequests: [...(metadata?.supportedPullRequestFilters ?? [])],\n\t\tpullRequestsAccountWide: [...(metadata?.supportedAccountWidePullRequestFilters ?? [])],\n\t\tpullRequestSearch: {\n\t\t\t...unsupportedPullRequestSearchCapabilities,\n\t\t\t...pullRequestSearch,\n\t\t\trelationships: [...(pullRequestSearch?.relationships ?? [])],\n\t\t\tstates: [...(pullRequestSearch?.states ?? [])],\n\t\t\t// Copied, so mutating the result can't corrupt the metadata table.\n\t\t\tsorts: [...(pullRequestSearch?.sorts ?? [])],\n\t\t},\n\t\tissues: [...(metadata?.supportedIssueFilters ?? [])],\n\t\tissuesAccountWide: [...(metadata?.supportedAccountWideIssueFilters ?? [])],\n\t\t// Always an object, never `undefined`: a provider WITHOUT a filtered issue search reports one whose\n\t\t// `relationships` is empty and whose flags are all false, so a consumer reads capabilities the same way\n\t\t// for every provider (and an empty `relationships` is the signal to hide the surface itself).\n\t\t//\n\t\t// Spread over the all-false baseline rather than defaulting each flag: a criterion added to\n\t\t// `IssueSearchCapabilities` then can't be forgotten here and silently reported as `undefined` (which a\n\t\t// consumer's `if (caps.x)` would read as unsupported — right answer, wrong reason, and untyped).\n\t\t// `supportedIssueSearch` is typed as the complete shape, so no member can arrive as an explicit undefined.\n\t\tissueSearch: {\n\t\t\t...unsupportedIssueSearchCapabilities,\n\t\t\t...issueSearch,\n\t\t\t// Copied, so mutating the result can't corrupt the metadata table.\n\t\t\trelationships: [...(issueSearch?.relationships ?? [])],\n\t\t\tsorts: [...(issueSearch?.sorts ?? [])],\n\t\t},\n\t\tissueSorts: [...(metadata?.supportedIssueSorts ?? [])],\n\t\tissueSortsAccountWide: [...(metadata?.supportedAccountWideIssueSorts ?? [])],\n\t};\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"filters.js","names":[],"sources":["../../../../../plus/integrations/src/reads/filters.ts"],"sourcesContent":["import type { IntegrationIds } from '../constants.js';\nimport type {\n\tIssueFilter,\n\tIssueSearchCapabilities,\n\tIssueSearchCriteria,\n\tIssueSearchRelationship,\n\tIssueSorting,\n\tPullRequestFilter,\n\tPullRequestSearchCapabilities,\n\tPullRequestSearchCriteria,\n} from '../providerFilters.js';\nimport type { ProviderRepoInput, ProviderReposInput } from '../providers/models.js';\nimport { providersMetadata } from '../providers/models.js';\n\n/**\n * Validation of a caller-supplied relationship filter set against what a provider can express server-side.\n *\n * Every one of these is ALL-OR-NOTHING, and deliberately so: dropping the members a provider can't express\n * would silently widen the read past what was asked for (requesting Author+Mention on a host without Mention\n * would return every authored item rather than the requested set), and narrowing the returned page afterward\n * would leave `items` describing a different result set than the `hasMore`/`cursor` the provider produced with\n * it. So an inexpressible set is refused whole, and the caller surfaces a warning instead of reading unfiltered.\n *\n * The three differ only in WHICH capability they check, because they govern three different provider queries —\n * see `IntegrationManager.getSupportedFilters`, which exposes the same capability table so a consumer can\n * intersect against it and never reach a refusal.\n */\nexport interface ResolvedFilters<T> {\n\tfilters?: T[];\n\tunsupported: boolean;\n}\n\n/**\n * Validates a repo-scoped PR filter set, so an unsupported filter never trips the read core's \"Unsupported\n * filters\" guard. `unsupported: true` when the caller DID request filters and the provider can't express even\n * ONE of them — the exact negation of the read core's `providerSupportsPullRequestFilters` (`every`), so this\n * can only ever pre-empt that guard, never disagree with it.\n *\n * Returns `{ filters }` (possibly undefined when none were requested — an unfiltered read is intended). Genuine\n * \"my pull requests\" self-scoping is delivered by the account-wide path\n * (`GitHostIntegration.getMyPullRequestsForUserResult`); this only governs the optional repo-scoped narrowing.\n */\nexport function resolvePullRequestFilters(\n\tid: IntegrationIds,\n\tfilters: PullRequestFilter[] | undefined,\n): ResolvedFilters<PullRequestFilter> {\n\tif (filters == null || filters.length === 0) return { unsupported: false };\n\n\tconst supported = providersMetadata[id]?.supportedPullRequestFilters;\n\tif (supported == null || filters.some(f => !supported.includes(f))) return { unsupported: true };\n\n\treturn { filters: filters, unsupported: false };\n}\n\n/**\n * Validates an account-wide PR relationship union independently from the repo-scoped capability. Dropping an\n * unsupported member would change the requested OR set, so validation is all-or-nothing.\n */\nexport function resolveAccountWidePullRequestFilters(\n\tid: IntegrationIds,\n\tfilters: PullRequestFilter[] | undefined,\n): ResolvedFilters<PullRequestFilter> {\n\tif (filters == null || filters.length === 0) return { unsupported: false };\n\n\tconst supported = providersMetadata[id]?.supportedAccountWidePullRequestFilters;\n\tif (supported == null || filters.some(f => !supported.includes(f))) return { unsupported: true };\n\n\treturn { filters: [...new Set(filters)], unsupported: false };\n}\n\n/** Why {@link resolvePullRequestSearchCriteria} refused a criteria set. */\nexport type PullRequestSearchCriteriaRejection =\n\t/** The provider has no filtered pull-request search. */\n\t| { reason: 'unsupported-search' }\n\t/** The provider has one, but cannot express every requested criterion server-side. */\n\t| { reason: 'unsupported-criteria'; criteria: string[] };\n\n/**\n * Validates filtered pull-request search criteria all-or-nothing. Dropping an unsupported criterion would\n * widen the provider query while leaving its page and cursor describing the wider set, so the facade refuses the\n * request before any upstream call instead.\n */\nexport function resolvePullRequestSearchCriteria(\n\tid: IntegrationIds,\n\tcriteria: PullRequestSearchCriteria | undefined,\n): { rejection?: PullRequestSearchCriteriaRejection } {\n\tconst supported = providersMetadata[id]?.supportedPullRequestSearch;\n\tif (supported == null || supported.relationships.length === 0) {\n\t\treturn { rejection: { reason: 'unsupported-search' } };\n\t}\n\tif (criteria == null) return {};\n\n\tconst unsupported: string[] = [];\n\tfor (const relationship of criteria.relationships ?? []) {\n\t\tif (!supported.relationships.includes(relationship)) {\n\t\t\tunsupported.push(`relationships:${relationship}`);\n\t\t}\n\t}\n\tfor (const state of criteria.states ?? []) {\n\t\tif (!supported.states.includes(state)) {\n\t\t\tunsupported.push(`states:${state}`);\n\t\t}\n\t}\n\tif (criteria.text != null && criteria.text.trim().length > 0 && !supported.text) {\n\t\tunsupported.push('text');\n\t}\n\tif (criteria.updatedAfter != null && !supported.updatedAfter) {\n\t\tunsupported.push('updatedAfter');\n\t}\n\tif (criteria.createdAfter != null && !supported.createdAfter) {\n\t\tunsupported.push('createdAfter');\n\t}\n\tif (criteria.includeArchived === true && !supported.includeArchived) {\n\t\tunsupported.push('includeArchived');\n\t}\n\t// `!= null`, not truthy: `draft: false` (only ready-for-review) is as much a request as `draft: true`.\n\tif (criteria.draft != null && !supported.draft) {\n\t\tunsupported.push('draft');\n\t}\n\t// Folded into the same `unsupported-criteria` rejection rather than its own reason, so a caller asking for one\n\t// inexpressible filter and one inexpressible sort learns about both at once. `updated:desc` is always in\n\t// `sorts` for a usable search, so the omitted (default) case never rejects.\n\tif (criteria.sort != null && !supported.sorts.includes(criteria.sort)) {\n\t\tunsupported.push(`sort:${criteria.sort}`);\n\t}\n\n\treturn unsupported.length > 0 ? { rejection: { reason: 'unsupported-criteria', criteria: unsupported } } : {};\n}\n\n/** Why a filtered pull-request search's repository/organization boundary was refused. */\nexport type PullRequestSearchScopeRejection =\n\t| { reason: 'unscoped' }\n\t| { reason: 'repo-ids' }\n\t| { reason: 'unsupported-repository-scope' }\n\t| { reason: 'unsupported-organization-scope' }\n\t/** Scope names a query cannot carry AS GIVEN; see {@link unusableSearchScopeNames}. */\n\t| { reason: 'unusable-scope'; scopes: string[] };\n\n/**\n * Validates the search boundary independently from its criteria. A current-user relationship is itself a safe\n * account-wide boundary; without one, a repository or organization scope is mandatory.\n *\n * Scope names go through the same {@link unusableSearchScopeNames} rule the issue search uses — shared rather\n * than re-derived, because the defect is identical on both reads.\n */\nexport function resolvePullRequestSearchScope(\n\tid: IntegrationIds,\n\trepos: ProviderReposInput | undefined,\n\torg: string | undefined,\n\tcriteria: PullRequestSearchCriteria | undefined,\n): { rejection?: PullRequestSearchScopeRejection; repos?: ProviderRepoInput[] } {\n\tconst supported = providersMetadata[id]?.supportedPullRequestSearch;\n\tlet resolvedRepos: ProviderRepoInput[] | undefined;\n\n\tif (repos?.length) {\n\t\tif (repos.some(r => typeof r === 'string' || typeof r === 'number')) {\n\t\t\treturn { rejection: { reason: 'repo-ids' } };\n\t\t}\n\t\tif (supported?.repositoryScope !== true) return { rejection: { reason: 'unsupported-repository-scope' } };\n\n\t\tresolvedRepos = repos as ProviderRepoInput[];\n\t}\n\n\tconst hasOrganizationScope = org != null && org.length > 0;\n\tif (hasOrganizationScope && supported?.organizationScope !== true) {\n\t\treturn { rejection: { reason: 'unsupported-organization-scope' } };\n\t}\n\n\t// After the capability checks and before the \"is it scoped at all\" rule: a scope the provider cannot express\n\t// at all is the more fundamental refusal, and an unusable value that WAS supplied must not be reported as a\n\t// missing one. `org` is passed bare, matching the issue twin — the helper applies its own empty-means-\n\t// unsupplied guard, so re-testing `hasOrganizationScope` here would only make the two resolvers look like\n\t// they differ.\n\tconst unusable = unusableSearchScopeNames(id, org, resolvedRepos);\n\tif (unusable.length > 0) return { rejection: { reason: 'unusable-scope', scopes: unusable } };\n\n\tif (resolvedRepos != null || hasOrganizationScope || (criteria?.relationships?.length ?? 0) > 0) {\n\t\treturn { repos: resolvedRepos };\n\t}\n\n\treturn { rejection: { reason: 'unscoped' } };\n}\n\n/**\n * Validates a caller-provided issue filter set against what the provider's ACCOUNT-WIDE issue read can express\n * server-side (`ProviderMetadata.supportedAccountWideIssueFilters`), which is a different — usually narrower —\n * set than the repo-scoped `ProviderMetadata.supportedIssueFilters`.\n *\n * Dropping the unexpressible members would silently widen the read back toward the provider's own union\n * (authored ∪ assigned ∪ mentioned for GitHub), which is the opposite of what a caller narrowing to\n * `[Assignee]` asked for.\n */\nexport function resolveAccountWideIssueFilters(\n\tid: IntegrationIds,\n\tfilters: IssueFilter[] | undefined,\n): ResolvedFilters<IssueFilter> {\n\tif (filters == null || filters.length === 0) return { unsupported: false };\n\n\tconst supported = providersMetadata[id]?.supportedAccountWideIssueFilters;\n\tif (supported == null || filters.some(f => !supported.includes(f))) return { unsupported: true };\n\n\treturn { filters: filters, unsupported: false };\n}\n\n/** Why {@link resolveIssueSearchCriteria} refused a criteria set, so the caller can word the refusal exactly. */\nexport type IssueSearchCriteriaRejection =\n\t/** The provider has no filtered issue search at all. */\n\t| { reason: 'unsupported-search' }\n\t/** The provider has one, but can't express these criteria server-side. */\n\t| { reason: 'unsupported-criteria'; criteria: string[] }\n\t/** `any-assignee` and `unassigned` partition the scope between them; asking for both asks for nothing. */\n\t| { reason: 'contradictory-relationships' };\n\n/**\n * Whether a provider exposes the FILTERED issue search at all — the one predicate a caller can check BEFORE\n * building criteria, so a read that would only be refused is never issued.\n *\n * The same test {@link resolveIssueSearchCriteria} makes for its `unsupported-search` rejection, named rather\n * than re-derived at each call site: a caller writing `supportedIssueSearch != null` itself is a copy free to\n * disagree with the validator about what \"has a search\" means. `broadenIssues` reads it to pick its engine —\n * the org-scoped search where there is one, the repository drain where there isn't — which is a CHOICE rather\n * than a refusal, so it needs the predicate without the rejection.\n */\nexport function supportsFilteredIssueSearch(id: IntegrationIds): boolean {\n\treturn providersMetadata[id]?.supportedIssueSearch != null;\n}\n\n/**\n * Validates a filtered issue search's criteria against {@link ProviderMetadata.supportedIssueSearch}.\n *\n * All-or-nothing like its three siblings above, and for the same reason: a criterion dropped because the\n * provider can't express it would serve a WIDER result than was asked for, and narrowing the returned page\n * afterward would leave `items` describing a different result set than the `hasMore`/`cursor` the provider\n * produced with it. So an inexpressible set is refused whole and the caller surfaces a warning.\n *\n * Unlike the sibling validators this also rejects a set that is internally contradictory, because this criteria\n * model is the only one with two members that partition the scope between them (`any-assignee` ∪ `unassigned` =\n * everything, ∩ = nothing). Silently keeping one would answer a question the caller didn't ask.\n *\n * `undefined` (or an empty relationship list plus no other criterion) is a valid UNNARROWED search of the given\n * scope, not an error — the caller's `repos`/`org` is what bounds it.\n */\nexport function resolveIssueSearchCriteria(\n\tid: IntegrationIds,\n\tcriteria: IssueSearchCriteria | undefined,\n): { rejection?: IssueSearchCriteriaRejection } {\n\tconst supported = providersMetadata[id]?.supportedIssueSearch;\n\tif (supported == null) return { rejection: { reason: 'unsupported-search' } };\n\tif (criteria == null) return {};\n\n\tconst relationships = criteria.relationships;\n\tif (relationships?.includes('any-assignee') && relationships.includes('unassigned')) {\n\t\treturn { rejection: { reason: 'contradictory-relationships' } };\n\t}\n\n\tconst unsupported: string[] = [];\n\tfor (const relationship of relationships ?? []) {\n\t\tif (!supported.relationships.includes(relationship)) {\n\t\t\tunsupported.push(`relationships:${relationship}`);\n\t\t}\n\t}\n\t// Only a criterion the caller actually SET can be unsupported. `false`/empty means \"don't narrow on this\",\n\t// which every provider can honor by doing nothing, so it must not be validated as a request.\n\tif (criteria.text != null && criteria.text.trim().length > 0 && !supported.text) {\n\t\tunsupported.push('text');\n\t}\n\tif (criteria.labels?.length && !supported.labels) {\n\t\tunsupported.push('labels');\n\t}\n\tif (criteria.milestone != null && !supported.milestone) {\n\t\tunsupported.push('milestone');\n\t}\n\tif (criteria.updatedAfter != null && !supported.updatedAfter) {\n\t\tunsupported.push('updatedAfter');\n\t}\n\tif (criteria.createdAfter != null && !supported.createdAfter) {\n\t\tunsupported.push('createdAfter');\n\t}\n\tif (criteria.withoutLinkedPullRequest === true && !supported.withoutLinkedPullRequest) {\n\t\tunsupported.push('withoutLinkedPullRequest');\n\t}\n\t// `'open'` is every provider's own default, so asking for it needs no state capability.\n\tif (criteria.state != null && criteria.state !== 'open' && !supported.states) {\n\t\tunsupported.push('state');\n\t}\n\t// Folded into the same `unsupported-criteria` rejection rather than given its own reason, so a caller asking\n\t// for one inexpressible filter and one inexpressible sort learns about both at once instead of fixing them one\n\t// refusal at a time. The `sorts` list is `supported`'s own, so this needs no branch on which surface it is.\n\tif (criteria.sort != null && !supported.sorts.includes(criteria.sort)) {\n\t\tunsupported.push(`sort:${criteria.sort}`);\n\t}\n\n\tif (unsupported.length > 0) return { rejection: { reason: 'unsupported-criteria', criteria: unsupported } };\n\n\treturn {};\n}\n\n/**\n * The relationships that bound a filtered issue search to the CURRENT USER, and so can stand in for a\n * repository/org scope.\n *\n * Derived from the union rather than spelled out per call site, because the distinction it encodes is the whole\n * reason the scope rule exists: `any-assignee` and `unassigned` are deliberately absent — they describe the\n * ISSUE, not the caller, so neither reduces the search to anyone's own world (measured: unscoped `no:assignee`\n * matches ~45 million issues on GitHub).\n */\nconst userScopingIssueSearchRelationships: readonly IssueSearchRelationship[] = ['authored', 'assigned', 'mentioned'];\n\n/** What a provider with NO filtered issue search reports: nothing expressible, spelled out rather than absent. */\nconst unsupportedIssueSearchCapabilities: IssueSearchCapabilities = {\n\trelationships: [],\n\ttext: false,\n\tlabels: false,\n\tmilestone: false,\n\tupdatedAfter: false,\n\tcreatedAfter: false,\n\twithoutLinkedPullRequest: false,\n\tstates: false,\n\tsorts: [],\n};\n\n/** What a provider with no filtered pull-request search reports. */\nconst unsupportedPullRequestSearchCapabilities: PullRequestSearchCapabilities = {\n\trelationships: [],\n\tstates: [],\n\ttext: false,\n\tupdatedAfter: false,\n\tcreatedAfter: false,\n\tincludeArchived: false,\n\tdraft: false,\n\trepositoryScope: false,\n\torganizationScope: false,\n\tsorts: [],\n};\n\n/** Why a filtered issue search's scope was refused, or `undefined` when it is usable. */\nexport type IssueSearchScopeRejection =\n\t/** No repositories, no org, and no user-relative relationship: a search of the whole host. */\n\t| { reason: 'unscoped' }\n\t/** Repositories given as ids. A search names repositories by PATH, so ids can't express a scope. */\n\t| { reason: 'repo-ids' }\n\t/** Scope names a query cannot carry AS GIVEN; see {@link unusableSearchScopeNames}. */\n\t| { reason: 'unusable-scope'; scopes: string[] };\n\n/**\n * Whether a scope name reaches the provider NAMING THE SAME SCOPE — the one rule behind every `unusable-scope`\n * refusal, and the canonical home for its reasoning: the call sites point here rather than restating it.\n *\n * The defect it exists to prevent: the boundary is checked against the value AS SUPPLIED while the request is\n * built from the value AFTER the provider sanitizes it, so a name a query cannot spell produces a read that is no\n * longer the read that was authorized. Three outcomes, all of which LOOK LIKE SUCCESS, so there is nothing for a\n * consumer to branch on:\n * - **emptied** — a value of only quotes/whitespace/control characters emits no scope qualifier at all. A scope is\n * also what makes a relationship-less read legal, so the request carries neither and every item on the host\n * matches: measured at 52 million issues across unrelated accounts.\n * - **altered** — a quote inside a real name sanitizes to a real but DIFFERENT scope (`git\"kraken` -> `gitkraken`),\n * whose answer looks entirely normal.\n * - **split** — whitespace delimits qualifiers, so `my org` emits `org:my org`: a search of `my` additionally\n * filtered by the free text `org`. Measured against the live API this is a wrong NARROWING rather than a\n * widening — `org:gitkraken bar` returns 12 where `org:gitkraken` returns 379.\n *\n * Refusing beats sanitizing, and the count probes are why it matters most: they deliberately apply exactly the\n * qualifiers their search would, so a sanitized scope makes the count AGREE with the wrong search rather than\n * disagree with it, and a consumer cross-checking \"N matched\" cannot detect it by construction. Nor can it\n * pre-empt the rule — the free-text sanitizing rules are published precisely so a caller can mirror them, the\n * scope rules are not — so only the caller knows which scope it meant, and the refusal goes back to it NAMING the\n * offending value.\n *\n * Provider-NEUTRAL by design, and deliberately not an import of GitHub's `sanitizeGitHubQualifierValue`: this\n * module validates for every provider, and a GitHub-specific rule reaching in here would be wrong for the next\n * one that declares a search. The character class below is stated as the common part of any query language,\n * because each class breaks a query on its own terms — a quote closes its own qualifier, a control character\n * cannot appear at all, whitespace delimits the next qualifier.\n *\n * A provider that never puts a scope name into a query string declares\n * {@link ProviderMetadata.exactSearchScopeNames}, and only blank names and control characters are refused for it:\n * Azure DevOps Server addresses a collection or repository as an encoded URL segment and matches a project against\n * the names discovery reported, so a space or a quote reaches it naming the same scope — and its names legitimately\n * contain both.\n *\n * EDGES ARE STRIPPED before the test, which is what keeps the predicate no stricter than the provider's own\n * sanitizing — the invariant that makes refusing safe to add, since it means this can only reject a name the\n * provider would have altered, never one it would have resolved correctly. Leading and trailing whitespace AND\n * control characters both qualify: a sanitizer maps a control character to a space, then collapses and trims, so\n * `'gitkraken\\n'` and `'gitkraken\\u0000'` alike emit `org:gitkraken` — the scope that was asked for. Note this\n * is wider than `String.trim()`, which leaves control characters in place.\n *\n * None of the three outcomes above survives the stripping, so the rule loses nothing: an all-edge value still\n * empties, an INNER space or control character still splits (`'git\\u0000kraken'` emits `git kraken`, two\n * tokens), and a quote still alters wherever it sits.\n */\nfunction isUsableSearchScopeName(id: IntegrationIds, name: string): boolean {\n\tconst stripped = stripSearchScopeEdges(name);\n\tif (providersMetadata[id]?.exactSearchScopeNames === true) {\n\t\t// eslint-disable-next-line no-control-regex\n\t\treturn stripped.length > 0 && !/[\\u0000-\\u001f\\u007f]/.test(stripped);\n\t}\n\n\t// eslint-disable-next-line no-control-regex\n\treturn stripped.length > 0 && !/[\"\\u0000-\\u001f\\u007f\\s]/.test(stripped);\n}\n\n/** The leading/trailing run a provider's sanitizing removes — see {@link isUsableSearchScopeName}. */\nfunction stripSearchScopeEdges(value: string): string {\n\t// eslint-disable-next-line no-control-regex\n\treturn value.replace(/^[\\s\\u0000-\\u001f\\u007f]+|[\\s\\u0000-\\u001f\\u007f]+$/g, '');\n}\n\n/**\n * The scope names a search cannot carry as given, as the strings to name in the refusal — empty when every one is\n * usable. See {@link isUsableSearchScopeName} for the rule and why it refuses rather than sanitizes.\n */\nfunction unusableSearchScopeNames(\n\tid: IntegrationIds,\n\torg: string | undefined,\n\trepos: readonly ProviderRepoInput[] | undefined,\n): string[] {\n\tconst unusable: string[] = [];\n\n\t// An EMPTY org is \"no org supplied\" and falls through to the remaining scopes; any other unusable value WAS\n\t// supplied, so it is refused rather than dropped.\n\tif (org != null && org.length > 0 && !isUsableSearchScopeName(id, org)) {\n\t\tunusable.push(org);\n\t}\n\n\t// A `repo:` qualifier names a repository by its JOINED `namespace/name` path, which is the value the rule\n\t// below is applied to. Both halves are read defensively, through the SAME locals the label is built from: the\n\t// descriptor form is only narrowed from a union by an element-type check, so a half-built descriptor reaches\n\t// here as `undefined` and must refuse rather than throw out of a facade that reports refusals as warnings —\n\t// and reporting it as `undefined/a` would name a value the caller never passed.\n\tfor (const repo of repos ?? []) {\n\t\tconst namespace = repo.namespace ?? '';\n\t\tconst name = repo.name ?? '';\n\t\tconst path = `${namespace}/${name}`;\n\t\t// BOTH the composite and each half, because neither sees what the other does:\n\t\t// - the composite catches an offender the halves cannot, since an edge of a half is an INTERIOR character\n\t\t// of the path — `{ 'git ', 'kraken' }` has two usable-looking halves and emits `repo:git /kraken`.\n\t\t// - the halves catch a BLANK one the composite cannot, since that offender sits at a composite EDGE where\n\t\t// stripping removes it — `' /a'` strips to `'/a'`, a perfectly spellable qualifier naming no repository.\n\t\t// Each half is measured after the same stripping, so `' '` and `''` are one case rather than two.\n\t\tif (\n\t\t\t!isUsableSearchScopeName(id, path) ||\n\t\t\tstripSearchScopeEdges(namespace).length === 0 ||\n\t\t\tstripSearchScopeEdges(name).length === 0\n\t\t) {\n\t\t\tunusable.push(path);\n\t\t}\n\t}\n\n\treturn unusable;\n}\n\n/**\n * Validates that a filtered issue search is scoped at all, and narrows `repos` to the descriptor form the\n * provider query needs.\n *\n * Shared by `searchIssuesPage` and `countIssues` because a count computed under different constraints than the\n * read it previews is a WRONG number rather than a missing one. The rejection is returned as a reason code, not a\n * warning: the two callers word it differently (whole-read vs naming the offending scope's key), and wording is\n * the warning layer's business.\n *\n * `repo-ids` and `unscoped` are mutually exclusive — one requires repositories and the other requires none — so\n * their relative order is free. `unusable-scope` is NOT: it must precede `unscoped`, so a value that was supplied\n * but cannot be used is never reported as a missing one.\n *\n * Every scope name is checked for naming the same scope after the provider sanitizes it, not merely for being\n * non-empty, and that distinction is a SECURITY one rather than a nicety — see {@link isUsableSearchScopeName}.\n */\nexport function resolveIssueSearchScope(\n\tid: IntegrationIds,\n\trepos: ProviderReposInput | undefined,\n\torg: string | undefined,\n\tcriteria: IssueSearchCriteria | undefined,\n): { rejection?: IssueSearchScopeRejection; repos?: ProviderRepoInput[] } {\n\tlet resolvedRepos: ProviderRepoInput[] | undefined;\n\n\tif (repos?.length) {\n\t\t// `ProviderReposInput` is a union of descriptor and id arrays; only the descriptor form is usable here.\n\t\tif (repos.some(r => typeof r === 'string' || typeof r === 'number')) {\n\t\t\treturn { rejection: { reason: 'repo-ids' } };\n\t\t}\n\n\t\tresolvedRepos = repos as ProviderRepoInput[];\n\t}\n\n\t// Checked BEFORE the \"is it scoped at all\" rule below, so an unusable value is never reported as a missing\n\t// one: it was supplied, and telling the caller to pass a scope it already passed names the wrong defect.\n\tconst unusable = unusableSearchScopeNames(id, org, resolvedRepos);\n\tif (unusable.length > 0) return { rejection: { reason: 'unusable-scope', scopes: unusable } };\n\n\tif (resolvedRepos != null) return { repos: resolvedRepos };\n\tif (org != null && org.length > 0) return {};\n\tif (criteria?.relationships?.some(r => userScopingIssueSearchRelationships.includes(r)) === true) return {};\n\n\treturn { rejection: { reason: 'unscoped' } };\n}\n\n/**\n * What {@link getSupportedFilters} reports: every vocabulary a provider's reads can express, so a consumer can\n * narrow to it BEFORE issuing a read. See that function for the capability table itself and why intersecting\n * against this is what keeps a filtered read from being refused.\n *\n * Named rather than written inline, because the same shape is the declared return type of THREE declarations that\n * all describe one value — this function, `IntegrationService.getSupportedFilters`, which only forwards to it, and\n * `IntegrationManager.getSupportedFilters`, which publishes it. Spelled out three times, a member added here\n * reached one of them and left the others a compile error away from the truth; that is exactly how the two sort\n * members below came to be written out three times.\n */\nexport type SupportedFilters = {\n\tpullRequests: PullRequestFilter[];\n\tpullRequestsAccountWide: PullRequestFilter[];\n\t/**\n\t * Criteria and scopes the filtered pull-request search can express. Always present; an empty relationship list\n\t * means the provider exposes no filtered pull-request search.\n\t */\n\tpullRequestSearch: PullRequestSearchCapabilities;\n\t/**\n\t * Filters the repo-scoped issue read accepts — and, for an issue tracker (Jira/Linear/Trello), its\n\t * project-scoped read, that being its only issue surface. A tracker therefore reports here and leaves\n\t * `issuesAccountWide` empty, so intersecting a tracker against that field would read \"cannot filter\" for a\n\t * provider that filters fine.\n\t */\n\tissues: IssueFilter[];\n\tissuesAccountWide: IssueFilter[];\n\t/**\n\t * What the filtered issue search (and the count probe over the same criteria) can express. Always present: a\n\t * provider with no filtered issue search reports an empty `relationships` and all-false flags, which is the\n\t * signal to hide that surface rather than to hide individual chips.\n\t */\n\tissueSearch: IssueSearchCapabilities;\n\t/**\n\t * Sort keys the repo-scoped issue read can express — and, for an issue tracker, its project-scoped read. Empty\n\t * means the read can't be ordered, so pass no `sort`.\n\t *\n\t * A key here is expressible on ONE provider query. A page spanning several repositories or projects is a merge,\n\t * and a merge can only order by what a normalized issue carries, so `priority`/`dueDate`/`resolved` are refused\n\t * there even where they are listed. Read one scope at a time to use them.\n\t */\n\tissueSorts: IssueSorting[];\n\t/**\n\t * Sort keys the account-wide issue read can express — a different vocabulary, not a subset: for GitLab the two\n\t * reads are different APIs. Needs no mergeability caveat, because every account-wide read is a union of several\n\t * queries, so only keys a merge can honor are listed at all. Empty for an issue tracker, which reports under\n\t * `issueSorts`.\n\t */\n\tissueSortsAccountWide: IssueSorting[];\n\t/**\n\t * Whether an issue tracker's reads can be asked for a `state` other than open (`closed`, `all`). False means\n\t * only open issues are readable, and the read refuses the other two rather than serving the open list.\n\t *\n\t * Tracker-only, like `issues`: the git hosts' state support belongs to the filtered search, under\n\t * `issueSearch.states`.\n\t */\n\tissueStates: boolean;\n};\n\n/**\n * The filters `listPullRequestsPage`/`listIssuesPage` (and the sweeps) accept for a provider, so a caller can\n * narrow to what the provider can express BEFORE issuing the read.\n *\n * This matters because the filter contract is all-or-nothing: a set containing even one unsupported filter is\n * refused outright ({@link resolvePullRequestFilters}) rather than silently narrowed, since falling through to\n * an unfiltered fetch would return every PR instead of the user's. Without this accessor a consumer had to\n * hardcode its own copy of the table — reachable only by importing the internal `providers/models.js`\n * subpath — and a copy that drifts turns a supported read into an empty page with `fetchFailed`.\n *\n * Empty means \"no filter of that kind is expressible\": either the provider has no such surface (issue trackers\n * have no pull requests; Bitbucket exposes no issues) or its metadata declares none. Callers should treat it as\n * \"don't pass filters\", not as an error. Returns copies, so mutating the result can't corrupt the metadata.\n *\n * `issues` and `issuesAccountWide` are separate because the repo-scoped and account-wide issue reads are\n * different provider queries with different filter surfaces, and the same `filters` input is validated against\n * whichever one the read uses (`repos` present or not). `issuesAccountWide` is generally the narrower of the\n * two: GitLab, for instance, can express `Assignee` and `Author` account-wide, but not `Mention`.\n *\n * That split describes the GIT-HOST reads only. An issue tracker (Jira/Linear/Trello) has neither — its issues\n * live under resource → project — so it reports its filters under `issues`, which is what\n * {@link IntegrationService.listIssueTrackerIssuesPage} validates against, and leaves `issuesAccountWide`\n * empty. Reading a tracker's capability off `issuesAccountWide` therefore under-reports it.\n *\n * `issueSearch` is a third, wider surface: the FILTERED issue search (`searchIssuesPage`, and the `countIssues`\n * probe over the same criteria), which is not bound to the user at all. It is a record of per-criterion flags\n * rather than a list, because its criteria aren't a single kind. An empty `relationships` means the provider has\n * no filtered issue search — hide the surface, not just its chips.\n *\n * `pullRequestSearch` describes the separate filtered PR search. Its table declares each criteria vocabulary\n * plus repository/organization scope support; an empty relationship list means the search itself is absent.\n *\n * `issueSorts` / `issueSortsAccountWide` / `issueSearch.sorts` are the ORDERING vocabulary of those same three\n * issue reads, split the same way and for the same reason (GitLab's repo-scoped read is GraphQL and its\n * account-wide read is REST, with genuinely different sort vocabularies). Empty means the read can't be ordered,\n * so pass no `sort`; a key not listed refuses the whole read exactly like an inexpressible filter. Note a key\n * listed here is expressible on ONE provider query: a read that fans out across projects can only honor a key\n * derivable from a normalized issue, so it refuses `priority`/`dueDate`/`resolved` on top of this table.\n *\n * Note this is a CAPABILITY table — \"what the provider can express\" — not a recommendation. A consumer\n * matching another tool's behavior may deliberately pass fewer filters than are listed here (or none, where an\n * already-scoped read would only be narrowed by them). Intersecting against this table is what keeps a\n * filtered read from being refused; it isn't a directive to use every filter in it.\n */\nexport function getSupportedFilters(providerId: IntegrationIds): SupportedFilters {\n\tconst metadata = providersMetadata[providerId];\n\tconst issueSearch = metadata?.supportedIssueSearch;\n\tconst pullRequestSearch = metadata?.supportedPullRequestSearch;\n\treturn {\n\t\tpullRequests: [...(metadata?.supportedPullRequestFilters ?? [])],\n\t\tpullRequestsAccountWide: [...(metadata?.supportedAccountWidePullRequestFilters ?? [])],\n\t\tpullRequestSearch: {\n\t\t\t...unsupportedPullRequestSearchCapabilities,\n\t\t\t...pullRequestSearch,\n\t\t\trelationships: [...(pullRequestSearch?.relationships ?? [])],\n\t\t\tstates: [...(pullRequestSearch?.states ?? [])],\n\t\t\t// Copied, so mutating the result can't corrupt the metadata table.\n\t\t\tsorts: [...(pullRequestSearch?.sorts ?? [])],\n\t\t},\n\t\tissues: [...(metadata?.supportedIssueFilters ?? [])],\n\t\tissuesAccountWide: [...(metadata?.supportedAccountWideIssueFilters ?? [])],\n\t\t// Always an object, never `undefined`: a provider WITHOUT a filtered issue search reports one whose\n\t\t// `relationships` is empty and whose flags are all false, so a consumer reads capabilities the same way\n\t\t// for every provider (and an empty `relationships` is the signal to hide the surface itself).\n\t\t//\n\t\t// Spread over the all-false baseline rather than defaulting each flag: a criterion added to\n\t\t// `IssueSearchCapabilities` then can't be forgotten here and silently reported as `undefined` (which a\n\t\t// consumer's `if (caps.x)` would read as unsupported — right answer, wrong reason, and untyped).\n\t\t// `supportedIssueSearch` is typed as the complete shape, so no member can arrive as an explicit undefined.\n\t\tissueSearch: {\n\t\t\t...unsupportedIssueSearchCapabilities,\n\t\t\t...issueSearch,\n\t\t\t// Copied, so mutating the result can't corrupt the metadata table.\n\t\t\trelationships: [...(issueSearch?.relationships ?? [])],\n\t\t\tsorts: [...(issueSearch?.sorts ?? [])],\n\t\t},\n\t\tissueSorts: [...(metadata?.supportedIssueSorts ?? [])],\n\t\tissueSortsAccountWide: [...(metadata?.supportedAccountWideIssueSorts ?? [])],\n\t\tissueStates: metadata?.supportsIssueStates === true,\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IssueShape, IssueSorting } from "../../../git/models/issue.js";
|
|
1
|
+
import { IssueShape, IssueSorting, IssueStateFilter } from "../../../git/models/issue.js";
|
|
2
2
|
import { IntegrationIds } from "../constants.js";
|
|
3
3
|
import { IssueFilter } from "../providerFilters.js";
|
|
4
4
|
import { ProviderPagedResult } from "../results.js";
|
|
@@ -29,6 +29,14 @@ export declare function listIssueTrackerIssuesPage(ctx: ProviderReadContext, opt
|
|
|
29
29
|
* ordered. Only the order within a page changes.
|
|
30
30
|
*/
|
|
31
31
|
sort?: IssueSorting;
|
|
32
|
+
/**
|
|
33
|
+
* Which issue states to read. Omitted reads open issues only.
|
|
34
|
+
*
|
|
35
|
+
* Applied server-side by every project read, so it narrows each project's drain rather than the merged page.
|
|
36
|
+
* Only a tracker declaring `ProviderMetadata.supportsIssueStates` can express anything but `'open'`; asking
|
|
37
|
+
* another for `'closed'`/`'all'` refuses the read rather than serving its open issues.
|
|
38
|
+
*/
|
|
39
|
+
state?: IssueStateFilter;
|
|
32
40
|
forceSync?: boolean;
|
|
33
41
|
page?: number;
|
|
34
42
|
cursor?: string;
|
|
@@ -13,6 +13,11 @@ import { isIssuesIntegration } from "../models/issuesIntegration.js";
|
|
|
13
13
|
import { projectKey, resourceIdForProject, resourceLabel, resourceMatchesOrg } from "./hierarchy.utils.js";
|
|
14
14
|
import { resolveIssueSort, toIssueOrdering } from "./ordering.js";
|
|
15
15
|
//#region ../plus/integrations/src/reads/issueTracker.ts
|
|
16
|
+
const issueStateFilters = [
|
|
17
|
+
"open",
|
|
18
|
+
"closed",
|
|
19
|
+
"all"
|
|
20
|
+
];
|
|
16
21
|
async function listIssueTrackerIssuesPage(ctx, options) {
|
|
17
22
|
const compositeCursor = parseIssueTrackerPageCursor(options.cursor);
|
|
18
23
|
const paginated = compositeCursor?.unpaged !== true && (options.page != null || options.cursor != null || options.itemsPerPage != null);
|
|
@@ -27,7 +32,8 @@ async function listIssueTrackerIssuesPage(ctx, options) {
|
|
|
27
32
|
nextPage: retry.nextPage,
|
|
28
33
|
retryPages: retry.pages ?? [],
|
|
29
34
|
retryProjects: retry.projects ?? [],
|
|
30
|
-
completedProjects: retry.completedProjects ?? []
|
|
35
|
+
completedProjects: retry.completedProjects ?? [],
|
|
36
|
+
state: options.state
|
|
31
37
|
}) : void 0;
|
|
32
38
|
return {
|
|
33
39
|
items,
|
|
@@ -74,6 +80,15 @@ async function listIssueTrackerIssuesPage(ctx, options) {
|
|
|
74
80
|
return emptyPage(true);
|
|
75
81
|
}
|
|
76
82
|
const sort = resolvedSort.sort;
|
|
83
|
+
if (options.state != null && (!issueStateFilters.includes(options.state) || options.state !== "open" && !providersMetadata[options.providerId]?.supportsIssueStates)) {
|
|
84
|
+
warnings.push(otherWarning(options.providerId, domain, options.connectionId, issueStateFilters.includes(options.state) ? `Issue state '${options.state}' is not supported by '${options.providerId}'.` : `Unknown issue state; expected one of ${issueStateFilters.join(", ")}.`));
|
|
85
|
+
return emptyPage(true);
|
|
86
|
+
}
|
|
87
|
+
const cursorState = compositeCursor != null ? compositeCursor.state ?? "open" : options.cursor != null ? "open" : void 0;
|
|
88
|
+
if (cursorState != null && cursorState !== (options.state ?? "open")) {
|
|
89
|
+
warnings.push(otherWarning(options.providerId, domain, options.connectionId, "The cursor belongs to a read of another issue state; start again without it."));
|
|
90
|
+
return emptyPage(true);
|
|
91
|
+
}
|
|
77
92
|
await ctx.forceRefreshIfRequested(integration, options.forceSync, options.connectionId);
|
|
78
93
|
const { value: resources, warning: resourcesWarning } = await runCaptured(options.providerId, domain, options.connectionId, () => integration.getResourcesForUserResult(options.connectionId));
|
|
79
94
|
if (resourcesWarning != null) warnings.push(resourcesWarning);
|
|
@@ -198,7 +213,8 @@ async function listIssueTrackerIssuesPage(ctx, options) {
|
|
|
198
213
|
user: userForProject(project),
|
|
199
214
|
userId: userIdForProject(project),
|
|
200
215
|
filters: options.filters,
|
|
201
|
-
sort
|
|
216
|
+
sort,
|
|
217
|
+
state: options.state
|
|
202
218
|
}, options.connectionId))
|
|
203
219
|
}));
|
|
204
220
|
let fetchFailed = projectDiscoveryFailed || accountLookupFailed;
|
|
@@ -246,7 +262,8 @@ async function listIssueTrackerIssuesPage(ctx, options) {
|
|
|
246
262
|
nextPage,
|
|
247
263
|
retryPages,
|
|
248
264
|
retryProjects: [...retryProjectKeys],
|
|
249
|
-
completedProjects: (trackCompletedProjects || projectDiscoveryFailed || projectDiscoveryTruncated || retryProjectKeys.size > 0) && (retryPages.length > 0 || nextPage != null) ? [...completedProjectKeys] : []
|
|
265
|
+
completedProjects: (trackCompletedProjects || projectDiscoveryFailed || projectDiscoveryTruncated || retryProjectKeys.size > 0) && (retryPages.length > 0 || nextPage != null) ? [...completedProjectKeys] : [],
|
|
266
|
+
state: options.state
|
|
250
267
|
});
|
|
251
268
|
return {
|
|
252
269
|
items: orderedItems,
|