@elisra-devops/docgen-data-provider 1.63.13 → 1.68.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/.github/workflows/ci.yml +26 -9
- package/.github/workflows/release.yml +9 -10
- package/README.md +50 -24
- package/bin/helpers/tfs.d.ts +3 -0
- package/bin/helpers/tfs.js +44 -7
- package/bin/helpers/tfs.js.map +1 -1
- package/bin/modules/GitDataProvider.d.ts +10 -0
- package/bin/modules/GitDataProvider.js +10 -0
- package/bin/modules/GitDataProvider.js.map +1 -1
- package/bin/modules/TestDataProvider.js +0 -1
- package/bin/modules/TestDataProvider.js.map +1 -1
- package/bin/modules/TicketsDataProvider.d.ts +63 -24
- package/bin/modules/TicketsDataProvider.js +216 -114
- package/bin/modules/TicketsDataProvider.js.map +1 -1
- package/bin/tests/helpers/helper.test.js +279 -0
- package/bin/tests/helpers/helper.test.js.map +1 -0
- package/bin/{helpers/test → tests/helpers}/tfs.test.js +312 -49
- package/bin/tests/helpers/tfs.test.js.map +1 -0
- package/bin/tests/index.test.js +25 -0
- package/bin/tests/index.test.js.map +1 -0
- package/bin/tests/models/tfs-data.test.js +160 -0
- package/bin/tests/models/tfs-data.test.js.map +1 -0
- package/bin/{modules/test → tests/modules}/JfrogDataProvider.test.js +9 -9
- package/bin/tests/modules/JfrogDataProvider.test.js.map +1 -0
- package/bin/tests/modules/ResultDataProvider.test.js +1942 -0
- package/bin/tests/modules/ResultDataProvider.test.js.map +1 -0
- package/bin/tests/modules/gitDataProvider.test.js +1888 -0
- package/bin/tests/modules/gitDataProvider.test.js.map +1 -0
- package/bin/{modules/test → tests/modules}/managmentDataProvider.test.js +13 -1
- package/bin/tests/modules/managmentDataProvider.test.js.map +1 -0
- package/bin/tests/modules/pipelineDataProvider.test.d.ts +1 -0
- package/bin/tests/modules/pipelineDataProvider.test.js +783 -0
- package/bin/tests/modules/pipelineDataProvider.test.js.map +1 -0
- package/bin/tests/modules/testDataProvider.test.d.ts +1 -0
- package/bin/tests/modules/testDataProvider.test.js +717 -0
- package/bin/tests/modules/testDataProvider.test.js.map +1 -0
- package/bin/tests/modules/ticketsDataProvider.test.d.ts +1 -0
- package/bin/tests/modules/ticketsDataProvider.test.js +1681 -0
- package/bin/tests/modules/ticketsDataProvider.test.js.map +1 -0
- package/bin/tests/utils/DataProviderUtils.test.d.ts +1 -0
- package/bin/tests/utils/DataProviderUtils.test.js +61 -0
- package/bin/tests/utils/DataProviderUtils.test.js.map +1 -0
- package/bin/tests/utils/testStepParserHelper.test.d.ts +1 -0
- package/bin/tests/utils/testStepParserHelper.test.js +359 -0
- package/bin/tests/utils/testStepParserHelper.test.js.map +1 -0
- package/package.json +9 -1
- package/src/helpers/tfs.ts +51 -7
- package/src/modules/GitDataProvider.ts +10 -0
- package/src/modules/TestDataProvider.ts +0 -1
- package/src/modules/TicketsDataProvider.ts +298 -141
- package/src/tests/helpers/helper.test.ts +337 -0
- package/src/tests/helpers/tfs.test.ts +1092 -0
- package/src/tests/index.test.ts +28 -0
- package/src/tests/models/tfs-data.test.ts +203 -0
- package/src/tests/modules/JfrogDataProvider.test.ts +167 -0
- package/src/tests/modules/ResultDataProvider.test.ts +2571 -0
- package/src/tests/modules/gitDataProvider.test.ts +2628 -0
- package/src/{modules/test → tests/modules}/managmentDataProvider.test.ts +33 -1
- package/src/tests/modules/pipelineDataProvider.test.ts +1038 -0
- package/src/tests/modules/testDataProvider.test.ts +1046 -0
- package/src/tests/modules/ticketsDataProvider.test.ts +2204 -0
- package/src/tests/utils/DataProviderUtils.test.ts +76 -0
- package/src/tests/utils/testStepParserHelper.test.ts +437 -0
- package/tsconfig.json +1 -0
- package/bin/helpers/test/tfs.test.js.map +0 -1
- package/bin/modules/test/JfrogDataProvider.test.js.map +0 -1
- package/bin/modules/test/ResultDataProvider.test.js +0 -444
- package/bin/modules/test/ResultDataProvider.test.js.map +0 -1
- package/bin/modules/test/gitDataProvider.test.js +0 -428
- package/bin/modules/test/gitDataProvider.test.js.map +0 -1
- package/bin/modules/test/managmentDataProvider.test.js.map +0 -1
- package/bin/modules/test/pipelineDataProvider.test.js +0 -237
- package/bin/modules/test/pipelineDataProvider.test.js.map +0 -1
- package/bin/modules/test/testDataProvider.test.js +0 -234
- package/bin/modules/test/testDataProvider.test.js.map +0 -1
- package/bin/modules/test/ticketsDataProvider.test.js +0 -348
- package/bin/modules/test/ticketsDataProvider.test.js.map +0 -1
- package/src/helpers/test/tfs.test.ts +0 -748
- package/src/modules/test/JfrogDataProvider.test.ts +0 -171
- package/src/modules/test/ResultDataProvider.test.ts +0 -542
- package/src/modules/test/gitDataProvider.test.ts +0 -645
- package/src/modules/test/pipelineDataProvider.test.ts +0 -292
- package/src/modules/test/testDataProvider.test.ts +0 -318
- package/src/modules/test/ticketsDataProvider.test.ts +0 -462
- /package/bin/{helpers/test/tfs.test.d.ts → tests/helpers/helper.test.d.ts} +0 -0
- /package/bin/{modules/test/JfrogDataProvider.test.d.ts → tests/helpers/tfs.test.d.ts} +0 -0
- /package/bin/{modules/test/ResultDataProvider.test.d.ts → tests/index.test.d.ts} +0 -0
- /package/bin/{modules/test/gitDataProvider.test.d.ts → tests/models/tfs-data.test.d.ts} +0 -0
- /package/bin/{modules/test/managmentDataProvider.test.d.ts → tests/modules/JfrogDataProvider.test.d.ts} +0 -0
- /package/bin/{modules/test/pipelineDataProvider.test.d.ts → tests/modules/ResultDataProvider.test.d.ts} +0 -0
- /package/bin/{modules/test/testDataProvider.test.d.ts → tests/modules/gitDataProvider.test.d.ts} +0 -0
- /package/bin/{modules/test/ticketsDataProvider.test.d.ts → tests/modules/managmentDataProvider.test.d.ts} +0 -0
|
@@ -1742,9 +1742,12 @@ export default class TicketsDataProvider {
|
|
|
1742
1742
|
targetAreaFilter?: string,
|
|
1743
1743
|
includeTreeQueries: boolean = false,
|
|
1744
1744
|
excludedFolderNames: string[] = [],
|
|
1745
|
-
includeFlatQueries: boolean = false
|
|
1745
|
+
includeFlatQueries: boolean = false,
|
|
1746
|
+
workItemTypeCache?: Map<string, string | null>
|
|
1746
1747
|
): Promise<any> {
|
|
1747
1748
|
try {
|
|
1749
|
+
// Per-invocation cache for ID->WorkItemType lookups; avoids global state and is safe for concurrency.
|
|
1750
|
+
const typeCache = workItemTypeCache ?? new Map<string, string | null>();
|
|
1748
1751
|
const shouldSkipFolder =
|
|
1749
1752
|
rootQuery?.isFolder &&
|
|
1750
1753
|
excludedFolderNames.some(
|
|
@@ -1768,7 +1771,12 @@ export default class TicketsDataProvider {
|
|
|
1768
1771
|
|
|
1769
1772
|
if (rootQuery.queryType === 'flat' && includeFlatQueries) {
|
|
1770
1773
|
const allTypes = Array.from(new Set([...(sources || []), ...(targets || [])]));
|
|
1771
|
-
const typesOk = this.
|
|
1774
|
+
const typesOk = await this.matchesFlatWorkItemTypeConditionAsync(
|
|
1775
|
+
rootQuery,
|
|
1776
|
+
wiql,
|
|
1777
|
+
allTypes,
|
|
1778
|
+
typeCache
|
|
1779
|
+
);
|
|
1772
1780
|
|
|
1773
1781
|
if (typesOk) {
|
|
1774
1782
|
const allowTree1 =
|
|
@@ -1787,7 +1795,25 @@ export default class TicketsDataProvider {
|
|
|
1787
1795
|
}
|
|
1788
1796
|
}
|
|
1789
1797
|
} else {
|
|
1790
|
-
|
|
1798
|
+
let matchesForward = false;
|
|
1799
|
+
if (!onlyTestReq) {
|
|
1800
|
+
matchesForward = await this.matchesSourceTargetConditionAsync(
|
|
1801
|
+
rootQuery,
|
|
1802
|
+
wiql,
|
|
1803
|
+
sources,
|
|
1804
|
+
targets,
|
|
1805
|
+
typeCache
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
const matchesReverse = await this.matchesSourceTargetConditionAsync(
|
|
1809
|
+
rootQuery,
|
|
1810
|
+
wiql,
|
|
1811
|
+
targets,
|
|
1812
|
+
sources,
|
|
1813
|
+
typeCache
|
|
1814
|
+
);
|
|
1815
|
+
|
|
1816
|
+
if (matchesForward) {
|
|
1791
1817
|
const matchesAreaPath =
|
|
1792
1818
|
sourceAreaFilter || targetAreaFilter
|
|
1793
1819
|
? this.matchesAreaPathCondition(wiql, sourceAreaFilter || '', targetAreaFilter || '')
|
|
@@ -1797,7 +1823,7 @@ export default class TicketsDataProvider {
|
|
|
1797
1823
|
tree1Node = this.buildQueryNode(rootQuery, parentId);
|
|
1798
1824
|
}
|
|
1799
1825
|
}
|
|
1800
|
-
if (
|
|
1826
|
+
if (matchesReverse) {
|
|
1801
1827
|
const matchesReverseAreaPath =
|
|
1802
1828
|
sourceAreaFilter || targetAreaFilter
|
|
1803
1829
|
? this.matchesAreaPathCondition(wiql, targetAreaFilter || '', sourceAreaFilter || '')
|
|
@@ -1817,20 +1843,22 @@ export default class TicketsDataProvider {
|
|
|
1817
1843
|
if (!rootQuery.children) {
|
|
1818
1844
|
const queryUrl = `${rootQuery.url}?$depth=2&$expand=all`;
|
|
1819
1845
|
const currentQuery = await TFSServices.getItemContent(queryUrl, this.token);
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1846
|
+
if (!currentQuery) {
|
|
1847
|
+
return { tree1: null, tree2: null };
|
|
1848
|
+
}
|
|
1849
|
+
return await this.structureFetchedQueries(
|
|
1850
|
+
currentQuery,
|
|
1851
|
+
onlyTestReq,
|
|
1852
|
+
currentQuery.id,
|
|
1853
|
+
sources,
|
|
1854
|
+
targets,
|
|
1855
|
+
sourceAreaFilter,
|
|
1856
|
+
targetAreaFilter,
|
|
1857
|
+
includeTreeQueries,
|
|
1858
|
+
excludedFolderNames,
|
|
1859
|
+
includeFlatQueries,
|
|
1860
|
+
typeCache
|
|
1861
|
+
);
|
|
1834
1862
|
}
|
|
1835
1863
|
|
|
1836
1864
|
// Process children recursively
|
|
@@ -1846,7 +1874,8 @@ export default class TicketsDataProvider {
|
|
|
1846
1874
|
targetAreaFilter,
|
|
1847
1875
|
includeTreeQueries,
|
|
1848
1876
|
excludedFolderNames,
|
|
1849
|
-
includeFlatQueries
|
|
1877
|
+
includeFlatQueries,
|
|
1878
|
+
typeCache
|
|
1850
1879
|
)
|
|
1851
1880
|
)
|
|
1852
1881
|
);
|
|
@@ -1905,7 +1934,8 @@ export default class TicketsDataProvider {
|
|
|
1905
1934
|
const tgtFilter = (targetAreaFilter || '').toLowerCase().trim();
|
|
1906
1935
|
|
|
1907
1936
|
const extractAreaPaths = (owner: 'source' | 'target'): string[] => {
|
|
1908
|
-
const
|
|
1937
|
+
const ownerPattern = `(?:${owner}|\\[${owner}\\])`;
|
|
1938
|
+
const re = new RegExp(`${ownerPattern}\\.\\[system\\.areapath\\][^']*'([^']+)'`, 'gi');
|
|
1909
1939
|
const results: string[] = [];
|
|
1910
1940
|
let match: RegExpExecArray | null;
|
|
1911
1941
|
while ((match = re.exec(wiqlLower)) !== null) {
|
|
@@ -1930,139 +1960,292 @@ export default class TicketsDataProvider {
|
|
|
1930
1960
|
return hasSourceAreaPath && hasTargetAreaPath;
|
|
1931
1961
|
}
|
|
1932
1962
|
|
|
1963
|
+
private async matchesSourceTargetConditionAsync(
|
|
1964
|
+
queryNode: any,
|
|
1965
|
+
wiql: string,
|
|
1966
|
+
source: string[],
|
|
1967
|
+
target: string[],
|
|
1968
|
+
workItemTypeCache: Map<string, string | null>
|
|
1969
|
+
): Promise<boolean> {
|
|
1970
|
+
/**
|
|
1971
|
+
* Matches source+target constraints for link WIQL.
|
|
1972
|
+
* For each side (Source/Target) we accept either:
|
|
1973
|
+
* - explicit `[System.WorkItemType]` constraints (all types must be within the allowed list), or
|
|
1974
|
+
* - explicit `[System.Id]` constraints when no type constraint exists (type is fetched by id and validated).
|
|
1975
|
+
*/
|
|
1976
|
+
const sourceOk = await this.isLinkSideAllowedByTypeOrId(
|
|
1977
|
+
queryNode,
|
|
1978
|
+
wiql,
|
|
1979
|
+
'Source',
|
|
1980
|
+
source,
|
|
1981
|
+
workItemTypeCache
|
|
1982
|
+
);
|
|
1983
|
+
if (!sourceOk) return false;
|
|
1984
|
+
const targetOk = await this.isLinkSideAllowedByTypeOrId(
|
|
1985
|
+
queryNode,
|
|
1986
|
+
wiql,
|
|
1987
|
+
'Target',
|
|
1988
|
+
target,
|
|
1989
|
+
workItemTypeCache
|
|
1990
|
+
);
|
|
1991
|
+
return targetOk;
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
private async matchesFlatWorkItemTypeConditionAsync(
|
|
1995
|
+
queryNode: any,
|
|
1996
|
+
wiql: string,
|
|
1997
|
+
allowedTypes: string[],
|
|
1998
|
+
workItemTypeCache: Map<string, string | null>
|
|
1999
|
+
): Promise<boolean> {
|
|
2000
|
+
return this.isFlatQueryAllowedByTypeOrId(queryNode, wiql, allowedTypes, workItemTypeCache);
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
// Build a normalized node object for tree outputs
|
|
2004
|
+
private buildQueryNode(rootQuery: any, parentId: any) {
|
|
2005
|
+
return {
|
|
2006
|
+
id: rootQuery.id,
|
|
2007
|
+
pId: parentId,
|
|
2008
|
+
value: rootQuery.name,
|
|
2009
|
+
title: rootQuery.name,
|
|
2010
|
+
queryType: rootQuery.queryType,
|
|
2011
|
+
columns: rootQuery.columns,
|
|
2012
|
+
wiql: rootQuery._links.wiql ?? undefined,
|
|
2013
|
+
isValidQuery: true,
|
|
2014
|
+
};
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
private getProjectFromQueryNode(queryNode: any): string | null {
|
|
2018
|
+
const href =
|
|
2019
|
+
queryNode?._links?.wiql?.href ||
|
|
2020
|
+
queryNode?._links?.self?.href ||
|
|
2021
|
+
queryNode?.url ||
|
|
2022
|
+
queryNode?._links?.html?.href;
|
|
2023
|
+
if (!href) return null;
|
|
2024
|
+
return this.getProjectFromWiqlHref(String(href));
|
|
2025
|
+
}
|
|
2026
|
+
|
|
1933
2027
|
/**
|
|
1934
|
-
*
|
|
1935
|
-
* source and target conditions. It checks if the WIQL contains references to the specified
|
|
1936
|
-
* source and target work item types.
|
|
1937
|
-
*
|
|
1938
|
-
* Supports both equality (=) and IN operators:
|
|
1939
|
-
* - Source.[System.WorkItemType] = 'Epic'
|
|
1940
|
-
* - Source.[System.WorkItemType] IN ('Epic', 'Feature', 'Requirement')
|
|
1941
|
-
*
|
|
1942
|
-
* @returns A boolean indicating whether the WIQL includes at least one valid source work item type
|
|
1943
|
-
* and at least one valid target work item type.
|
|
2028
|
+
* Normalizes allowed work item types into a lowercase set.
|
|
1944
2029
|
*/
|
|
1945
|
-
private
|
|
1946
|
-
|
|
1947
|
-
const isTargetIncluded = this.matchesWorkItemTypeCondition(wiql, 'Target', target);
|
|
1948
|
-
return isSourceIncluded && isTargetIncluded;
|
|
2030
|
+
private normalizeAllowedTypes(allowedTypes: string[]): Set<string> {
|
|
2031
|
+
return new Set((allowedTypes || []).map((t) => String(t).trim().toLowerCase()).filter(Boolean));
|
|
1949
2032
|
}
|
|
1950
2033
|
|
|
1951
2034
|
/**
|
|
1952
|
-
*
|
|
1953
|
-
* Supports both = and IN operators.
|
|
1954
|
-
*
|
|
1955
|
-
* @param wiql - The WIQL string to evaluate
|
|
1956
|
-
* @param context - Either 'Source' or 'Target'
|
|
1957
|
-
* @param allowedTypes - Array of allowed work item types
|
|
1958
|
-
* @returns true if all work item types in the WIQL are in the allowedTypes array
|
|
2035
|
+
* Returns true when every value in `found` exists in `allowed`.
|
|
1959
2036
|
*/
|
|
1960
|
-
private
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
allowedTypes: string[]
|
|
1964
|
-
): boolean {
|
|
1965
|
-
const wiqlStr = String(wiql || '');
|
|
1966
|
-
|
|
1967
|
-
// If allowedTypes is empty, accept any work item type (for backward compatibility)
|
|
1968
|
-
if (!allowedTypes || allowedTypes.length === 0) {
|
|
1969
|
-
const fieldPresenceRegex = new RegExp(`${context}\\.\\[System.WorkItemType\\]`, 'i');
|
|
1970
|
-
return fieldPresenceRegex.test(wiqlStr);
|
|
2037
|
+
private areAllAllowed(found: Set<string>, allowed: Set<string>): boolean {
|
|
2038
|
+
for (const v of found) {
|
|
2039
|
+
if (!allowed.has(v)) return false;
|
|
1971
2040
|
}
|
|
2041
|
+
return true;
|
|
2042
|
+
}
|
|
1972
2043
|
|
|
1973
|
-
|
|
2044
|
+
/**
|
|
2045
|
+
* Builds a regex-ready field selector for WIQL.
|
|
2046
|
+
* - Link queries: Source/Target can appear as `Source.[...]` or `[Source].[...]`
|
|
2047
|
+
* - Flat queries: no owner prefix, e.g. `[System.WorkItemType]`
|
|
2048
|
+
*/
|
|
2049
|
+
private buildWiqlFieldPattern(owner: 'Source' | 'Target' | null, fieldRef: string): string {
|
|
2050
|
+
const escapedField = String(fieldRef).replace(/\./g, '\\.');
|
|
2051
|
+
if (!owner) {
|
|
2052
|
+
return `\\[${escapedField}\\]`;
|
|
2053
|
+
}
|
|
2054
|
+
const ownerPattern = `(?:${owner}|\\[${owner}\\])`;
|
|
2055
|
+
return `${ownerPattern}\\.\\[${escapedField}\\]`;
|
|
2056
|
+
}
|
|
1974
2057
|
|
|
1975
|
-
|
|
1976
|
-
|
|
2058
|
+
/**
|
|
2059
|
+
* Extracts all quoted values used in `=` or `IN (...)` comparisons for a given field.
|
|
2060
|
+
* Returned values are trimmed and lowercased.
|
|
2061
|
+
*
|
|
2062
|
+
* Examples:
|
|
2063
|
+
* - `<field> = 'Epic'`
|
|
2064
|
+
* - `<field> IN ('Epic','Feature')`
|
|
2065
|
+
*/
|
|
2066
|
+
private extractQuotedValuesForField(wiql: string, fieldPattern: string): Set<string> {
|
|
2067
|
+
const wiqlStr = String(wiql || '');
|
|
2068
|
+
const found = new Set<string>();
|
|
1977
2069
|
|
|
1978
|
-
|
|
1979
|
-
const
|
|
2070
|
+
const eqRe = new RegExp(`${fieldPattern}\\s*=\\s*'([^']+)'`, 'gi');
|
|
2071
|
+
const inRe = new RegExp(`${fieldPattern}\\s+IN\\s*\\(([^)]+)\\)`, 'gi');
|
|
1980
2072
|
|
|
1981
|
-
const foundTypes = new Set<string>();
|
|
1982
2073
|
let match: RegExpExecArray | null;
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
while ((match = equalityRegex.exec(wiqlStr)) !== null) {
|
|
1986
|
-
foundTypes.add(String(match[1]).trim().toLowerCase());
|
|
2074
|
+
while ((match = eqRe.exec(wiqlStr)) !== null) {
|
|
2075
|
+
found.add(String(match[1]).trim().toLowerCase());
|
|
1987
2076
|
}
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
// Extract all quoted values from the IN clause
|
|
1993
|
-
const typeMatches = typesString.matchAll(/'([^']+)'/g);
|
|
1994
|
-
for (const typeMatch of typeMatches) {
|
|
1995
|
-
foundTypes.add(String(typeMatch[1]).trim().toLowerCase());
|
|
2077
|
+
while ((match = inRe.exec(wiqlStr)) !== null) {
|
|
2078
|
+
const inner = String(match[1] || '');
|
|
2079
|
+
for (const mm of inner.matchAll(/'([^']+)'/g)) {
|
|
2080
|
+
found.add(String(mm[1]).trim().toLowerCase());
|
|
1996
2081
|
}
|
|
1997
2082
|
}
|
|
2083
|
+
return found;
|
|
2084
|
+
}
|
|
1998
2085
|
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2086
|
+
/**
|
|
2087
|
+
* Extracts all numeric values used in `=` or `IN (...)` comparisons for a given field.
|
|
2088
|
+
* Supports both quoted and unquoted numbers.
|
|
2089
|
+
*
|
|
2090
|
+
* Examples:
|
|
2091
|
+
* - `<field> = 123`
|
|
2092
|
+
* - `<field> IN (123, '456')`
|
|
2093
|
+
*/
|
|
2094
|
+
private extractNumericValuesForField(wiql: string, fieldPattern: string): Set<string> {
|
|
2095
|
+
const wiqlStr = String(wiql || '');
|
|
2096
|
+
const found = new Set<string>();
|
|
2003
2097
|
|
|
2004
|
-
|
|
2005
|
-
const
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2098
|
+
const eqRe = new RegExp(`${fieldPattern}\\s*=\\s*'?([0-9]+)'?`, 'gi');
|
|
2099
|
+
const inRe = new RegExp(`${fieldPattern}\\s+IN\\s*\\(([^)]+)\\)`, 'gi');
|
|
2100
|
+
|
|
2101
|
+
let match: RegExpExecArray | null;
|
|
2102
|
+
while ((match = eqRe.exec(wiqlStr)) !== null) {
|
|
2103
|
+
found.add(String(match[1]));
|
|
2104
|
+
}
|
|
2105
|
+
while ((match = inRe.exec(wiqlStr)) !== null) {
|
|
2106
|
+
const inner = String(match[1] || '');
|
|
2107
|
+
for (const mm of inner.matchAll(/'?([0-9]+)'?/g)) {
|
|
2108
|
+
found.add(String(mm[1]));
|
|
2010
2109
|
}
|
|
2011
2110
|
}
|
|
2012
|
-
|
|
2013
|
-
// All found types are valid
|
|
2014
|
-
return true;
|
|
2111
|
+
return found;
|
|
2015
2112
|
}
|
|
2016
2113
|
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
columns: rootQuery.columns,
|
|
2026
|
-
wiql: rootQuery._links.wiql ?? undefined,
|
|
2027
|
-
isValidQuery: true,
|
|
2028
|
-
};
|
|
2114
|
+
/**
|
|
2115
|
+
* Extracts work item IDs from WIQL.
|
|
2116
|
+
* - Link queries: pass `context` to match `Source.[System.Id]` / `Target.[System.Id]` (with or without `[Source]`)
|
|
2117
|
+
* - Flat queries: omit `context` to match `[System.Id]`
|
|
2118
|
+
*/
|
|
2119
|
+
private extractWorkItemIdsFromWiql(wiql: string, context?: 'Source' | 'Target'): string[] {
|
|
2120
|
+
const fieldPattern = this.buildWiqlFieldPattern(context ?? null, 'System.Id');
|
|
2121
|
+
return [...this.extractNumericValuesForField(String(wiql || ''), fieldPattern).values()];
|
|
2029
2122
|
}
|
|
2030
2123
|
|
|
2031
2124
|
/**
|
|
2032
|
-
*
|
|
2033
|
-
*
|
|
2125
|
+
* Determines if a link-query side (Source/Target) is allowed based on WIQL constraints.
|
|
2126
|
+
*
|
|
2127
|
+
* Rules:
|
|
2128
|
+
* - If `allowedTypes` is empty: preserve legacy behavior by requiring that WorkItemType is present for that side.
|
|
2129
|
+
* - If WIQL contains a WorkItemType constraint for that side: all specified types must be within `allowedTypes`.
|
|
2130
|
+
* - Otherwise, if WIQL contains a System.Id constraint for that side: fetch the work item type(s) by id and validate.
|
|
2131
|
+
* - Otherwise: reject.
|
|
2034
2132
|
*/
|
|
2035
|
-
private
|
|
2036
|
-
|
|
2133
|
+
private async isLinkSideAllowedByTypeOrId(
|
|
2134
|
+
queryNode: any,
|
|
2135
|
+
wiql: string,
|
|
2136
|
+
context: 'Source' | 'Target',
|
|
2137
|
+
allowedTypes: string[],
|
|
2138
|
+
workItemTypeCache: Map<string, string | null>
|
|
2139
|
+
): Promise<boolean> {
|
|
2140
|
+
const wiqlStr = String(wiql || '');
|
|
2141
|
+
|
|
2142
|
+
// Preserve existing behavior when no allowedTypes are provided.
|
|
2037
2143
|
if (!allowedTypes || allowedTypes.length === 0) {
|
|
2038
|
-
|
|
2144
|
+
const fieldPresenceRegex = new RegExp(
|
|
2145
|
+
`${this.buildWiqlFieldPattern(context, 'System.WorkItemType')}`,
|
|
2146
|
+
'i'
|
|
2147
|
+
);
|
|
2148
|
+
return fieldPresenceRegex.test(wiqlStr);
|
|
2039
2149
|
}
|
|
2040
2150
|
|
|
2151
|
+
const allowed = this.normalizeAllowedTypes(allowedTypes);
|
|
2152
|
+
const typeFieldPattern = this.buildWiqlFieldPattern(context, 'System.WorkItemType');
|
|
2153
|
+
const typesInWiql = this.extractQuotedValuesForField(wiqlStr, typeFieldPattern);
|
|
2154
|
+
if (typesInWiql.size > 0) {
|
|
2155
|
+
return this.areAllAllowed(typesInWiql, allowed);
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
const ids = this.extractWorkItemIdsFromWiql(wiqlStr, context);
|
|
2159
|
+
if (ids.length === 0) return false;
|
|
2160
|
+
|
|
2161
|
+
const project = this.getProjectFromQueryNode(queryNode);
|
|
2162
|
+
if (!project) return false;
|
|
2163
|
+
|
|
2164
|
+
for (const id of ids) {
|
|
2165
|
+
const wiType = await this.getWorkItemTypeById(project, id, workItemTypeCache);
|
|
2166
|
+
if (!wiType) return false;
|
|
2167
|
+
if (!allowed.has(String(wiType).toLowerCase())) return false;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
return true;
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
/**
|
|
2174
|
+
* Determines if a flat query is allowed based on WIQL constraints.
|
|
2175
|
+
*
|
|
2176
|
+
* Rules:
|
|
2177
|
+
* - If `allowedTypes` is empty: preserve legacy behavior by requiring that `[System.WorkItemType]` appears in WIQL.
|
|
2178
|
+
* - If WIQL contains a WorkItemType constraint: all specified types must be within `allowedTypes`.
|
|
2179
|
+
* - Otherwise, if WIQL contains `[System.Id]`: fetch the work item type(s) by id and validate.
|
|
2180
|
+
* - Otherwise: reject.
|
|
2181
|
+
*/
|
|
2182
|
+
private async isFlatQueryAllowedByTypeOrId(
|
|
2183
|
+
queryNode: any,
|
|
2184
|
+
wiql: string,
|
|
2185
|
+
allowedTypes: string[],
|
|
2186
|
+
workItemTypeCache: Map<string, string | null>
|
|
2187
|
+
): Promise<boolean> {
|
|
2041
2188
|
const wiqlStr = String(wiql || '');
|
|
2042
|
-
const eqRe = /\[System\.WorkItemType\]\s*=\s*'([^']+)'/gi;
|
|
2043
|
-
const inRe = /\[System\.WorkItemType\]\s+IN\s*\(([^)]+)\)/gi;
|
|
2044
2189
|
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
found.add(m[1].trim().toLowerCase());
|
|
2190
|
+
// Preserve existing behavior when no allowedTypes are provided.
|
|
2191
|
+
if (!allowedTypes || allowedTypes.length === 0) {
|
|
2192
|
+
return /\[System\.WorkItemType\]/i.test(wiqlStr);
|
|
2049
2193
|
}
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2194
|
+
|
|
2195
|
+
const allowed = this.normalizeAllowedTypes(allowedTypes);
|
|
2196
|
+
const typeFieldPattern = this.buildWiqlFieldPattern(null, 'System.WorkItemType');
|
|
2197
|
+
const typesInWiql = this.extractQuotedValuesForField(wiqlStr, typeFieldPattern);
|
|
2198
|
+
if (typesInWiql.size > 0) {
|
|
2199
|
+
return this.areAllAllowed(typesInWiql, allowed);
|
|
2055
2200
|
}
|
|
2056
2201
|
|
|
2057
|
-
|
|
2202
|
+
const ids = this.extractWorkItemIdsFromWiql(wiqlStr);
|
|
2203
|
+
if (ids.length === 0) return false;
|
|
2058
2204
|
|
|
2059
|
-
const
|
|
2060
|
-
|
|
2061
|
-
|
|
2205
|
+
const project = this.getProjectFromQueryNode(queryNode);
|
|
2206
|
+
if (!project) return false;
|
|
2207
|
+
|
|
2208
|
+
for (const id of ids) {
|
|
2209
|
+
const wiType = await this.getWorkItemTypeById(project, id, workItemTypeCache);
|
|
2210
|
+
if (!wiType) return false;
|
|
2211
|
+
if (!allowed.has(String(wiType).toLowerCase())) return false;
|
|
2062
2212
|
}
|
|
2213
|
+
|
|
2063
2214
|
return true;
|
|
2064
2215
|
}
|
|
2065
2216
|
|
|
2217
|
+
/**
|
|
2218
|
+
* Fetches the work item type for a given work item ID.
|
|
2219
|
+
* Uses caching to avoid repeated API calls.
|
|
2220
|
+
*
|
|
2221
|
+
* @param project The project name
|
|
2222
|
+
* @param id The work item ID
|
|
2223
|
+
* @param workItemTypeCache The cache map for work item types
|
|
2224
|
+
* @returns The work item type or null if not found
|
|
2225
|
+
*/
|
|
2226
|
+
private async getWorkItemTypeById(
|
|
2227
|
+
project: string,
|
|
2228
|
+
id: string,
|
|
2229
|
+
workItemTypeCache: Map<string, string | null>
|
|
2230
|
+
): Promise<string | null> {
|
|
2231
|
+
const cacheKey = `${project}:${id}`;
|
|
2232
|
+
if (workItemTypeCache.has(cacheKey)) {
|
|
2233
|
+
return workItemTypeCache.get(cacheKey) ?? null;
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
try {
|
|
2237
|
+
const url = `${this.orgUrl}${project}/_apis/wit/workitems/${id}?fields=System.WorkItemType`;
|
|
2238
|
+
const wi = await this.limit(() => TFSServices.getItemContent(url, this.token));
|
|
2239
|
+
const wiType = wi?.fields?.['System.WorkItemType'];
|
|
2240
|
+
const normalized = wiType ? String(wiType) : null;
|
|
2241
|
+
workItemTypeCache.set(cacheKey, normalized);
|
|
2242
|
+
return normalized;
|
|
2243
|
+
} catch (e) {
|
|
2244
|
+
workItemTypeCache.set(cacheKey, null);
|
|
2245
|
+
return null;
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2066
2249
|
/**
|
|
2067
2250
|
* Matches flat query WIQL against an area path filter by checking any referenced [System.AreaPath].
|
|
2068
2251
|
* Compares only the leaf segment of the path and performs a case-insensitive substring match.
|
|
@@ -2358,30 +2541,4 @@ export default class TicketsDataProvider {
|
|
|
2358
2541
|
throw err;
|
|
2359
2542
|
}
|
|
2360
2543
|
}
|
|
2361
|
-
|
|
2362
|
-
/**
|
|
2363
|
-
* Helper method to flatten a tree structure into a flat array of work items
|
|
2364
|
-
*/
|
|
2365
|
-
private flattenTreeToWorkItems(roots: any[]): any[] {
|
|
2366
|
-
const result: any[] = [];
|
|
2367
|
-
|
|
2368
|
-
const traverse = (node: any) => {
|
|
2369
|
-
if (!node) return;
|
|
2370
|
-
|
|
2371
|
-
result.push({
|
|
2372
|
-
id: node.id,
|
|
2373
|
-
title: node.title,
|
|
2374
|
-
description: node.description,
|
|
2375
|
-
htmlUrl: node.htmlUrl,
|
|
2376
|
-
url: node.htmlUrl, // Some nodes might use url instead
|
|
2377
|
-
});
|
|
2378
|
-
|
|
2379
|
-
if (Array.isArray(node.children)) {
|
|
2380
|
-
node.children.forEach(traverse);
|
|
2381
|
-
}
|
|
2382
|
-
};
|
|
2383
|
-
|
|
2384
|
-
roots.forEach(traverse);
|
|
2385
|
-
return result;
|
|
2386
|
-
}
|
|
2387
2544
|
}
|