@gitkraken/core-gitlens 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -1
- package/dist/git-cli/exec/git.js +1 -1
- package/dist/git-cli/providers/config.js +2 -2
- package/dist/git-cli/providers/pausedOperations.js +2 -2
- package/dist/git-cli/providers/pausedOperations.js.map +1 -1
- package/dist/git-cli/providers/staging.js +1 -1
- package/dist/ipc/discovery.js +1 -1
- package/dist/plus/agents/providers/claudeCodeTranscript.js +6 -6
- package/dist/plus/agents/providers/claudeCodeTranscript.js.map +1 -1
- package/dist/plus/ai/providers/openAICompatibleProviderBase.js +1 -1
- package/dist/plus/integrations/index.d.ts +3 -3
- package/dist/plus/integrations/index.js.map +1 -1
- package/dist/plus/integrations/integrationService.d.ts +10 -2
- package/dist/plus/integrations/integrationService.js +9 -1
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/manager.d.ts +25 -1
- package/dist/plus/integrations/models/issueReads.d.ts +5 -0
- package/dist/plus/integrations/providers/azure/azure.js +1 -1
- package/dist/plus/integrations/providers/jira.d.ts +2 -2
- package/dist/plus/integrations/providers/jira.js +2 -2
- package/dist/plus/integrations/providers/jira.js.map +1 -1
- package/dist/plus/integrations/providers/linear.d.ts +35 -2
- package/dist/plus/integrations/providers/linear.js +106 -9
- package/dist/plus/integrations/providers/linear.js.map +1 -1
- package/dist/plus/integrations/providers/models.d.ts +4 -1
- package/dist/plus/integrations/providers/models.js.map +1 -1
- package/dist/plus/integrations/providers/providerApiClient.js +4 -1
- package/dist/plus/integrations/providers/providerApiClient.js.map +1 -1
- package/dist/plus/integrations/providers/providerErrors.d.ts +4 -1
- package/dist/plus/integrations/providers/providerErrors.js +13 -4
- package/dist/plus/integrations/providers/providerErrors.js.map +1 -1
- package/dist/plus/integrations/providers/providersApi.d.ts +7 -1
- package/dist/plus/integrations/providers/providersApi.js +9 -1
- package/dist/plus/integrations/providers/providersApi.js.map +1 -1
- package/dist/plus/integrations/reads/cursors.d.ts +8 -0
- package/dist/plus/integrations/reads/cursors.js +14 -4
- package/dist/plus/integrations/reads/cursors.js.map +1 -1
- package/dist/plus/integrations/reads/hierarchy.d.ts +8 -2
- package/dist/plus/integrations/reads/hierarchy.js +42 -2
- package/dist/plus/integrations/reads/hierarchy.js.map +1 -1
- package/dist/plus/integrations/reads/issueTracker.d.ts +10 -0
- package/dist/plus/integrations/reads/issueTracker.js +46 -5
- package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
- package/dist/plus/integrations/reads/selectionConfirmation.d.ts +5 -3
- package/dist/plus/integrations/reads/selectionConfirmation.js.map +1 -1
- package/dist/plus/integrations/reads/warnings.js +1 -1
- package/dist/plus/integrations/reads/warnings.js.map +1 -1
- package/dist/plus/integrations/results.d.ts +18 -2
- package/dist/plus/integrations/results.js.map +1 -1
- package/dist/plus/integrations/utils/integration.utils.d.ts +3 -0
- package/dist/plus/integrations/utils/integration.utils.js +5 -1
- package/dist/plus/integrations/utils/integration.utils.js.map +1 -1
- package/dist/utils/env/node/exec.js +2 -2
- package/docs/integrations.md +37 -21
- package/package.json +1 -1
|
@@ -28,7 +28,14 @@ export interface IssueTrackerPageCursor {
|
|
|
28
28
|
* were never read.
|
|
29
29
|
*/
|
|
30
30
|
state?: 'closed' | 'all';
|
|
31
|
+
/**
|
|
32
|
+
* The Linear Projects the read that minted this cursor was narrowed to, sorted, omitted for none. Recorded for the
|
|
33
|
+
* reason `state` is: a team drained under one set of Projects has not been read under another.
|
|
34
|
+
*/
|
|
35
|
+
linearProjects?: string[];
|
|
31
36
|
}
|
|
37
|
+
/** Non-empty strings only, deduped and sorted; `undefined` when nothing survives (so the field is omitted). */
|
|
38
|
+
export declare function linearProjectIds(values: unknown): string[] | undefined;
|
|
32
39
|
export declare function parseIssueTrackerPageCursor(cursor: string | undefined): IssueTrackerPageCursor | undefined;
|
|
33
40
|
export declare function toIssueTrackerPageCursor(options: {
|
|
34
41
|
currentPage: number;
|
|
@@ -38,6 +45,7 @@ export declare function toIssueTrackerPageCursor(options: {
|
|
|
38
45
|
retryProjects: readonly string[];
|
|
39
46
|
completedProjects?: readonly string[];
|
|
40
47
|
state?: IssueStateFilter;
|
|
48
|
+
linearProjects?: readonly string[];
|
|
41
49
|
}): string | undefined;
|
|
42
50
|
/** Identifies one org slice of the fan-out. Connection and domain are part of the key: two accounts, or two
|
|
43
51
|
* self-managed hosts, can each have an org of the same name. */
|
|
@@ -8,6 +8,12 @@ function positiveIntegers(values) {
|
|
|
8
8
|
const valid = values.filter((value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0);
|
|
9
9
|
return valid.length > 0 ? [...new Set(valid)] : void 0;
|
|
10
10
|
}
|
|
11
|
+
/** Non-empty strings only, deduped and sorted; `undefined` when nothing survives (so the field is omitted). */
|
|
12
|
+
function linearProjectIds(values) {
|
|
13
|
+
if (!Array.isArray(values)) return void 0;
|
|
14
|
+
const valid = values.filter((value) => typeof value === "string" && value.length > 0);
|
|
15
|
+
return valid.length > 0 ? [...new Set(valid)].sort() : void 0;
|
|
16
|
+
}
|
|
11
17
|
/**
|
|
12
18
|
* Non-empty project keys only, upgraded then deduped, so a legacy and a current key for one project merge;
|
|
13
19
|
* `undefined` when nothing survives (so the field is omitted).
|
|
@@ -40,6 +46,7 @@ function parseIssueTrackerPageCursor(cursor) {
|
|
|
40
46
|
const retryPages = positiveIntegers(parsed.retryPages);
|
|
41
47
|
const retryProjects = projectKeys(parsed.retryProjects);
|
|
42
48
|
const completedProjects = projectKeys(parsed.completedProjects);
|
|
49
|
+
const linearProjects = linearProjectIds(parsed.linearProjects);
|
|
43
50
|
return {
|
|
44
51
|
type: "issue-tracker-page",
|
|
45
52
|
currentPage: parsed.currentPage,
|
|
@@ -48,18 +55,20 @@ function parseIssueTrackerPageCursor(cursor) {
|
|
|
48
55
|
...retryPages != null ? { retryPages } : {},
|
|
49
56
|
...retryProjects != null ? { retryProjects } : {},
|
|
50
57
|
...completedProjects != null ? { completedProjects } : {},
|
|
51
|
-
...parsed.state === "closed" || parsed.state === "all" ? { state: parsed.state } : {}
|
|
58
|
+
...parsed.state === "closed" || parsed.state === "all" ? { state: parsed.state } : {},
|
|
59
|
+
...linearProjects != null ? { linearProjects } : {}
|
|
52
60
|
};
|
|
53
61
|
} catch {
|
|
54
62
|
return;
|
|
55
63
|
}
|
|
56
64
|
}
|
|
57
65
|
function toIssueTrackerPageCursor(options) {
|
|
66
|
+
const linearProjects = linearProjectIds(options.linearProjects);
|
|
58
67
|
const retryPages = [...new Set(options.retryPages)].sort((a, b) => a - b);
|
|
59
68
|
const retryProjects = [...new Set(options.retryProjects)].sort();
|
|
60
69
|
const completedProjects = retryPages.length > 0 || retryProjects.length > 0 || options.nextPage != null ? [...new Set(options.completedProjects ?? [])].sort() : [];
|
|
61
70
|
if (retryPages.length === 0 && retryProjects.length === 0 && options.nextPage == null) return;
|
|
62
|
-
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);
|
|
71
|
+
if (retryPages.length === 0 && retryProjects.length === 0 && completedProjects.length === 0 && options.nextPage != null && options.unpaged !== true && (options.state == null || options.state === "open") && linearProjects == null) return toPageCursor(options.nextPage);
|
|
63
72
|
return JSON.stringify({
|
|
64
73
|
type: "issue-tracker-page",
|
|
65
74
|
currentPage: options.currentPage,
|
|
@@ -68,7 +77,8 @@ function toIssueTrackerPageCursor(options) {
|
|
|
68
77
|
...retryPages.length > 0 ? { retryPages } : {},
|
|
69
78
|
...retryProjects.length > 0 ? { retryProjects } : {},
|
|
70
79
|
...completedProjects.length > 0 ? { completedProjects } : {},
|
|
71
|
-
...options.state === "closed" || options.state === "all" ? { state: options.state } : {}
|
|
80
|
+
...options.state === "closed" || options.state === "all" ? { state: options.state } : {},
|
|
81
|
+
...linearProjects != null ? { linearProjects } : {}
|
|
72
82
|
});
|
|
73
83
|
}
|
|
74
84
|
/** Whether a bundle entry addresses the same org slice as `org`, comparing domains by host. */
|
|
@@ -113,6 +123,6 @@ function toBroadenIssuesCursor(cursors, exhausted, orgCount) {
|
|
|
113
123
|
});
|
|
114
124
|
}
|
|
115
125
|
//#endregion
|
|
116
|
-
export { getBroadenIssuesCursor, isBroadenIssuesOrgExhausted, parseIssueTrackerPageCursor, toBroadenIssuesCursor, toIssueTrackerPageCursor };
|
|
126
|
+
export { getBroadenIssuesCursor, isBroadenIssuesOrgExhausted, linearProjectIds, parseIssueTrackerPageCursor, toBroadenIssuesCursor, toIssueTrackerPageCursor };
|
|
117
127
|
|
|
118
128
|
//# sourceMappingURL=cursors.js.map
|
|
@@ -1 +1 @@
|
|
|
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/**\n * Non-empty project keys only, upgraded then deduped, so a legacy and a current key for one project merge;\n * `undefined` when nothing survives (so the field is omitted).\n */\nfunction projectKeys(values: unknown): string[] | undefined {\n\tif (!Array.isArray(values)) return undefined;\n\n\tconst valid = values\n\t\t.filter((value): value is string => typeof value === 'string' && value.length > 0)\n\t\t.map(upgradeProjectKey);\n\treturn valid.length > 0 ? [...new Set(valid)] : undefined;\n}\n\n/**\n * Reads a `projectKey` (`[resourceId, id || key]`) from a cursor, converting one minted when it also carried the\n * project key (`[resourceId, id, key]`) so that cursor still resumes its retrying and completed projects.\n */\nfunction upgradeProjectKey(key: string): string {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(key);\n\t} catch {\n\t\treturn key;\n\t}\n\tif (!Array.isArray(parsed) || parsed.length !== 3 || !parsed.every(part => typeof part === 'string')) return key;\n\n\tconst [resourceId, id, legacyKey] = parsed as [string, string, string];\n\treturn JSON.stringify([resourceId, id || legacyKey]);\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 = projectKeys(parsed.retryProjects);\n\t\tconst completedProjects = projectKeys(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
|
+
{"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\t/**\n\t * The Linear Projects the read that minted this cursor was narrowed to, sorted, omitted for none. Recorded for the\n\t * reason `state` is: a team drained under one set of Projects has not been read under another.\n\t */\n\tlinearProjects?: 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 and sorted; `undefined` when nothing survives (so the field is omitted). */\nexport function linearProjectIds(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)].sort() : undefined;\n}\n\n/**\n * Non-empty project keys only, upgraded then deduped, so a legacy and a current key for one project merge;\n * `undefined` when nothing survives (so the field is omitted).\n */\nfunction projectKeys(values: unknown): string[] | undefined {\n\tif (!Array.isArray(values)) return undefined;\n\n\tconst valid = values\n\t\t.filter((value): value is string => typeof value === 'string' && value.length > 0)\n\t\t.map(upgradeProjectKey);\n\treturn valid.length > 0 ? [...new Set(valid)] : undefined;\n}\n\n/**\n * Reads a `projectKey` (`[resourceId, id || key]`) from a cursor, converting one minted when it also carried the\n * project key (`[resourceId, id, key]`) so that cursor still resumes its retrying and completed projects.\n */\nfunction upgradeProjectKey(key: string): string {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(key);\n\t} catch {\n\t\treturn key;\n\t}\n\tif (!Array.isArray(parsed) || parsed.length !== 3 || !parsed.every(part => typeof part === 'string')) return key;\n\n\tconst [resourceId, id, legacyKey] = parsed as [string, string, string];\n\treturn JSON.stringify([resourceId, id || legacyKey]);\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 = projectKeys(parsed.retryProjects);\n\t\tconst completedProjects = projectKeys(parsed.completedProjects);\n\t\tconst linearProjects = linearProjectIds(parsed.linearProjects);\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\t...(linearProjects != null ? { linearProjects: linearProjects } : {}),\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\tlinearProjects?: readonly string[];\n}): string | undefined {\n\tconst linearProjects = linearProjectIds(options.linearProjects);\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\tlinearProjects == null\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\t...(linearProjects != null ? { linearProjects: linearProjects } : {}),\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":";;;;;AA8CA,SAAS,iBAAiB,QAAuC;CAChE,IAAI,CAAC,MAAM,QAAQ,MAAM,GAAG,OAAO,KAAA;CAEnC,MAAM,QAAQ,OAAO,QACnB,UAA2B,OAAO,UAAU,YAAY,OAAO,cAAc,KAAK,KAAK,QAAQ,CACjG;CACA,OAAO,MAAM,SAAS,IAAI,CAAC,GAAG,IAAI,IAAI,KAAK,CAAC,IAAI,KAAA;AACjD;;AAGA,SAAgB,iBAAiB,QAAuC;CACvE,IAAI,CAAC,MAAM,QAAQ,MAAM,GAAG,OAAO,KAAA;CAEnC,MAAM,QAAQ,OAAO,QAAQ,UAA2B,OAAO,UAAU,YAAY,MAAM,SAAS,CAAC;CACrG,OAAO,MAAM,SAAS,IAAI,CAAC,GAAG,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,KAAA;AACxD;;;;;AAMA,SAAS,YAAY,QAAuC;CAC3D,IAAI,CAAC,MAAM,QAAQ,MAAM,GAAG,OAAO,KAAA;CAEnC,MAAM,QAAQ,OACZ,QAAQ,UAA2B,OAAO,UAAU,YAAY,MAAM,SAAS,CAAC,CAAC,CACjF,IAAI,iBAAiB;CACvB,OAAO,MAAM,SAAS,IAAI,CAAC,GAAG,IAAI,IAAI,KAAK,CAAC,IAAI,KAAA;AACjD;;;;;AAMA,SAAS,kBAAkB,KAAqB;CAC/C,IAAI;CACJ,IAAI;EACH,SAAS,KAAK,MAAM,GAAG;CACxB,QAAQ;EACP,OAAO;CACR;CACA,IAAI,CAAC,MAAM,QAAQ,MAAM,KAAK,OAAO,WAAW,KAAK,CAAC,OAAO,OAAM,SAAQ,OAAO,SAAS,QAAQ,GAAG,OAAO;CAE7G,MAAM,CAAC,YAAY,IAAI,aAAa;CACpC,OAAO,KAAK,UAAU,CAAC,YAAY,MAAM,SAAS,CAAC;AACpD;AAEA,SAAgB,4BAA4B,QAAgE;CAC3G,IAAI,UAAU,MAAM,OAAO,KAAA;CAE3B,IAAI;EACH,MAAM,SAAS,KAAK,MAAM,MAAM;EAChC,IACC,OAAO,SAAS,wBAChB,OAAO,OAAO,gBAAgB,YAC9B,CAAC,OAAO,cAAc,OAAO,WAAW,KACxC,OAAO,cAAc,GAErB;EAGD,MAAM,aAAa,iBAAiB,OAAO,UAAU;EACrD,MAAM,gBAAgB,YAAY,OAAO,aAAa;EACtD,MAAM,oBAAoB,YAAY,OAAO,iBAAiB;EAC9D,MAAM,iBAAiB,iBAAiB,OAAO,cAAc;EAC7D,OAAO;GACN,MAAM;GACN,aAAa,OAAO;GACpB,GAAI,OAAO,YAAY,OAAO,EAAE,SAAS,KAAK,IAAI,CAAC;GACnD,GAAI,OAAO,OAAO,aAAa,YAAY,OAAO,cAAc,OAAO,QAAQ,KAAK,OAAO,WAAW,IACnG,EAAE,UAAU,OAAO,SAAS,IAC5B,CAAC;GACJ,GAAI,cAAc,OAAO,EAAc,WAAW,IAAI,CAAC;GACvD,GAAI,iBAAiB,OAAO,EAAiB,cAAc,IAAI,CAAC;GAChE,GAAI,qBAAqB,OAAO,EAAqB,kBAAkB,IAAI,CAAC;GAC5E,GAAI,OAAO,UAAU,YAAY,OAAO,UAAU,QAAQ,EAAE,OAAO,OAAO,MAAM,IAAI,CAAC;GACrF,GAAI,kBAAkB,OAAO,EAAkB,eAAe,IAAI,CAAC;EACpE;CACD,QAAQ;EACP;CACD;AACD;AAEA,SAAgB,yBAAyB,SASlB;CACtB,MAAM,iBAAiB,iBAAiB,QAAQ,cAAc;CAC9D,MAAM,aAAa,CAAC,GAAG,IAAI,IAAI,QAAQ,UAAU,CAAC,CAAC,CAAC,MAAM,GAAG,MAAM,IAAI,CAAC;CACxE,MAAM,gBAAgB,CAAC,GAAG,IAAI,IAAI,QAAQ,aAAa,CAAC,CAAC,CAAC,KAAK;CAC/D,MAAM,oBACL,WAAW,SAAS,KAAK,cAAc,SAAS,KAAK,QAAQ,YAAY,OACtE,CAAC,GAAG,IAAI,IAAI,QAAQ,qBAAqB,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,IACnD,CAAC;CACL,IAAI,WAAW,WAAW,KAAK,cAAc,WAAW,KAAK,QAAQ,YAAY,MAChF;CAID,IACC,WAAW,WAAW,KACtB,cAAc,WAAW,KACzB,kBAAkB,WAAW,KAC7B,QAAQ,YAAY,QACpB,QAAQ,YAAY,SAGnB,QAAQ,SAAS,QAAQ,QAAQ,UAAU,WAC5C,kBAAkB,MAElB,OAAO,aAAa,QAAQ,QAAQ;CAErC,OAAO,KAAK,UAAU;EACrB,MAAM;EACN,aAAa,QAAQ;EACrB,GAAI,QAAQ,YAAY,OAAO,EAAE,SAAS,KAAK,IAAI,CAAC;EACpD,GAAI,QAAQ,YAAY,OAAO,EAAE,UAAU,QAAQ,SAAS,IAAI,CAAC;EACjE,GAAI,WAAW,SAAS,IAAI,EAAc,WAAW,IAAI,CAAC;EAC1D,GAAI,cAAc,SAAS,IAAI,EAAiB,cAAc,IAAI,CAAC;EACnE,GAAI,kBAAkB,SAAS,IAAI,EAAqB,kBAAkB,IAAI,CAAC;EAC/E,GAAI,QAAQ,UAAU,YAAY,QAAQ,UAAU,QAAQ,EAAE,OAAO,QAAQ,MAAM,IAAI,CAAC;EACxF,GAAI,kBAAkB,OAAO,EAAkB,eAAe,IAAI,CAAC;CACpE,CAAkC;AACnC;;AA4BA,SAAS,WACR,OACA,KACU;CACV,OACC,MAAM,eAAe,IAAI,cACzB,MAAM,QAAQ,IAAI,QAClB,MAAM,iBAAiB,IAAI,iBAC1B,eAAe,MAAM,MAAM,KAAK,MAAM,aAAa,eAAe,IAAI,MAAM,KAAK,IAAI;AAExF;;;;;;AAOA,SAAgB,uBACf,QACA,KACA,MACA,UACqB;CACrB,IAAI,aAAa,GAAG,OAAO,UAAU,aAAa,IAAI;CAEtD,IAAI,UAAU,MACb,IAAI;EAEH,MAAM,QADS,KAAK,MAAM,MACP,CAAC,CAAC,SAAS,MAAK,MAAK,WAAW,GAAG,GAAG,CAAC;EAC1D,IAAI,OAAO,UAAU,MAAM,OAAO,MAAM;EACxC,IAAI,OAAO,OAAO,cAAc,YAAY,OAAO,cAAc,MAAM,SAAS,KAAK,MAAM,YAAY,GACtG,OAAO,aAAa,MAAM,SAAS;CAErC,QAAQ,CAAC;CAGV,OAAO,aAAa,IAAI;AACzB;;;;;;;AAQA,SAAgB,4BACf,QACA,KACA,UACU;CACV,IAAI,aAAa,KAAK,UAAU,MAAM,OAAO;CAE7C,IAAI;EAEH,OADe,KAAK,MAAM,MACd,CAAC,CAAC,WAAW,MAAK,MAAK,WAAW,GAAG,GAAG,CAAC,KAAK;CAC3D,QAAQ;EACP,OAAO;CACR;AACD;;AAGA,SAAgB,sBACf,SACA,WACA,UACqB;CACrB,IAAI,QAAQ,WAAW,GAAG,OAAO,KAAA;CACjC,IAAI,aAAa,GAChB,OAAO,QAAQ,EAAE,CAAC,WAAW,QAAQ,EAAE,CAAC,aAAa,OAAO,aAAa,QAAQ,EAAE,CAAC,SAAS,IAAI,KAAA;CAKlG,OAAO,KAAK,UAAU;EAAW;EAAoB;CAAU,CAAC;AACjE"}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { IntegrationIds } from "../constants.js";
|
|
2
|
-
import { ProviderOrganization, ProviderPagedResult, ProviderRepositoryShape, ProviderResult } from "../results.js";
|
|
2
|
+
import { ProviderLinearProjectShape, ProviderOrganization, ProviderPagedResult, ProviderRepositoryShape, ProviderResult } from "../results.js";
|
|
3
3
|
import { ProviderReadContext } from "./context.js";
|
|
4
|
-
import { ListOrgsOptions, ListProjectsOptions } from "../manager.js";
|
|
4
|
+
import { ListLinearProjectsOptions, ListOrgsOptions, ListProjectsOptions } from "../manager.js";
|
|
5
5
|
//#region ../plus/integrations/src/reads/hierarchy.d.ts
|
|
6
6
|
export declare function listOrgs(ctx: ProviderReadContext, options?: ListOrgsOptions): Promise<ProviderResult<ProviderOrganization>>;
|
|
7
7
|
export declare function listProjects(ctx: ProviderReadContext, options?: ListProjectsOptions): Promise<ProviderResult<ProviderOrganization>>;
|
|
8
|
+
/**
|
|
9
|
+
* The Linear workspace's Projects (Linear's own `Project` entity), apart from the teams {@link listProjects} lists as
|
|
10
|
+
* Linear's projects. Reads the same connection a Linear issue read would, through the integration's cached, drained
|
|
11
|
+
* read, and reports a drain it could not finish as `fetchFailed` with a warning, as {@link listProjects} does.
|
|
12
|
+
*/
|
|
13
|
+
export declare function listLinearProjects(ctx: ProviderReadContext, options?: ListLinearProjectsOptions): Promise<ProviderResult<ProviderLinearProjectShape>>;
|
|
8
14
|
export declare function listRepos(ctx: ProviderReadContext, options: {
|
|
9
15
|
providerId: IntegrationIds;
|
|
10
16
|
org?: string;
|
|
@@ -2,7 +2,7 @@ import { mapBounded } from "../../../utils/promise.js";
|
|
|
2
2
|
import { supportedOrderedCloudIntegrationIds, supportedOrderedCloudIssuesIntegrationIds } from "../constants.js";
|
|
3
3
|
import { appendDedupedWarning } from "../results.js";
|
|
4
4
|
import { mergeAssessmentInto } from "../collectionMetadata.js";
|
|
5
|
-
import { isGitHostIntegration, isIssuesHostIntegrationId, warnOnMissingSessionForDomain } from "../utils/integration.utils.js";
|
|
5
|
+
import { isGitHostIntegration, isIssuesHostIntegrationId, isLinearIntegration, warnOnMissingSessionForDomain } from "../utils/integration.utils.js";
|
|
6
6
|
import { toProviderRepositoryShape } from "../providers/models.js";
|
|
7
7
|
import { isIssuesIntegration } from "../models/issuesIntegration.js";
|
|
8
8
|
import { pageToCursor, refusedPage, resolveContinuation, resolveCurrentPage, toProviderPageInfo } from "./paging.js";
|
|
@@ -159,6 +159,46 @@ async function listProjects(ctx, options) {
|
|
|
159
159
|
});
|
|
160
160
|
return mergeHierarchyResults(results);
|
|
161
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* The Linear workspace's Projects (Linear's own `Project` entity), apart from the teams {@link listProjects} lists as
|
|
164
|
+
* Linear's projects. Reads the same connection a Linear issue read would, through the integration's cached, drained
|
|
165
|
+
* read, and reports a drain it could not finish as `fetchFailed` with a warning, as {@link listProjects} does.
|
|
166
|
+
*/
|
|
167
|
+
async function listLinearProjects(ctx, options) {
|
|
168
|
+
const id = "linear";
|
|
169
|
+
const connectionId = options?.connectionId;
|
|
170
|
+
const integration = await ctx.getIntegrationForRead(id, connectionId);
|
|
171
|
+
if (integration == null) {
|
|
172
|
+
const early = ctx.earlyReturnConnectionWarnings(id, connectionId);
|
|
173
|
+
return {
|
|
174
|
+
items: [],
|
|
175
|
+
warnings: early.warnings,
|
|
176
|
+
fetchFailed: early.fetchFailed || void 0
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
if (!isLinearIntegration(integration)) return {
|
|
180
|
+
items: [],
|
|
181
|
+
warnings: []
|
|
182
|
+
};
|
|
183
|
+
const domain = ctx.domainForRead(integration, id, connectionId);
|
|
184
|
+
await ctx.forceRefreshIfRequested(integration, options?.forceSync, connectionId);
|
|
185
|
+
const warnings = [];
|
|
186
|
+
const { value: projects, warning } = await runCaptured(id, domain, connectionId, () => integration.getWorkspaceProjectsResult(connectionId));
|
|
187
|
+
let fetchFailed = warning != null && projects == null;
|
|
188
|
+
if (warning != null) warnings.push(warning);
|
|
189
|
+
if (projects != null && mergeHierarchyIncompleteness(warnings, id, domain, connectionId, projects, "Project")) fetchFailed = true;
|
|
190
|
+
return {
|
|
191
|
+
items: (projects?.values ?? []).map((project) => ({
|
|
192
|
+
id: project.id,
|
|
193
|
+
providerId: id,
|
|
194
|
+
name: project.name,
|
|
195
|
+
state: project.state,
|
|
196
|
+
url: project.url
|
|
197
|
+
})),
|
|
198
|
+
warnings,
|
|
199
|
+
fetchFailed: fetchFailed || void 0
|
|
200
|
+
};
|
|
201
|
+
}
|
|
162
202
|
async function listRepos(ctx, options) {
|
|
163
203
|
const page = Math.max(1, Math.trunc(options.page ?? 1));
|
|
164
204
|
if (isIssuesHostIntegrationId(options.providerId)) return refusedPage(page, [gitHostOnlySurfaceWarning(options.providerId, void 0, options.connectionId, "repository discovery")], true);
|
|
@@ -252,6 +292,6 @@ async function listRepos(ctx, options) {
|
|
|
252
292
|
return result;
|
|
253
293
|
}
|
|
254
294
|
//#endregion
|
|
255
|
-
export { listOrgs, listProjects, listRepos };
|
|
295
|
+
export { listLinearProjects, listOrgs, listProjects, listRepos };
|
|
256
296
|
|
|
257
297
|
//# sourceMappingURL=hierarchy.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hierarchy.js","names":[],"sources":["../../../../../plus/integrations/src/reads/hierarchy.ts"],"sourcesContent":["import { mapBounded } from '@gitlens/utils/promise.js';\nimport { mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport {\n\tGitCloudHostIntegrationId,\n\tGitSelfManagedHostIntegrationId,\n\tproviderFanOutConcurrency,\n\tsupportedOrderedCloudIntegrationIds,\n\tsupportedOrderedCloudIssuesIntegrationIds,\n} from '../constants.js';\nimport type { ListOrgsOptions, ListProjectsOptions } from '../manager.js';\nimport { isIssuesIntegration } from '../models/issuesIntegration.js';\nimport type { ProviderHierarchyResult } from '../providers/models.js';\nimport { toProviderRepositoryShape } from '../providers/models.js';\nimport type {\n\tProviderOrganization,\n\tProviderPagedResult,\n\tProviderRepositoryShape,\n\tProviderResult,\n\tProviderWarning,\n} from '../results.js';\nimport { appendDedupedWarning } from '../results.js';\nimport {\n\tisGitHostIntegration,\n\tisIssuesHostIntegrationId,\n\twarnOnMissingSessionForDomain,\n} from '../utils/integration.utils.js';\nimport type { ProviderReadContext } from './context.js';\nimport { discoverResources, runCaptured } from './drains.js';\nimport {\n\tassertHierarchyReadTarget,\n\tmergeHierarchyResults,\n\tprojectToOrg,\n\tresourceMatchesOrg,\n\tresourceToOrg,\n\twithProviderContext,\n} from './hierarchy.utils.js';\nimport { pageToCursor, refusedPage, resolveContinuation, resolveCurrentPage, toProviderPageInfo } from './paging.js';\nimport { gitHostOnlySurfaceWarning, truncationWarning, unsupportedWarning } from './warnings.js';\n\n/**\n * Folds a flat hierarchy result's incompleteness into `warnings`, returning whether it leaves the read\n * non-authoritative. The single funnel for both hierarchy reads, so neither can drift from the other on any of\n * the four decisions it makes:\n *\n * - The metadata is assessed FIRST, so the truncation warning can tell whether it already explained this gap.\n * Ordered deliberately: emitting first meant an expired credential produced BOTH a typed `auth` warning and\n * an unclassifiable one for the same cause, and a consumer routing on classification showed the second as\n * \"this provider failed to load\", with a remedy that doesn't exist, next to the reconnect prompt that was the\n * actual fix.\n * - The read states the truncation itself only when the metadata reported nothing — the rule\n * `assessCollectionMetadata` applies to its own generic fallback. Whatever the metadata reported already names\n * this provider and says something stronger, so restating it costs a false verdict and adds no information.\n * - `'exhausted'`, never `'page-budget'`: `truncated` is one boolean here, so a drain that stopped at its page\n * budget is indistinguishable from one whose cursor stalled, and `IncompleteReadCause` makes `'exhausted'`\n * the default whenever a raisable budget is not demonstrably the cause.\n * - A truncation counts as non-authoritative even though the omission above asserts the read succeeded. Unlike\n * paged repository reads, a flattened hierarchy result has no page object on which to carry incompleteness,\n * so `fetchFailed` is the only signal a consumer has that the list is short.\n */\nfunction mergeHierarchyIncompleteness(\n\twarnings: ProviderWarning[],\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tresult: Pick<ProviderHierarchyResult<unknown>, 'truncated' | 'metadata'>,\n\treadKind: 'Organization' | 'Project',\n): boolean {\n\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, result.metadata);\n\tif (!result.truncated) return assessment.fetchFailed || assessment.truncated;\n\n\tif (!assessment.reported) {\n\t\t// Deduped like every other `truncationWarning` emission (see `drains.ts`), not plain-pushed: no caller can\n\t\t// collide with it today, but nothing about this funnel guarantees a third read won't.\n\t\tappendDedupedWarning(warnings, truncationWarning(id, domain, connectionId, readKind, 'exhausted'));\n\t}\n\treturn true;\n}\n\nexport async function listOrgs(\n\tctx: ProviderReadContext,\n\toptions?: ListOrgsOptions,\n): Promise<ProviderResult<ProviderOrganization>> {\n\tassertHierarchyReadTarget('listOrgs', options);\n\tconst ids = options?.providerId != null ? [options.providerId] : supportedOrderedCloudIntegrationIds;\n\tconst singleProvider = ids.length === 1;\n\tconst connectionId = singleProvider ? options?.connectionId : undefined;\n\tconst requestedDomain = singleProvider ? options?.domain : undefined;\n\n\tconst results = await mapBounded(ids, providerFanOutConcurrency, async id => {\n\t\tconst integration = await ctx.getIntegrationForRead(id, connectionId, requestedDomain);\n\t\tif (integration == null) {\n\t\t\t// A specifically requested connection that can't be resolved is a broken connection, not a\n\t\t\t// provider with no orgs — surface it (warning + fetchFailed) instead of dropping the id\n\t\t\t// silently, so a caller can tell it apart from an account that genuinely has no orgs.\n\t\t\tconst early = ctx.earlyReturnConnectionWarnings(id, connectionId, requestedDomain);\n\t\t\treturn {\n\t\t\t\titems: [] as ProviderOrganization[],\n\t\t\t\twarnings: early.warnings,\n\t\t\t\tfetchFailed: early.fetchFailed,\n\t\t\t};\n\t\t}\n\n\t\tconst items: ProviderOrganization[] = [];\n\t\tconst warnings: ProviderWarning[] = [];\n\t\tlet fetchFailed = false;\n\t\tconst domain = ctx.domainForRead(integration, id, connectionId, requestedDomain);\n\t\tconst warnOnMissingSession = warnOnMissingSessionForDomain(id, requestedDomain);\n\t\tif (isIssuesIntegration(integration)) {\n\t\t\t// Issue trackers expose \"resources\" (Jira sites, Linear orgs, …) as their org analogue.\n\t\t\tconst {\n\t\t\t\tvalue: resources,\n\t\t\t\twarning,\n\t\t\t\tmetadata,\n\t\t\t} = await discoverResources(integration, id, domain, connectionId, {\n\t\t\t\twarnOnMissingSession: warnOnMissingSession,\n\t\t\t});\n\t\t\tif (resources != null) {\n\t\t\t\titems.push(...resources.map(r => resourceToOrg(id, r)));\n\t\t\t}\n\t\t\tif (warning != null) {\n\t\t\t\twarnings.push(warning);\n\t\t\t\t// A warning with no value is a hard read failure, not an empty account.\n\t\t\t\tif (resources == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (mergeAssessmentInto(warnings, id, domain, connectionId, metadata).truncated) {\n\t\t\t\tfetchFailed = true;\n\t\t\t}\n\t\t} else if (!integration.supportsOrganizationDiscovery) {\n\t\t\t// The provider registers no org-discovery hook. Report it as\n\t\t\t// explicitly unsupported rather than contributing a silent empty list that a caller can't\n\t\t\t// tell apart from \"this account has no orgs\".\n\t\t\tfetchFailed = true;\n\t\t\twarnings.push(\n\t\t\t\tunsupportedWarning(id, domain, connectionId, `Organization discovery is not supported by '${id}'.`),\n\t\t\t);\n\t\t} else {\n\t\t\tconst { value, warning } = await runCaptured(\n\t\t\t\tid,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t() => integration.getOrganizationsForUserResult(connectionId),\n\t\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t\t);\n\t\t\tif (value != null) {\n\t\t\t\titems.push(...value.values.map(org => withProviderContext(id, org)));\n\n\t\t\t\tif (mergeHierarchyIncompleteness(warnings, id, domain, connectionId, value, 'Organization')) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (warning != null) {\n\t\t\t\twarnings.push(warning);\n\t\t\t\tif (value == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed };\n\t});\n\n\treturn mergeHierarchyResults(results);\n}\n\nexport async function listProjects(\n\tctx: ProviderReadContext,\n\toptions?: ListProjectsOptions,\n): Promise<ProviderResult<ProviderOrganization>> {\n\tassertHierarchyReadTarget('listProjects', options);\n\tconst ids =\n\t\toptions?.providerId != null\n\t\t\t? [options.providerId]\n\t\t\t: [\n\t\t\t\t\t...supportedOrderedCloudIssuesIntegrationIds,\n\t\t\t\t\tGitCloudHostIntegrationId.AzureDevOps,\n\t\t\t\t\tGitSelfManagedHostIntegrationId.AzureDevOpsServer,\n\t\t\t\t];\n\tconst singleProvider = ids.length === 1;\n\tconst connectionId = singleProvider ? options?.connectionId : undefined;\n\tconst requestedDomain = singleProvider ? options?.domain : undefined;\n\n\tconst results = await mapBounded(ids, providerFanOutConcurrency, async id => {\n\t\tconst integration = await ctx.getIntegrationForRead(id, connectionId, requestedDomain);\n\t\tif (integration == null) {\n\t\t\t// A requested connection that can't be resolved is a broken connection, not a provider with\n\t\t\t// no projects — surface it (warning + fetchFailed) instead of dropping the id silently.\n\t\t\tconst early = ctx.earlyReturnConnectionWarnings(id, connectionId, requestedDomain);\n\t\t\treturn {\n\t\t\t\titems: [] as ProviderOrganization[],\n\t\t\t\twarnings: early.warnings,\n\t\t\t\tfetchFailed: early.fetchFailed,\n\t\t\t};\n\t\t}\n\n\t\tconst items: ProviderOrganization[] = [];\n\t\tconst warnings: ProviderWarning[] = [];\n\t\tlet fetchFailed = false;\n\t\tconst domain = ctx.domainForRead(integration, id, connectionId, requestedDomain);\n\t\tconst org = options?.org;\n\t\tconst warnOnMissingSession = warnOnMissingSessionForDomain(id, requestedDomain);\n\n\t\t// Git hosts with a project tier (Azure DevOps) read projects through their own hierarchy hook,\n\t\t// scoped to `org` when given. Non-Azure git hosts have no project tier and return undefined.\n\t\tif (!isIssuesIntegration(integration)) {\n\t\t\t// Check the capability first, like listOrgs/listRepos/listIssuesPage do. Without it, a host\n\t\t\t// with no project tier returns `undefined` from a successful read, which `runCaptured` can't\n\t\t\t// tell apart from an unresolvable session — turning a healthy provider into a `no-connection`\n\t\t\t// warning + `fetchFailed` (and, in Kepler, a spurious reconnect prompt).\n\t\t\tif (!integration.supportsProjectDiscovery) {\n\t\t\t\treturn { items: items, warnings: warnings, fetchFailed: false };\n\t\t\t}\n\n\t\t\tconst { value: projects, warning } = await runCaptured(\n\t\t\t\tid,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t() => integration.getProjectsForOrgResult(org, connectionId),\n\t\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t\t);\n\t\t\tif (warning != null) {\n\t\t\t\twarnings.push(warning);\n\t\t\t\tif (projects == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (projects != null) {\n\t\t\t\titems.push(...projects.values.map(project => withProviderContext(id, project)));\n\n\t\t\t\tif (mergeHierarchyIncompleteness(warnings, id, domain, connectionId, projects, 'Project')) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed };\n\t\t}\n\n\t\tconst {\n\t\t\tvalue: resources,\n\t\t\twarning: resourcesWarning,\n\t\t\tmetadata: resourcesMetadata,\n\t\t} = await discoverResources(integration, id, domain, connectionId, {\n\t\t\twarnOnMissingSession: warnOnMissingSession,\n\t\t});\n\t\tif (resourcesWarning != null) {\n\t\t\twarnings.push(resourcesWarning);\n\t\t\tif (resources == null) {\n\t\t\t\tfetchFailed = true;\n\t\t\t}\n\t\t}\n\t\tif (mergeAssessmentInto(warnings, id, domain, connectionId, resourcesMetadata).truncated) {\n\t\t\tfetchFailed = true;\n\t\t}\n\n\t\t// By any identifier, unlike the issue reads' `selectorMatches`: this lists what the caller can then select, so a\n\t\t// match through a name shows the user those projects rather than silently reading another backlog.\n\t\tconst scopedResources =\n\t\t\torg != null ? resources?.filter(resource => resourceMatchesOrg(resource, org)) : resources;\n\t\tif (scopedResources != null && scopedResources.length !== 0) {\n\t\t\tconst { value: projects, warning: projectsWarning } = await runCaptured(id, domain, connectionId, () =>\n\t\t\t\tintegration.getProjectsForResourcesWithMetadataResult(scopedResources, connectionId),\n\t\t\t);\n\t\t\tif (projectsWarning != null) {\n\t\t\t\twarnings.push(projectsWarning);\n\t\t\t\tif (projects == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (projects != null) {\n\t\t\t\titems.push(...projects.values.map(project => projectToOrg(id, project, scopedResources)));\n\t\t\t}\n\t\t\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, projects?.metadata);\n\t\t\tif (assessment.fetchFailed || assessment.truncated) {\n\t\t\t\tfetchFailed = true;\n\t\t\t}\n\t\t}\n\n\t\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed };\n\t});\n\n\treturn mergeHierarchyResults(results);\n}\n\nexport async function listRepos(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\torg?: string;\n\t\tproject?: string;\n\t\tpage?: number;\n\t\tcursor?: string;\n\t\titemsPerPage?: number;\n\t\tconnectionId?: string;\n\t\tdomain?: string;\n\t},\n): Promise<ProviderPagedResult<ProviderRepositoryShape>> {\n\t// Truncated as well as floored, like every other paged read here: `page` is reported back as\n\t// `page.currentPage` and bounds the cursor walk below, and neither can be fractional.\n\tconst page = Math.max(1, Math.trunc(options.page ?? 1));\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'repository discovery')],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\t// A supplied connection or domain that no longer resolves is a broken target, not an empty account —\n\t\t// surface a no-connection warning + fetchFailed rather than a silent empty page.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\treturn refusedPage(page, early.warnings, early.fetchFailed);\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'repository discovery')],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\n\tconst accountWide = options.org == null;\n\tconst supported = accountWide\n\t\t? integration.supportsUserRepositoryDiscovery\n\t\t: integration.supportsRepositoryDiscovery;\n\tif (!supported) {\n\t\t// No matching repo-discovery hook — org-scoped (e.g. Bitbucket Data Center) or account-wide (e.g.\n\t\t// Bitbucket/Azure, whose repos can only be walked per workspace/org). Report unsupported rather than\n\t\t// a silent empty page indistinguishable from \"no repos\"; for the account-wide case the caller should\n\t\t// fan out per org from listOrgs instead.\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[\n\t\t\t\tunsupportedWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\taccountWide\n\t\t\t\t\t\t? `Account-wide repository discovery is not supported by '${options.providerId}'; list repositories per org instead.`\n\t\t\t\t\t\t: `Repository discovery is not supported by '${options.providerId}'.`,\n\t\t\t\t),\n\t\t\t],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst org = options.org;\n\tconst warnOnMissingSession = warnOnMissingSessionForDomain(options.providerId, options.domain);\n\tconst readPage = (cursor: string | undefined) =>\n\t\trunCaptured(\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\t() =>\n\t\t\t\torg == null\n\t\t\t\t\t? integration.getRepositoriesForUserResult({ cursor: cursor, connectionId: options.connectionId })\n\t\t\t\t\t: integration.getRepositoriesForOrgResult(org, {\n\t\t\t\t\t\t\tproject: options.project,\n\t\t\t\t\t\t\tcursor: cursor,\n\t\t\t\t\t\t\tconnectionId: options.connectionId,\n\t\t\t\t\t\t}),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t);\n\n\t/**\n\t * Builds one page's result from what the provider returned. Shared by the initial read and by every round of\n\t * the cursor walk below, so a walked page N is described by exactly the same rules as a directly-requested one\n\t * (position, continuation, truncation) rather than by a second copy of them.\n\t */\n\tconst toResult = (\n\t\t{ value, warning }: Awaited<ReturnType<typeof readPage>>,\n\t\trequestedPage: number,\n\t\tsuppliedCursor: string | undefined,\n\t): ProviderPagedResult<ProviderRepositoryShape> => {\n\t\tconst items = value?.values ?? [];\n\t\tconst warnings = warning != null ? [warning] : [];\n\t\t// The repos read core is cursor-only and can't accept a page size, so don't echo the requested\n\t\t// `itemsPerPage` as if it were applied — report what the provider returned (its own pageSize when\n\t\t// available, else the actual item count).\n\t\tconst paged = toProviderPageInfo(items.length, value?.paging);\n\t\t// Convert the SDK collection metadata into scope-aware warnings + failure/truncation flags, appending\n\t\t// them to any captured thrown-error warning without discarding the partial result's items.\n\t\tconst assessment = mergeAssessmentInto(\n\t\t\twarnings,\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\tvalue?.metadata,\n\t\t);\n\t\t// Whether this read honors a page NUMBER, decided from what the provider actually reported rather than\n\t\t// guessed from the absence of a cursor. A numbered-page repos host reports its position (`paging.page`)\n\t\t// and/or its successor (`paging.nextPage`) — Bitbucket's workspace walk consumes `page` as a real 1-based\n\t\t// page and reports `nextPage` without echoing `currentPage`, so both signals have to count. Every wired\n\t\t// cursor-based repos read (GitHub's org/user walks, GitLab's user walk, Bitbucket's cursor read) reports\n\t\t// neither. Keying off `paged.cursor == null` instead read the paging layer's \"reported another page,\n\t\t// handed back no continuation\" sentinel as evidence of a numbered host, which is exactly backwards.\n\t\tconst pageAdvanceable = value?.paging?.page != null || value?.paging?.nextPage != null;\n\t\t// Numbered-page hosts that don't echo `currentPage` may still be advanced by the requested `page` (initial\n\t\t// read) or by the cursor the caller threaded back. A cursor-only host ignores an UNTHREADED `page`\n\t\t// request, so its page-less first page is page 1.\n\t\tconst currentPage = resolveCurrentPage({\n\t\t\tproviderPage: paged.page.currentPage,\n\t\t\trequestedPage: requestedPage,\n\t\t\tsuppliedCursor: suppliedCursor,\n\t\t\tpageAdvanceable: pageAdvanceable,\n\t\t});\n\t\t// Never advertise `hasMore` without a continuation the caller can act on — the same contract\n\t\t// `listPullRequestsPage`, `listIssuesPage` and `broadenIssues` hold, via the same helper. A provider that\n\t\t// reports another page but hands back neither `endCursor` nor `nextPage` (surfaced as the `'{}'` sentinel,\n\t\t// which `toProviderPageInfo` drops) is terminal-but-incomplete: `hasMore: false` + `page.truncated`.\n\t\t// Synthesizing a page-number cursor for a cursor-only host instead — which is what this read used to do\n\t\t// unconditionally — handed back a continuation the provider ignores, so it answered with its FIRST page\n\t\t// again while still reporting `hasMore: true`. A consumer draining until `hasMore` clears (Kepler's repo\n\t\t// drain does exactly that) then looped to its own page cap, accumulating a duplicate copy of every repo\n\t\t// per round, and never saw a truncation signal.\n\t\tconst continuation = resolveContinuation(paged, pageAdvanceable ? currentPage + 1 : undefined);\n\t\t// The org-hierarchy read can also stop at a defensive backstop with more repos unlisted and no cursor to\n\t\t// resume (top-level `truncated`, or `paging.truncated` on a single-page read). Metadata incompleteness and\n\t\t// a demoted continuation are two further, independent sources of the same signal.\n\t\tconst truncated =\n\t\t\t(value?.truncated ?? false) ||\n\t\t\t(value?.paging?.truncated ?? false) ||\n\t\t\tassessment.truncated ||\n\t\t\tcontinuation.truncated;\n\t\treturn {\n\t\t\t// Normalize the raw provider-apis repos to the GitLens-owned shape at the surface boundary.\n\t\t\titems: items.map(toProviderRepositoryShape),\n\t\t\twarnings: warnings,\n\t\t\tpage: { ...paged.page, currentPage: currentPage, truncated: truncated || undefined },\n\t\t\thasMore: continuation.hasMore,\n\t\t\tcursor: continuation.cursor,\n\t\t\tfetchFailed: assessment.fetchFailed || (warning != null && value == null) || undefined,\n\t\t};\n\t};\n\n\tconst first = await readPage(\n\t\toptions.cursor ??\n\t\t\t(integration.id === GitSelfManagedHostIntegrationId.BitbucketServer ? undefined : pageToCursor(page)),\n\t);\n\tconst result = toResult(first, page, options.cursor);\n\t// A numbered-page host honored the synthesized cursor, so this IS page N; a cursor-only host ignored it and\n\t// answered with page 1, which the walk below advances. Decided from the provider's own paging metadata rather\n\t// than assumed, hence after the read.\n\tconst pageAdvanceable = first.value?.paging?.page != null || first.value?.paging?.nextPage != null;\n\tif (options.cursor != null || page === 1 || pageAdvanceable) return result;\n\n\t// Walk the provider's real continuations to the requested page. Only the last page's items are kept — pages\n\t// 1..N returned as \"page N\" would duplicate items for a normal paged consumer — while warnings, truncation,\n\t// and failure accumulate across the whole walked prefix, since an earlier round's problem still means this\n\t// result isn't authoritative. The provider read is re-issued per round, but the integration is resolved once:\n\t// re-entering this function per page would repeat its auth resolution N times for page N.\n\tconst walkedWarnings = [...result.warnings];\n\tlet walkedFetchFailed = result.fetchFailed === true;\n\tlet walkedTruncated = result.page.truncated === true;\n\tlet previous = result;\n\tfor (let currentPage = 2; currentPage <= page; currentPage++) {\n\t\tif (!previous.hasMore || previous.cursor == null) {\n\t\t\t// The requested page lies past the provider's terminal cursor: an EMPTY page N, never the last\n\t\t\t// available page relabeled (see ProviderPageInfo.currentPage).\n\t\t\treturn {\n\t\t\t\titems: [],\n\t\t\t\twarnings: walkedWarnings,\n\t\t\t\tpage: { currentPage: page, itemsPerPage: 0, truncated: walkedTruncated || undefined },\n\t\t\t\thasMore: false,\n\t\t\t\tfetchFailed: walkedFetchFailed || undefined,\n\t\t\t};\n\t\t}\n\n\t\tconst suppliedCursor = previous.cursor;\n\t\tconst requested = toResult(await readPage(suppliedCursor), currentPage, suppliedCursor);\n\t\tfor (const warning of requested.warnings) {\n\t\t\tappendDedupedWarning(walkedWarnings, warning);\n\t\t}\n\t\twalkedFetchFailed ||= requested.fetchFailed === true;\n\t\twalkedTruncated ||= requested.page.truncated === true;\n\t\tif (currentPage === page) {\n\t\t\treturn {\n\t\t\t\t...requested,\n\t\t\t\twarnings: walkedWarnings,\n\t\t\t\tpage: { ...requested.page, truncated: walkedTruncated || undefined },\n\t\t\t\tfetchFailed: walkedFetchFailed || undefined,\n\t\t\t};\n\t\t}\n\n\t\tprevious = requested;\n\t}\n\n\treturn result;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"hierarchy.js","names":[],"sources":["../../../../../plus/integrations/src/reads/hierarchy.ts"],"sourcesContent":["import { mapBounded } from '@gitlens/utils/promise.js';\nimport { mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport {\n\tGitCloudHostIntegrationId,\n\tGitSelfManagedHostIntegrationId,\n\tIssuesCloudHostIntegrationId,\n\tproviderFanOutConcurrency,\n\tsupportedOrderedCloudIntegrationIds,\n\tsupportedOrderedCloudIssuesIntegrationIds,\n} from '../constants.js';\nimport type { ListLinearProjectsOptions, ListOrgsOptions, ListProjectsOptions } from '../manager.js';\nimport { isIssuesIntegration } from '../models/issuesIntegration.js';\nimport type { ProviderHierarchyResult } from '../providers/models.js';\nimport { toProviderRepositoryShape } from '../providers/models.js';\nimport type {\n\tProviderLinearProjectShape,\n\tProviderOrganization,\n\tProviderPagedResult,\n\tProviderRepositoryShape,\n\tProviderResult,\n\tProviderWarning,\n} from '../results.js';\nimport { appendDedupedWarning } from '../results.js';\nimport {\n\tisGitHostIntegration,\n\tisIssuesHostIntegrationId,\n\tisLinearIntegration,\n\twarnOnMissingSessionForDomain,\n} from '../utils/integration.utils.js';\nimport type { ProviderReadContext } from './context.js';\nimport { discoverResources, runCaptured } from './drains.js';\nimport {\n\tassertHierarchyReadTarget,\n\tmergeHierarchyResults,\n\tprojectToOrg,\n\tresourceMatchesOrg,\n\tresourceToOrg,\n\twithProviderContext,\n} from './hierarchy.utils.js';\nimport { pageToCursor, refusedPage, resolveContinuation, resolveCurrentPage, toProviderPageInfo } from './paging.js';\nimport { gitHostOnlySurfaceWarning, truncationWarning, unsupportedWarning } from './warnings.js';\n\n/**\n * Folds a flat hierarchy result's incompleteness into `warnings`, returning whether it leaves the read\n * non-authoritative. The single funnel for both hierarchy reads, so neither can drift from the other on any of\n * the four decisions it makes:\n *\n * - The metadata is assessed FIRST, so the truncation warning can tell whether it already explained this gap.\n * Ordered deliberately: emitting first meant an expired credential produced BOTH a typed `auth` warning and\n * an unclassifiable one for the same cause, and a consumer routing on classification showed the second as\n * \"this provider failed to load\", with a remedy that doesn't exist, next to the reconnect prompt that was the\n * actual fix.\n * - The read states the truncation itself only when the metadata reported nothing — the rule\n * `assessCollectionMetadata` applies to its own generic fallback. Whatever the metadata reported already names\n * this provider and says something stronger, so restating it costs a false verdict and adds no information.\n * - `'exhausted'`, never `'page-budget'`: `truncated` is one boolean here, so a drain that stopped at its page\n * budget is indistinguishable from one whose cursor stalled, and `IncompleteReadCause` makes `'exhausted'`\n * the default whenever a raisable budget is not demonstrably the cause.\n * - A truncation counts as non-authoritative even though the omission above asserts the read succeeded. Unlike\n * paged repository reads, a flattened hierarchy result has no page object on which to carry incompleteness,\n * so `fetchFailed` is the only signal a consumer has that the list is short.\n */\nfunction mergeHierarchyIncompleteness(\n\twarnings: ProviderWarning[],\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tresult: Pick<ProviderHierarchyResult<unknown>, 'truncated' | 'metadata'>,\n\treadKind: 'Organization' | 'Project',\n): boolean {\n\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, result.metadata);\n\tif (!result.truncated) return assessment.fetchFailed || assessment.truncated;\n\n\tif (!assessment.reported) {\n\t\t// Deduped like every other `truncationWarning` emission (see `drains.ts`), not plain-pushed: no caller can\n\t\t// collide with it today, but nothing about this funnel guarantees a third read won't.\n\t\tappendDedupedWarning(warnings, truncationWarning(id, domain, connectionId, readKind, 'exhausted'));\n\t}\n\treturn true;\n}\n\nexport async function listOrgs(\n\tctx: ProviderReadContext,\n\toptions?: ListOrgsOptions,\n): Promise<ProviderResult<ProviderOrganization>> {\n\tassertHierarchyReadTarget('listOrgs', options);\n\tconst ids = options?.providerId != null ? [options.providerId] : supportedOrderedCloudIntegrationIds;\n\tconst singleProvider = ids.length === 1;\n\tconst connectionId = singleProvider ? options?.connectionId : undefined;\n\tconst requestedDomain = singleProvider ? options?.domain : undefined;\n\n\tconst results = await mapBounded(ids, providerFanOutConcurrency, async id => {\n\t\tconst integration = await ctx.getIntegrationForRead(id, connectionId, requestedDomain);\n\t\tif (integration == null) {\n\t\t\t// A specifically requested connection that can't be resolved is a broken connection, not a\n\t\t\t// provider with no orgs — surface it (warning + fetchFailed) instead of dropping the id\n\t\t\t// silently, so a caller can tell it apart from an account that genuinely has no orgs.\n\t\t\tconst early = ctx.earlyReturnConnectionWarnings(id, connectionId, requestedDomain);\n\t\t\treturn {\n\t\t\t\titems: [] as ProviderOrganization[],\n\t\t\t\twarnings: early.warnings,\n\t\t\t\tfetchFailed: early.fetchFailed,\n\t\t\t};\n\t\t}\n\n\t\tconst items: ProviderOrganization[] = [];\n\t\tconst warnings: ProviderWarning[] = [];\n\t\tlet fetchFailed = false;\n\t\tconst domain = ctx.domainForRead(integration, id, connectionId, requestedDomain);\n\t\tconst warnOnMissingSession = warnOnMissingSessionForDomain(id, requestedDomain);\n\t\tif (isIssuesIntegration(integration)) {\n\t\t\t// Issue trackers expose \"resources\" (Jira sites, Linear orgs, …) as their org analogue.\n\t\t\tconst {\n\t\t\t\tvalue: resources,\n\t\t\t\twarning,\n\t\t\t\tmetadata,\n\t\t\t} = await discoverResources(integration, id, domain, connectionId, {\n\t\t\t\twarnOnMissingSession: warnOnMissingSession,\n\t\t\t});\n\t\t\tif (resources != null) {\n\t\t\t\titems.push(...resources.map(r => resourceToOrg(id, r)));\n\t\t\t}\n\t\t\tif (warning != null) {\n\t\t\t\twarnings.push(warning);\n\t\t\t\t// A warning with no value is a hard read failure, not an empty account.\n\t\t\t\tif (resources == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (mergeAssessmentInto(warnings, id, domain, connectionId, metadata).truncated) {\n\t\t\t\tfetchFailed = true;\n\t\t\t}\n\t\t} else if (!integration.supportsOrganizationDiscovery) {\n\t\t\t// The provider registers no org-discovery hook. Report it as\n\t\t\t// explicitly unsupported rather than contributing a silent empty list that a caller can't\n\t\t\t// tell apart from \"this account has no orgs\".\n\t\t\tfetchFailed = true;\n\t\t\twarnings.push(\n\t\t\t\tunsupportedWarning(id, domain, connectionId, `Organization discovery is not supported by '${id}'.`),\n\t\t\t);\n\t\t} else {\n\t\t\tconst { value, warning } = await runCaptured(\n\t\t\t\tid,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t() => integration.getOrganizationsForUserResult(connectionId),\n\t\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t\t);\n\t\t\tif (value != null) {\n\t\t\t\titems.push(...value.values.map(org => withProviderContext(id, org)));\n\n\t\t\t\tif (mergeHierarchyIncompleteness(warnings, id, domain, connectionId, value, 'Organization')) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (warning != null) {\n\t\t\t\twarnings.push(warning);\n\t\t\t\tif (value == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed };\n\t});\n\n\treturn mergeHierarchyResults(results);\n}\n\nexport async function listProjects(\n\tctx: ProviderReadContext,\n\toptions?: ListProjectsOptions,\n): Promise<ProviderResult<ProviderOrganization>> {\n\tassertHierarchyReadTarget('listProjects', options);\n\tconst ids =\n\t\toptions?.providerId != null\n\t\t\t? [options.providerId]\n\t\t\t: [\n\t\t\t\t\t...supportedOrderedCloudIssuesIntegrationIds,\n\t\t\t\t\tGitCloudHostIntegrationId.AzureDevOps,\n\t\t\t\t\tGitSelfManagedHostIntegrationId.AzureDevOpsServer,\n\t\t\t\t];\n\tconst singleProvider = ids.length === 1;\n\tconst connectionId = singleProvider ? options?.connectionId : undefined;\n\tconst requestedDomain = singleProvider ? options?.domain : undefined;\n\n\tconst results = await mapBounded(ids, providerFanOutConcurrency, async id => {\n\t\tconst integration = await ctx.getIntegrationForRead(id, connectionId, requestedDomain);\n\t\tif (integration == null) {\n\t\t\t// A requested connection that can't be resolved is a broken connection, not a provider with\n\t\t\t// no projects — surface it (warning + fetchFailed) instead of dropping the id silently.\n\t\t\tconst early = ctx.earlyReturnConnectionWarnings(id, connectionId, requestedDomain);\n\t\t\treturn {\n\t\t\t\titems: [] as ProviderOrganization[],\n\t\t\t\twarnings: early.warnings,\n\t\t\t\tfetchFailed: early.fetchFailed,\n\t\t\t};\n\t\t}\n\n\t\tconst items: ProviderOrganization[] = [];\n\t\tconst warnings: ProviderWarning[] = [];\n\t\tlet fetchFailed = false;\n\t\tconst domain = ctx.domainForRead(integration, id, connectionId, requestedDomain);\n\t\tconst org = options?.org;\n\t\tconst warnOnMissingSession = warnOnMissingSessionForDomain(id, requestedDomain);\n\n\t\t// Git hosts with a project tier (Azure DevOps) read projects through their own hierarchy hook,\n\t\t// scoped to `org` when given. Non-Azure git hosts have no project tier and return undefined.\n\t\tif (!isIssuesIntegration(integration)) {\n\t\t\t// Check the capability first, like listOrgs/listRepos/listIssuesPage do. Without it, a host\n\t\t\t// with no project tier returns `undefined` from a successful read, which `runCaptured` can't\n\t\t\t// tell apart from an unresolvable session — turning a healthy provider into a `no-connection`\n\t\t\t// warning + `fetchFailed` (and, in Kepler, a spurious reconnect prompt).\n\t\t\tif (!integration.supportsProjectDiscovery) {\n\t\t\t\treturn { items: items, warnings: warnings, fetchFailed: false };\n\t\t\t}\n\n\t\t\tconst { value: projects, warning } = await runCaptured(\n\t\t\t\tid,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t() => integration.getProjectsForOrgResult(org, connectionId),\n\t\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t\t);\n\t\t\tif (warning != null) {\n\t\t\t\twarnings.push(warning);\n\t\t\t\tif (projects == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (projects != null) {\n\t\t\t\titems.push(...projects.values.map(project => withProviderContext(id, project)));\n\n\t\t\t\tif (mergeHierarchyIncompleteness(warnings, id, domain, connectionId, projects, 'Project')) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed };\n\t\t}\n\n\t\tconst {\n\t\t\tvalue: resources,\n\t\t\twarning: resourcesWarning,\n\t\t\tmetadata: resourcesMetadata,\n\t\t} = await discoverResources(integration, id, domain, connectionId, {\n\t\t\twarnOnMissingSession: warnOnMissingSession,\n\t\t});\n\t\tif (resourcesWarning != null) {\n\t\t\twarnings.push(resourcesWarning);\n\t\t\tif (resources == null) {\n\t\t\t\tfetchFailed = true;\n\t\t\t}\n\t\t}\n\t\tif (mergeAssessmentInto(warnings, id, domain, connectionId, resourcesMetadata).truncated) {\n\t\t\tfetchFailed = true;\n\t\t}\n\n\t\t// By any identifier, unlike the issue reads' `selectorMatches`: this lists what the caller can then select, so a\n\t\t// match through a name shows the user those projects rather than silently reading another backlog.\n\t\tconst scopedResources =\n\t\t\torg != null ? resources?.filter(resource => resourceMatchesOrg(resource, org)) : resources;\n\t\tif (scopedResources != null && scopedResources.length !== 0) {\n\t\t\tconst { value: projects, warning: projectsWarning } = await runCaptured(id, domain, connectionId, () =>\n\t\t\t\tintegration.getProjectsForResourcesWithMetadataResult(scopedResources, connectionId),\n\t\t\t);\n\t\t\tif (projectsWarning != null) {\n\t\t\t\twarnings.push(projectsWarning);\n\t\t\t\tif (projects == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (projects != null) {\n\t\t\t\titems.push(...projects.values.map(project => projectToOrg(id, project, scopedResources)));\n\t\t\t}\n\t\t\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, projects?.metadata);\n\t\t\tif (assessment.fetchFailed || assessment.truncated) {\n\t\t\t\tfetchFailed = true;\n\t\t\t}\n\t\t}\n\n\t\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed };\n\t});\n\n\treturn mergeHierarchyResults(results);\n}\n\n/**\n * The Linear workspace's Projects (Linear's own `Project` entity), apart from the teams {@link listProjects} lists as\n * Linear's projects. Reads the same connection a Linear issue read would, through the integration's cached, drained\n * read, and reports a drain it could not finish as `fetchFailed` with a warning, as {@link listProjects} does.\n */\nexport async function listLinearProjects(\n\tctx: ProviderReadContext,\n\toptions?: ListLinearProjectsOptions,\n): Promise<ProviderResult<ProviderLinearProjectShape>> {\n\tconst id = IssuesCloudHostIntegrationId.Linear;\n\tconst connectionId = options?.connectionId;\n\tconst integration = await ctx.getIntegrationForRead(id, connectionId);\n\tif (integration == null) {\n\t\t// A requested connection that can't be resolved is a broken connection, not a workspace with no Projects.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(id, connectionId);\n\t\treturn { items: [], warnings: early.warnings, fetchFailed: early.fetchFailed || undefined };\n\t}\n\n\tif (!isLinearIntegration(integration)) return { items: [], warnings: [] };\n\n\tconst domain = ctx.domainForRead(integration, id, connectionId);\n\tawait ctx.forceRefreshIfRequested(integration, options?.forceSync, connectionId);\n\n\tconst warnings: ProviderWarning[] = [];\n\tconst { value: projects, warning } = await runCaptured(id, domain, connectionId, () =>\n\t\tintegration.getWorkspaceProjectsResult(connectionId),\n\t);\n\t// A warning with no value is a failed read, not a workspace with no Projects.\n\tlet fetchFailed = warning != null && projects == null;\n\tif (warning != null) {\n\t\twarnings.push(warning);\n\t}\n\tif (projects != null && mergeHierarchyIncompleteness(warnings, id, domain, connectionId, projects, 'Project')) {\n\t\tfetchFailed = true;\n\t}\n\n\treturn {\n\t\titems: (projects?.values ?? []).map(project => ({\n\t\t\tid: project.id,\n\t\t\tproviderId: id,\n\t\t\tname: project.name,\n\t\t\tstate: project.state,\n\t\t\turl: project.url,\n\t\t})),\n\t\twarnings: warnings,\n\t\tfetchFailed: fetchFailed || undefined,\n\t};\n}\n\nexport async function listRepos(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\torg?: string;\n\t\tproject?: string;\n\t\tpage?: number;\n\t\tcursor?: string;\n\t\titemsPerPage?: number;\n\t\tconnectionId?: string;\n\t\tdomain?: string;\n\t},\n): Promise<ProviderPagedResult<ProviderRepositoryShape>> {\n\t// Truncated as well as floored, like every other paged read here: `page` is reported back as\n\t// `page.currentPage` and bounds the cursor walk below, and neither can be fractional.\n\tconst page = Math.max(1, Math.trunc(options.page ?? 1));\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'repository discovery')],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\t// A supplied connection or domain that no longer resolves is a broken target, not an empty account —\n\t\t// surface a no-connection warning + fetchFailed rather than a silent empty page.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\treturn refusedPage(page, early.warnings, early.fetchFailed);\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'repository discovery')],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\n\tconst accountWide = options.org == null;\n\tconst supported = accountWide\n\t\t? integration.supportsUserRepositoryDiscovery\n\t\t: integration.supportsRepositoryDiscovery;\n\tif (!supported) {\n\t\t// No matching repo-discovery hook — org-scoped (e.g. Bitbucket Data Center) or account-wide (e.g.\n\t\t// Bitbucket/Azure, whose repos can only be walked per workspace/org). Report unsupported rather than\n\t\t// a silent empty page indistinguishable from \"no repos\"; for the account-wide case the caller should\n\t\t// fan out per org from listOrgs instead.\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[\n\t\t\t\tunsupportedWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\taccountWide\n\t\t\t\t\t\t? `Account-wide repository discovery is not supported by '${options.providerId}'; list repositories per org instead.`\n\t\t\t\t\t\t: `Repository discovery is not supported by '${options.providerId}'.`,\n\t\t\t\t),\n\t\t\t],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst org = options.org;\n\tconst warnOnMissingSession = warnOnMissingSessionForDomain(options.providerId, options.domain);\n\tconst readPage = (cursor: string | undefined) =>\n\t\trunCaptured(\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\t() =>\n\t\t\t\torg == null\n\t\t\t\t\t? integration.getRepositoriesForUserResult({ cursor: cursor, connectionId: options.connectionId })\n\t\t\t\t\t: integration.getRepositoriesForOrgResult(org, {\n\t\t\t\t\t\t\tproject: options.project,\n\t\t\t\t\t\t\tcursor: cursor,\n\t\t\t\t\t\t\tconnectionId: options.connectionId,\n\t\t\t\t\t\t}),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t);\n\n\t/**\n\t * Builds one page's result from what the provider returned. Shared by the initial read and by every round of\n\t * the cursor walk below, so a walked page N is described by exactly the same rules as a directly-requested one\n\t * (position, continuation, truncation) rather than by a second copy of them.\n\t */\n\tconst toResult = (\n\t\t{ value, warning }: Awaited<ReturnType<typeof readPage>>,\n\t\trequestedPage: number,\n\t\tsuppliedCursor: string | undefined,\n\t): ProviderPagedResult<ProviderRepositoryShape> => {\n\t\tconst items = value?.values ?? [];\n\t\tconst warnings = warning != null ? [warning] : [];\n\t\t// The repos read core is cursor-only and can't accept a page size, so don't echo the requested\n\t\t// `itemsPerPage` as if it were applied — report what the provider returned (its own pageSize when\n\t\t// available, else the actual item count).\n\t\tconst paged = toProviderPageInfo(items.length, value?.paging);\n\t\t// Convert the SDK collection metadata into scope-aware warnings + failure/truncation flags, appending\n\t\t// them to any captured thrown-error warning without discarding the partial result's items.\n\t\tconst assessment = mergeAssessmentInto(\n\t\t\twarnings,\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\tvalue?.metadata,\n\t\t);\n\t\t// Whether this read honors a page NUMBER, decided from what the provider actually reported rather than\n\t\t// guessed from the absence of a cursor. A numbered-page repos host reports its position (`paging.page`)\n\t\t// and/or its successor (`paging.nextPage`) — Bitbucket's workspace walk consumes `page` as a real 1-based\n\t\t// page and reports `nextPage` without echoing `currentPage`, so both signals have to count. Every wired\n\t\t// cursor-based repos read (GitHub's org/user walks, GitLab's user walk, Bitbucket's cursor read) reports\n\t\t// neither. Keying off `paged.cursor == null` instead read the paging layer's \"reported another page,\n\t\t// handed back no continuation\" sentinel as evidence of a numbered host, which is exactly backwards.\n\t\tconst pageAdvanceable = value?.paging?.page != null || value?.paging?.nextPage != null;\n\t\t// Numbered-page hosts that don't echo `currentPage` may still be advanced by the requested `page` (initial\n\t\t// read) or by the cursor the caller threaded back. A cursor-only host ignores an UNTHREADED `page`\n\t\t// request, so its page-less first page is page 1.\n\t\tconst currentPage = resolveCurrentPage({\n\t\t\tproviderPage: paged.page.currentPage,\n\t\t\trequestedPage: requestedPage,\n\t\t\tsuppliedCursor: suppliedCursor,\n\t\t\tpageAdvanceable: pageAdvanceable,\n\t\t});\n\t\t// Never advertise `hasMore` without a continuation the caller can act on — the same contract\n\t\t// `listPullRequestsPage`, `listIssuesPage` and `broadenIssues` hold, via the same helper. A provider that\n\t\t// reports another page but hands back neither `endCursor` nor `nextPage` (surfaced as the `'{}'` sentinel,\n\t\t// which `toProviderPageInfo` drops) is terminal-but-incomplete: `hasMore: false` + `page.truncated`.\n\t\t// Synthesizing a page-number cursor for a cursor-only host instead — which is what this read used to do\n\t\t// unconditionally — handed back a continuation the provider ignores, so it answered with its FIRST page\n\t\t// again while still reporting `hasMore: true`. A consumer draining until `hasMore` clears (Kepler's repo\n\t\t// drain does exactly that) then looped to its own page cap, accumulating a duplicate copy of every repo\n\t\t// per round, and never saw a truncation signal.\n\t\tconst continuation = resolveContinuation(paged, pageAdvanceable ? currentPage + 1 : undefined);\n\t\t// The org-hierarchy read can also stop at a defensive backstop with more repos unlisted and no cursor to\n\t\t// resume (top-level `truncated`, or `paging.truncated` on a single-page read). Metadata incompleteness and\n\t\t// a demoted continuation are two further, independent sources of the same signal.\n\t\tconst truncated =\n\t\t\t(value?.truncated ?? false) ||\n\t\t\t(value?.paging?.truncated ?? false) ||\n\t\t\tassessment.truncated ||\n\t\t\tcontinuation.truncated;\n\t\treturn {\n\t\t\t// Normalize the raw provider-apis repos to the GitLens-owned shape at the surface boundary.\n\t\t\titems: items.map(toProviderRepositoryShape),\n\t\t\twarnings: warnings,\n\t\t\tpage: { ...paged.page, currentPage: currentPage, truncated: truncated || undefined },\n\t\t\thasMore: continuation.hasMore,\n\t\t\tcursor: continuation.cursor,\n\t\t\tfetchFailed: assessment.fetchFailed || (warning != null && value == null) || undefined,\n\t\t};\n\t};\n\n\tconst first = await readPage(\n\t\toptions.cursor ??\n\t\t\t(integration.id === GitSelfManagedHostIntegrationId.BitbucketServer ? undefined : pageToCursor(page)),\n\t);\n\tconst result = toResult(first, page, options.cursor);\n\t// A numbered-page host honored the synthesized cursor, so this IS page N; a cursor-only host ignored it and\n\t// answered with page 1, which the walk below advances. Decided from the provider's own paging metadata rather\n\t// than assumed, hence after the read.\n\tconst pageAdvanceable = first.value?.paging?.page != null || first.value?.paging?.nextPage != null;\n\tif (options.cursor != null || page === 1 || pageAdvanceable) return result;\n\n\t// Walk the provider's real continuations to the requested page. Only the last page's items are kept — pages\n\t// 1..N returned as \"page N\" would duplicate items for a normal paged consumer — while warnings, truncation,\n\t// and failure accumulate across the whole walked prefix, since an earlier round's problem still means this\n\t// result isn't authoritative. The provider read is re-issued per round, but the integration is resolved once:\n\t// re-entering this function per page would repeat its auth resolution N times for page N.\n\tconst walkedWarnings = [...result.warnings];\n\tlet walkedFetchFailed = result.fetchFailed === true;\n\tlet walkedTruncated = result.page.truncated === true;\n\tlet previous = result;\n\tfor (let currentPage = 2; currentPage <= page; currentPage++) {\n\t\tif (!previous.hasMore || previous.cursor == null) {\n\t\t\t// The requested page lies past the provider's terminal cursor: an EMPTY page N, never the last\n\t\t\t// available page relabeled (see ProviderPageInfo.currentPage).\n\t\t\treturn {\n\t\t\t\titems: [],\n\t\t\t\twarnings: walkedWarnings,\n\t\t\t\tpage: { currentPage: page, itemsPerPage: 0, truncated: walkedTruncated || undefined },\n\t\t\t\thasMore: false,\n\t\t\t\tfetchFailed: walkedFetchFailed || undefined,\n\t\t\t};\n\t\t}\n\n\t\tconst suppliedCursor = previous.cursor;\n\t\tconst requested = toResult(await readPage(suppliedCursor), currentPage, suppliedCursor);\n\t\tfor (const warning of requested.warnings) {\n\t\t\tappendDedupedWarning(walkedWarnings, warning);\n\t\t}\n\t\twalkedFetchFailed ||= requested.fetchFailed === true;\n\t\twalkedTruncated ||= requested.page.truncated === true;\n\t\tif (currentPage === page) {\n\t\t\treturn {\n\t\t\t\t...requested,\n\t\t\t\twarnings: walkedWarnings,\n\t\t\t\tpage: { ...requested.page, truncated: walkedTruncated || undefined },\n\t\t\t\tfetchFailed: walkedFetchFailed || undefined,\n\t\t\t};\n\t\t}\n\n\t\tprevious = requested;\n\t}\n\n\treturn result;\n}\n"],"mappings":"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"}
|
|
@@ -37,6 +37,16 @@ export declare function listIssueTrackerIssuesPage(ctx: ProviderReadContext, opt
|
|
|
37
37
|
* another for `'closed'`/`'all'` refuses the read rather than serving its open issues.
|
|
38
38
|
*/
|
|
39
39
|
state?: IssueStateFilter;
|
|
40
|
+
/**
|
|
41
|
+
* Linear only: narrows the read to issues in any of these Linear Projects (Linear's `Project` entity, by id).
|
|
42
|
+
*
|
|
43
|
+
* A narrowing of each team's read, not a scope of its own: the read is still windowed and selected by team
|
|
44
|
+
* (`org`/`project`), and each team reports the issues in these Projects, server-side, so they don't count
|
|
45
|
+
* against its page backstop. A Linear Project spans teams, so a read of every team is what reads a Project
|
|
46
|
+
* whole. Refused by every other tracker rather than ignored, since ignoring it would publish the whole project.
|
|
47
|
+
* Recorded in a composite cursor and checked on resume, as `state` is.
|
|
48
|
+
*/
|
|
49
|
+
linearProjects?: string[];
|
|
40
50
|
forceSync?: boolean;
|
|
41
51
|
page?: number;
|
|
42
52
|
cursor?: string;
|
|
@@ -4,11 +4,11 @@ import "../constants.js";
|
|
|
4
4
|
import { appendDedupedWarning, reconcileOmissionsWithFailure } from "../results.js";
|
|
5
5
|
import { assessCollectionMetadata, mergeAssessmentInto } from "../collectionMetadata.js";
|
|
6
6
|
import "../providerFilters.js";
|
|
7
|
-
import { isIssuesHostIntegrationId } from "../utils/integration.utils.js";
|
|
7
|
+
import { isIssuesHostIntegrationId, isLinearIntegration } from "../utils/integration.utils.js";
|
|
8
8
|
import { mergeCollectionMetadata, parsePageCursor } from "../providers/utils/providerPaging.js";
|
|
9
9
|
import { providersMetadata } from "../providers/models.js";
|
|
10
10
|
import { isIssuesIntegration } from "../models/issuesIntegration.js";
|
|
11
|
-
import { parseIssueTrackerPageCursor, toIssueTrackerPageCursor } from "./cursors.js";
|
|
11
|
+
import { linearProjectIds, parseIssueTrackerPageCursor, toIssueTrackerPageCursor } from "./cursors.js";
|
|
12
12
|
import { incompleteReadWarning, issueTrackerOnlySurfaceWarning, otherWarning, projectCutWarning, unmatchedSelectorWarning, unmergeableIssueSortWarning, unsupportedIssueSortWarning, unsupportedWarning } from "./warnings.js";
|
|
13
13
|
import { discoverResources, runCaptured } from "./drains.js";
|
|
14
14
|
import { projectKey, resourceIdForProject, resourceLabel, selectorMatches } from "./hierarchy.utils.js";
|
|
@@ -32,6 +32,7 @@ async function readIssueTrackerIssuesPage(ctx, options, refreshed) {
|
|
|
32
32
|
const paginated = compositeCursor?.unpaged !== true && (options.page != null || options.cursor != null || options.itemsPerPage != null);
|
|
33
33
|
const page = Math.max(1, Math.trunc(compositeCursor?.currentPage ?? parsePageCursor(options.cursor) ?? options.page ?? 1));
|
|
34
34
|
const projectsPerPage = Math.max(1, Math.trunc(options.itemsPerPage ?? 20));
|
|
35
|
+
const linearProjects = linearProjectIds(options.linearProjects);
|
|
35
36
|
const items = [];
|
|
36
37
|
const warnings = [];
|
|
37
38
|
const emptyPage = (fetchFailed, truncated, retry) => {
|
|
@@ -42,7 +43,8 @@ async function readIssueTrackerIssuesPage(ctx, options, refreshed) {
|
|
|
42
43
|
retryPages: retry.pages ?? [],
|
|
43
44
|
retryProjects: retry.projects ?? [],
|
|
44
45
|
completedProjects: retry.completedProjects ?? [],
|
|
45
|
-
state: options.state
|
|
46
|
+
state: options.state,
|
|
47
|
+
linearProjects
|
|
46
48
|
}) : void 0;
|
|
47
49
|
return {
|
|
48
50
|
items,
|
|
@@ -98,7 +100,28 @@ async function readIssueTrackerIssuesPage(ctx, options, refreshed) {
|
|
|
98
100
|
warnings.push(otherWarning(options.providerId, domain, options.connectionId, "The cursor belongs to a read of another issue state; start again without it."));
|
|
99
101
|
return emptyPage(true);
|
|
100
102
|
}
|
|
103
|
+
if (options.linearProjects != null) {
|
|
104
|
+
if (!Array.isArray(options.linearProjects) || options.linearProjects.some((id) => typeof id !== "string" || id.length === 0)) {
|
|
105
|
+
warnings.push(otherWarning(options.providerId, domain, options.connectionId, "Linear Projects must be a list of non-empty ids."));
|
|
106
|
+
return emptyPage(true);
|
|
107
|
+
}
|
|
108
|
+
if (options.linearProjects.length > 0 && options.providerId !== "linear") {
|
|
109
|
+
warnings.push(unsupportedWarning(options.providerId, domain, options.connectionId, `Narrowing by Linear Project is not supported by '${options.providerId}'.`));
|
|
110
|
+
return emptyPage(true);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (options.cursor != null && (compositeCursor?.linearProjects ?? []).join("\0") !== (linearProjects ?? []).join("\0")) {
|
|
114
|
+
warnings.push(otherWarning(options.providerId, domain, options.connectionId, "The cursor belongs to a read of other Linear Projects; start again without it."));
|
|
115
|
+
return emptyPage(true);
|
|
116
|
+
}
|
|
101
117
|
await ctx.forceRefreshIfRequested(integration, options.forceSync, options.connectionId);
|
|
118
|
+
if (linearProjects != null && isLinearIntegration(integration)) {
|
|
119
|
+
const missing = await findUnlistedLinearProject(integration, linearProjects, options.connectionId);
|
|
120
|
+
if (missing != null) warnings.push(unmatchedSelectorWarning(options.providerId, domain, options.connectionId, {
|
|
121
|
+
kind: "linearProject",
|
|
122
|
+
value: missing
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
102
125
|
await integration.getCurrentAccount({ connectionId: options.connectionId }).catch(() => void 0);
|
|
103
126
|
const { value: resources, warning: resourcesWarning, metadata: resourcesMetadata } = await discoverResources(integration, options.providerId, domain, options.connectionId);
|
|
104
127
|
if (resourcesWarning != null) warnings.push(resourcesWarning);
|
|
@@ -282,7 +305,8 @@ async function readIssueTrackerIssuesPage(ctx, options, refreshed) {
|
|
|
282
305
|
userId: userIdForProject(project),
|
|
283
306
|
filters: options.filters,
|
|
284
307
|
sort,
|
|
285
|
-
state: options.state
|
|
308
|
+
state: options.state,
|
|
309
|
+
...linearProjects != null ? { linearProjects } : {}
|
|
286
310
|
}
|
|
287
311
|
})), options.connectionId);
|
|
288
312
|
} catch (ex) {
|
|
@@ -348,7 +372,8 @@ async function readIssueTrackerIssuesPage(ctx, options, refreshed) {
|
|
|
348
372
|
retryPages,
|
|
349
373
|
retryProjects: [...retryProjectKeys],
|
|
350
374
|
completedProjects: (trackCompletedProjects || projectDiscoveryFailed || projectDiscoveryTruncated || retryProjectKeys.size > 0) && (retryPages.length > 0 || nextPage != null) ? [...completedProjectKeys] : [],
|
|
351
|
-
state: options.state
|
|
375
|
+
state: options.state,
|
|
376
|
+
linearProjects
|
|
352
377
|
});
|
|
353
378
|
return {
|
|
354
379
|
items: orderedItems,
|
|
@@ -365,6 +390,22 @@ async function readIssueTrackerIssuesPage(ctx, options, refreshed) {
|
|
|
365
390
|
unexpectedGap: nextPage != null && cursor == null
|
|
366
391
|
};
|
|
367
392
|
}
|
|
393
|
+
/**
|
|
394
|
+
* The first of `ids` the workspace's Projects don't list, confirmed against a list read past the cache, since a cached
|
|
395
|
+
* one can predate a Project created since. Undefined when every id is listed, or when no complete list can prove one
|
|
396
|
+
* missing: the read then goes ahead, and an id that names nothing reads as empty.
|
|
397
|
+
*/
|
|
398
|
+
async function findUnlistedLinearProject(integration, ids, connectionId) {
|
|
399
|
+
let unlisted;
|
|
400
|
+
for (const fresh of [false, true]) {
|
|
401
|
+
const projects = (await integration.getWorkspaceProjectsResult(connectionId, { fresh }))?.value;
|
|
402
|
+
if (projects == null || projects.truncated === true || projects.metadata != null && projects.metadata.completeness !== "complete") return;
|
|
403
|
+
const listed = new Set(projects.values.map((project) => project.id));
|
|
404
|
+
unlisted = ids.find((id) => !listed.has(id));
|
|
405
|
+
if (unlisted == null) return void 0;
|
|
406
|
+
}
|
|
407
|
+
return unlisted;
|
|
408
|
+
}
|
|
368
409
|
//#endregion
|
|
369
410
|
export { listIssueTrackerIssuesPage };
|
|
370
411
|
|