@elisra-devops/docgen-data-provider 1.63.12 → 1.67.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/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/MangementDataProvider.js +7 -1
- package/bin/modules/MangementDataProvider.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 -27
- package/bin/modules/TicketsDataProvider.js +226 -122
- 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 +39 -31
- 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 +10 -1
- package/src/helpers/tfs.ts +51 -7
- package/src/modules/GitDataProvider.ts +10 -0
- package/src/modules/MangementDataProvider.ts +6 -1
- package/src/modules/TestDataProvider.ts +0 -1
- package/src/modules/TicketsDataProvider.ts +311 -151
- 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 +63 -32
- 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 -433
- 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 -322
- 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 -691
- 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 -434
- /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
|
@@ -69,7 +69,9 @@ export default class TicketsDataProvider {
|
|
|
69
69
|
const fieldsArr = Array.isArray(fieldsResp?.value) ? fieldsResp.value : [];
|
|
70
70
|
const candidates = fieldsArr
|
|
71
71
|
.filter((f: any) => {
|
|
72
|
-
const nm = String(f?.name || '')
|
|
72
|
+
const nm = String(f?.name || '')
|
|
73
|
+
.toLowerCase()
|
|
74
|
+
.replace(/_/g, ' ');
|
|
73
75
|
return nm.includes('requirement type');
|
|
74
76
|
})
|
|
75
77
|
.map((f: any) => f?.referenceName)
|
|
@@ -569,7 +571,7 @@ export default class TicketsDataProvider {
|
|
|
569
571
|
queries,
|
|
570
572
|
false,
|
|
571
573
|
null,
|
|
572
|
-
['
|
|
574
|
+
['epic', 'feature', 'requirement'],
|
|
573
575
|
[],
|
|
574
576
|
undefined,
|
|
575
577
|
undefined,
|
|
@@ -643,8 +645,8 @@ export default class TicketsDataProvider {
|
|
|
643
645
|
queries,
|
|
644
646
|
onlySourceSide,
|
|
645
647
|
null,
|
|
646
|
-
['
|
|
647
|
-
['
|
|
648
|
+
['epic', 'feature', 'requirement'],
|
|
649
|
+
['epic', 'feature', 'requirement'],
|
|
648
650
|
'sys', // Source area filter for tree1: System area paths
|
|
649
651
|
'soft' // Target area filter for tree1: Software area paths (tree2 will be reversed automatically)
|
|
650
652
|
);
|
|
@@ -660,8 +662,8 @@ export default class TicketsDataProvider {
|
|
|
660
662
|
folder,
|
|
661
663
|
false,
|
|
662
664
|
null,
|
|
663
|
-
['
|
|
664
|
-
['
|
|
665
|
+
['epic', 'feature', 'requirement'],
|
|
666
|
+
['epic', 'feature', 'requirement'],
|
|
665
667
|
undefined,
|
|
666
668
|
undefined,
|
|
667
669
|
true
|
|
@@ -1740,9 +1742,12 @@ export default class TicketsDataProvider {
|
|
|
1740
1742
|
targetAreaFilter?: string,
|
|
1741
1743
|
includeTreeQueries: boolean = false,
|
|
1742
1744
|
excludedFolderNames: string[] = [],
|
|
1743
|
-
includeFlatQueries: boolean = false
|
|
1745
|
+
includeFlatQueries: boolean = false,
|
|
1746
|
+
workItemTypeCache?: Map<string, string | null>
|
|
1744
1747
|
): Promise<any> {
|
|
1745
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>();
|
|
1746
1751
|
const shouldSkipFolder =
|
|
1747
1752
|
rootQuery?.isFolder &&
|
|
1748
1753
|
excludedFolderNames.some(
|
|
@@ -1766,14 +1771,17 @@ export default class TicketsDataProvider {
|
|
|
1766
1771
|
|
|
1767
1772
|
if (rootQuery.queryType === 'flat' && includeFlatQueries) {
|
|
1768
1773
|
const allTypes = Array.from(new Set([...(sources || []), ...(targets || [])]));
|
|
1769
|
-
const typesOk = this.
|
|
1774
|
+
const typesOk = await this.matchesFlatWorkItemTypeConditionAsync(
|
|
1775
|
+
rootQuery,
|
|
1776
|
+
wiql,
|
|
1777
|
+
allTypes,
|
|
1778
|
+
typeCache
|
|
1779
|
+
);
|
|
1770
1780
|
|
|
1771
1781
|
if (typesOk) {
|
|
1772
1782
|
const allowTree1 =
|
|
1773
1783
|
!onlyTestReq &&
|
|
1774
|
-
(sourceAreaFilter
|
|
1775
|
-
? this.matchesFlatAreaCondition(wiql, sourceAreaFilter || '')
|
|
1776
|
-
: true);
|
|
1784
|
+
(sourceAreaFilter ? this.matchesFlatAreaCondition(wiql, sourceAreaFilter || '') : true);
|
|
1777
1785
|
const allowTree2 = targetAreaFilter
|
|
1778
1786
|
? this.matchesFlatAreaCondition(wiql, targetAreaFilter || '')
|
|
1779
1787
|
: true;
|
|
@@ -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,144 +1960,300 @@ 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
|
-
* @param wiql - The WIQL string to evaluate.
|
|
1943
|
-
* @param source - An array of source work item types to check for in the WIQL.
|
|
1944
|
-
* @param target - An array of target work item types to check for in the WIQL.
|
|
1945
|
-
* @returns A boolean indicating whether the WIQL includes at least one valid source work item type
|
|
1946
|
-
* and at least one valid target work item type.
|
|
2028
|
+
* Normalizes allowed work item types into a lowercase set.
|
|
1947
2029
|
*/
|
|
1948
|
-
private
|
|
1949
|
-
|
|
1950
|
-
const isTargetIncluded = this.matchesWorkItemTypeCondition(wiql, 'Target', target);
|
|
1951
|
-
return isSourceIncluded && isTargetIncluded;
|
|
2030
|
+
private normalizeAllowedTypes(allowedTypes: string[]): Set<string> {
|
|
2031
|
+
return new Set((allowedTypes || []).map((t) => String(t).trim().toLowerCase()).filter(Boolean));
|
|
1952
2032
|
}
|
|
1953
2033
|
|
|
1954
2034
|
/**
|
|
1955
|
-
*
|
|
1956
|
-
* Supports both = and IN operators.
|
|
1957
|
-
*
|
|
1958
|
-
* @param wiql - The WIQL string to evaluate
|
|
1959
|
-
* @param context - Either 'Source' or 'Target'
|
|
1960
|
-
* @param allowedTypes - Array of allowed work item types
|
|
1961
|
-
* @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`.
|
|
1962
2036
|
*/
|
|
1963
|
-
private
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
allowedTypes: string[]
|
|
1967
|
-
): boolean {
|
|
1968
|
-
// If allowedTypes is empty, accept any work item type (for backward compatibility)
|
|
1969
|
-
if (allowedTypes.length === 0) {
|
|
1970
|
-
return wiql.includes(`${context}.[System.WorkItemType]`);
|
|
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
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
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
|
+
}
|
|
1977
2057
|
|
|
1978
|
-
|
|
1979
|
-
|
|
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>();
|
|
1980
2069
|
|
|
1981
|
-
const
|
|
2070
|
+
const eqRe = new RegExp(`${fieldPattern}\\s*=\\s*'([^']+)'`, 'gi');
|
|
2071
|
+
const inRe = new RegExp(`${fieldPattern}\\s+IN\\s*\\(([^)]+)\\)`, 'gi');
|
|
1982
2072
|
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
foundTypes.add(match[1].trim());
|
|
2073
|
+
let match: RegExpExecArray | null;
|
|
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(typeMatch[1].trim());
|
|
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
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
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]));
|
|
2009
2109
|
}
|
|
2010
2110
|
}
|
|
2011
|
-
|
|
2012
|
-
// All found types are valid
|
|
2013
|
-
return true;
|
|
2111
|
+
return found;
|
|
2014
2112
|
}
|
|
2015
2113
|
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
columns: rootQuery.columns,
|
|
2025
|
-
wiql: rootQuery._links.wiql ?? undefined,
|
|
2026
|
-
isValidQuery: true,
|
|
2027
|
-
};
|
|
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()];
|
|
2028
2122
|
}
|
|
2029
2123
|
|
|
2030
2124
|
/**
|
|
2031
|
-
*
|
|
2032
|
-
*
|
|
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.
|
|
2033
2132
|
*/
|
|
2034
|
-
private
|
|
2035
|
-
|
|
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.
|
|
2036
2143
|
if (!allowedTypes || allowedTypes.length === 0) {
|
|
2037
|
-
|
|
2144
|
+
const fieldPresenceRegex = new RegExp(
|
|
2145
|
+
`${this.buildWiqlFieldPattern(context, 'System.WorkItemType')}`,
|
|
2146
|
+
'i'
|
|
2147
|
+
);
|
|
2148
|
+
return fieldPresenceRegex.test(wiqlStr);
|
|
2038
2149
|
}
|
|
2039
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> {
|
|
2040
2188
|
const wiqlStr = String(wiql || '');
|
|
2041
|
-
const eqRe = /\[System\.WorkItemType\]\s*=\s*'([^']+)'/gi;
|
|
2042
|
-
const inRe = /\[System\.WorkItemType\]\s+IN\s*\(([^)]+)\)/gi;
|
|
2043
2189
|
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
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);
|
|
2048
2193
|
}
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
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);
|
|
2054
2200
|
}
|
|
2055
2201
|
|
|
2056
|
-
|
|
2202
|
+
const ids = this.extractWorkItemIdsFromWiql(wiqlStr);
|
|
2203
|
+
if (ids.length === 0) return false;
|
|
2204
|
+
|
|
2205
|
+
const project = this.getProjectFromQueryNode(queryNode);
|
|
2206
|
+
if (!project) return false;
|
|
2057
2207
|
|
|
2058
|
-
const
|
|
2059
|
-
|
|
2060
|
-
if (!
|
|
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;
|
|
2061
2212
|
}
|
|
2213
|
+
|
|
2062
2214
|
return true;
|
|
2063
2215
|
}
|
|
2064
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
|
+
|
|
2065
2249
|
/**
|
|
2066
2250
|
* Matches flat query WIQL against an area path filter by checking any referenced [System.AreaPath].
|
|
2067
2251
|
* Compares only the leaf segment of the path and performs a case-insensitive substring match.
|
|
2068
2252
|
*/
|
|
2069
2253
|
private matchesFlatAreaCondition(wiql: string, areaFilter: string): boolean {
|
|
2070
|
-
const filter = String(areaFilter || '')
|
|
2254
|
+
const filter = String(areaFilter || '')
|
|
2255
|
+
.trim()
|
|
2256
|
+
.toLowerCase();
|
|
2071
2257
|
if (!filter) return true;
|
|
2072
2258
|
|
|
2073
2259
|
const wiqlLower = String(wiql || '').toLowerCase();
|
|
@@ -2355,30 +2541,4 @@ export default class TicketsDataProvider {
|
|
|
2355
2541
|
throw err;
|
|
2356
2542
|
}
|
|
2357
2543
|
}
|
|
2358
|
-
|
|
2359
|
-
/**
|
|
2360
|
-
* Helper method to flatten a tree structure into a flat array of work items
|
|
2361
|
-
*/
|
|
2362
|
-
private flattenTreeToWorkItems(roots: any[]): any[] {
|
|
2363
|
-
const result: any[] = [];
|
|
2364
|
-
|
|
2365
|
-
const traverse = (node: any) => {
|
|
2366
|
-
if (!node) return;
|
|
2367
|
-
|
|
2368
|
-
result.push({
|
|
2369
|
-
id: node.id,
|
|
2370
|
-
title: node.title,
|
|
2371
|
-
description: node.description,
|
|
2372
|
-
htmlUrl: node.htmlUrl,
|
|
2373
|
-
url: node.htmlUrl, // Some nodes might use url instead
|
|
2374
|
-
});
|
|
2375
|
-
|
|
2376
|
-
if (Array.isArray(node.children)) {
|
|
2377
|
-
node.children.forEach(traverse);
|
|
2378
|
-
}
|
|
2379
|
-
};
|
|
2380
|
-
|
|
2381
|
-
roots.forEach(traverse);
|
|
2382
|
-
return result;
|
|
2383
|
-
}
|
|
2384
2544
|
}
|