@elisra-devops/docgen-data-provider 1.109.0 → 1.110.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/bin/modules/TicketsDataProvider.d.ts +9 -1
- package/bin/modules/TicketsDataProvider.js +93 -45
- package/bin/modules/TicketsDataProvider.js.map +1 -1
- package/bin/tests/modules/ticketsDataProvider.test.js +196 -21
- package/bin/tests/modules/ticketsDataProvider.test.js.map +1 -1
- package/package.json +1 -1
- package/src/modules/TicketsDataProvider.ts +138 -43
- package/src/tests/modules/ticketsDataProvider.test.ts +242 -26
|
@@ -100,6 +100,13 @@ export default class TicketsDataProvider {
|
|
|
100
100
|
* @returns An object containing `systemRequirementsQueryTree`.
|
|
101
101
|
*/
|
|
102
102
|
private fetchSystemRequirementQueries;
|
|
103
|
+
/**
|
|
104
|
+
* Fetches flat Customer/System requirement queries for SysRS traceability.
|
|
105
|
+
* OneHop/tree queries are intentionally excluded for this picker.
|
|
106
|
+
*/
|
|
107
|
+
private fetchFlatUpstreamQueries;
|
|
108
|
+
private fetchCustomerRequirementQueries;
|
|
109
|
+
private structureCustomerRequirementQueries;
|
|
103
110
|
private fetchSrsQueries;
|
|
104
111
|
private fetchSysRsQueries;
|
|
105
112
|
/**
|
|
@@ -213,7 +220,7 @@ export default class TicketsDataProvider {
|
|
|
213
220
|
* Recursively structures fetched queries into two hierarchical trees (tree1 and tree2)
|
|
214
221
|
* by matching WIQL against allowed Source/Target types and optional area filters.
|
|
215
222
|
* Supports leaf queries of type:
|
|
216
|
-
* - oneHop (
|
|
223
|
+
* - oneHop (when includeOneHopQueries === true)
|
|
217
224
|
* - tree (when includeTreeQueries === true)
|
|
218
225
|
* - flat (when includeFlatQueries === true)
|
|
219
226
|
*
|
|
@@ -227,6 +234,7 @@ export default class TicketsDataProvider {
|
|
|
227
234
|
* @param includeTreeQueries - Include 'tree' queries in addition to 'oneHop'. Defaults to `false`.
|
|
228
235
|
* @param excludedFolderNames - Optional list of folder names to skip entirely (case-insensitive exact match).
|
|
229
236
|
* @param includeFlatQueries - Include 'flat' queries (matched by [System.WorkItemType] and [System.AreaPath]). Defaults to `false`.
|
|
237
|
+
* @param includeOneHopQueries - Include 'oneHop' queries. Defaults to `true` for legacy callers.
|
|
230
238
|
* @returns A promise resolving to an object with `tree1` and `tree2` nodes, or `null` for each when none match.
|
|
231
239
|
* @throws Logs an error if an exception occurs during processing.
|
|
232
240
|
*/
|
|
@@ -27,7 +27,7 @@ const HISTORICAL_WORK_ITEM_FIELDS = [
|
|
|
27
27
|
'Custom.TestPhase',
|
|
28
28
|
];
|
|
29
29
|
/** Default fields fetched per work item in tree/flat query parsing. */
|
|
30
|
-
const WI_DEFAULT_FIELDS = 'System.Description,System.Title,Microsoft.VSTS.TCM.ReproSteps,Microsoft.VSTS.CMMI.Symptom';
|
|
30
|
+
const WI_DEFAULT_FIELDS = 'System.Description,System.Title,System.WorkItemType,Microsoft.VSTS.TCM.ReproSteps,Microsoft.VSTS.CMMI.Symptom';
|
|
31
31
|
class TicketsDataProvider {
|
|
32
32
|
constructor(orgUrl, token) {
|
|
33
33
|
this.orgUrl = '';
|
|
@@ -524,6 +524,50 @@ class TicketsDataProvider {
|
|
|
524
524
|
excludedFolderNames, true);
|
|
525
525
|
return { systemRequirementsQueryTree };
|
|
526
526
|
}
|
|
527
|
+
/**
|
|
528
|
+
* Fetches flat Customer/System requirement queries for SysRS traceability.
|
|
529
|
+
* OneHop/tree queries are intentionally excluded for this picker.
|
|
530
|
+
*/
|
|
531
|
+
async fetchFlatUpstreamQueries(queries, excludedFolderNames = [], allowedTypes = ['requirement']) {
|
|
532
|
+
const { tree1: systemRequirementsQueryTree } = await this.structureFetchedQueries(queries, false, null, allowedTypes, [], undefined, undefined, false, excludedFolderNames, true, false);
|
|
533
|
+
return { systemRequirementsQueryTree };
|
|
534
|
+
}
|
|
535
|
+
async fetchCustomerRequirementQueries(queries) {
|
|
536
|
+
const systemRequirementsQueryTree = await this.structureCustomerRequirementQueries(queries);
|
|
537
|
+
return { systemRequirementsQueryTree };
|
|
538
|
+
}
|
|
539
|
+
async structureCustomerRequirementQueries(rootQuery, parentId = null) {
|
|
540
|
+
try {
|
|
541
|
+
if (!(rootQuery === null || rootQuery === void 0 ? void 0 : rootQuery.hasChildren)) {
|
|
542
|
+
const isExecutableQuery = !(rootQuery === null || rootQuery === void 0 ? void 0 : rootQuery.isFolder) && ['flat', 'tree', 'oneHop'].includes(rootQuery === null || rootQuery === void 0 ? void 0 : rootQuery.queryType);
|
|
543
|
+
return isExecutableQuery ? this.buildQueryNode(rootQuery, parentId) : null;
|
|
544
|
+
}
|
|
545
|
+
if (!rootQuery.children) {
|
|
546
|
+
const queryUrl = `${rootQuery.url}?$depth=2&$expand=all`;
|
|
547
|
+
const currentQuery = await tfs_1.TFSServices.getItemContent(queryUrl, this.token);
|
|
548
|
+
if (!currentQuery) {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
return await this.structureCustomerRequirementQueries(currentQuery, currentQuery.id);
|
|
552
|
+
}
|
|
553
|
+
const childResults = await Promise.all(rootQuery.children.map((child) => this.structureCustomerRequirementQueries(child, rootQuery.id)));
|
|
554
|
+
const children = childResults.filter((child) => child !== null);
|
|
555
|
+
return children.length > 0
|
|
556
|
+
? {
|
|
557
|
+
id: rootQuery.id,
|
|
558
|
+
pId: parentId,
|
|
559
|
+
value: rootQuery.name,
|
|
560
|
+
title: rootQuery.name,
|
|
561
|
+
children,
|
|
562
|
+
}
|
|
563
|
+
: null;
|
|
564
|
+
}
|
|
565
|
+
catch (err) {
|
|
566
|
+
logger_1.default.error(`Error occurred while constructing the customer query list ${err.message} with query ${JSON.stringify(rootQuery)}`);
|
|
567
|
+
logger_1.default.error(`Error stack ${err.message}`);
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
527
571
|
async fetchSrsQueries(rootQueries) {
|
|
528
572
|
const srsFolder = await this.findQueryFolderByName(rootQueries, 'srs');
|
|
529
573
|
if (!srsFolder) {
|
|
@@ -553,27 +597,30 @@ class TicketsDataProvider {
|
|
|
553
597
|
async fetchSysRsQueries(rootQueries) {
|
|
554
598
|
const { root: sysRsRoot, found: sysRsRootFound } = await this.getDocTypeRoot(rootQueries, 'sysrs');
|
|
555
599
|
logger_1.default.debug(`[GetSharedQueries][sysrs] using ${sysRsRootFound ? 'dedicated folder' : 'root queries'}`);
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
]);
|
|
560
|
-
const systemToCustomerFolder = await this.findChildFolderByPossibleNames(sysRsRoot, [
|
|
561
|
-
'system to customer',
|
|
562
|
-
'system-to-customer',
|
|
563
|
-
'system customer',
|
|
564
|
-
'subsystem to system',
|
|
565
|
-
'customer to system',
|
|
566
|
-
]);
|
|
567
|
-
const systemToSubsystemFolder = await this.findChildFolderByPossibleNames(sysRsRoot, [
|
|
568
|
-
'system to subsystem',
|
|
569
|
-
'system-to-subsystem',
|
|
570
|
-
'system subsystem',
|
|
571
|
-
]);
|
|
572
|
-
const subsystemToSystemRequirementsQueries = await this.fetchRequirementsTraceQueriesForFolder(systemToCustomerFolder);
|
|
600
|
+
const systemToSubsystemFolderNames = ['system to subsystem', 'system-to-subsystem', 'system subsystem'];
|
|
601
|
+
const systemRequirementsQueries = await this.fetchSystemRequirementQueries(sysRsRoot, systemToSubsystemFolderNames);
|
|
602
|
+
const systemToSubsystemFolder = await this.findChildFolderByPossibleNames(sysRsRoot, systemToSubsystemFolderNames);
|
|
573
603
|
const systemToSubsystemRequirementsQueries = await this.fetchRequirementsTraceQueriesForFolder(systemToSubsystemFolder);
|
|
604
|
+
// Customer/System requirements (traceability table) picker: scan the entire
|
|
605
|
+
// dedicated SysRS folder for executable query nodes. The selected query
|
|
606
|
+
// defines the customer-side candidate set, so discovery does not infer
|
|
607
|
+
// "customer" semantics from WIQL. Scope is intentionally limited to the
|
|
608
|
+
// dedicated `sysrs` folder so unrelated queries from the Shared Queries root
|
|
609
|
+
// do not leak into the picker.
|
|
610
|
+
let customerRequirementsQueries = null;
|
|
611
|
+
if (sysRsRootFound) {
|
|
612
|
+
customerRequirementsQueries = await this.fetchCustomerRequirementQueries(sysRsRoot);
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
logger_1.default.debug('[GetSharedQueries][sysrs] dedicated sysrs folder not found; skipping customer-requirements picker');
|
|
616
|
+
}
|
|
574
617
|
return {
|
|
575
618
|
systemRequirementsQueries,
|
|
576
|
-
|
|
619
|
+
customerRequirementsQueries,
|
|
620
|
+
// Legacy field retained for backward compatibility with callers/tests.
|
|
621
|
+
// The sub-system -> system trace table is out of scope for v0 and no
|
|
622
|
+
// longer sourced from a hardcoded "System to Customer" folder.
|
|
623
|
+
subsystemToSystemRequirementsQueries: null,
|
|
577
624
|
systemToSubsystemRequirementsQueries,
|
|
578
625
|
};
|
|
579
626
|
}
|
|
@@ -1752,21 +1799,25 @@ class TicketsDataProvider {
|
|
|
1752
1799
|
};
|
|
1753
1800
|
}
|
|
1754
1801
|
async PopulateWorkItemsByIds(workItemsArray = [], projectName = '') {
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
//
|
|
1802
|
+
const baseUrl = `${this.orgUrl}${projectName}/_apis/wit/workitemsbatch`;
|
|
1803
|
+
// On-prem Azure DevOps Server rejects this POST with HTTP 400 unless an
|
|
1804
|
+
// api-version is explicitly set on the URL. Use the same fallback chain
|
|
1805
|
+
// (7.1 -> 5.1 -> no version) the historical batch hydration already uses,
|
|
1806
|
+
// so the helper stays compatible with ADO cloud and older on-prem tenants.
|
|
1807
|
+
const postBatch = (currentIds) => this.withHistoricalApiVersionFallback('populate-workitems-batch', (apiVersion) => tfs_1.TFSServices.getItemContent(this.appendApiVersion(baseUrl, apiVersion), this.token, 'post', {
|
|
1808
|
+
$expand: 'Relations',
|
|
1809
|
+
ids: currentIds,
|
|
1810
|
+
})).then(({ result }) => result);
|
|
1811
|
+
const res = [];
|
|
1812
|
+
const divByMax = Math.floor(workItemsArray.length / 200);
|
|
1813
|
+
const modulusByMax = workItemsArray.length % 200;
|
|
1760
1814
|
for (let i = 0; i < divByMax; i++) {
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1815
|
+
const from = i * 200;
|
|
1816
|
+
const to = (i + 1) * 200;
|
|
1817
|
+
const currentIds = workItemsArray.slice(from, to);
|
|
1764
1818
|
try {
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
ids: currentIds,
|
|
1768
|
-
});
|
|
1769
|
-
res = [...res, ...subRes.value];
|
|
1819
|
+
const subRes = await postBatch(currentIds);
|
|
1820
|
+
res.push(...subRes.value);
|
|
1770
1821
|
}
|
|
1771
1822
|
catch (error) {
|
|
1772
1823
|
logger_1.default.error(`error populating workitems array`);
|
|
@@ -1774,22 +1825,18 @@ class TicketsDataProvider {
|
|
|
1774
1825
|
return [];
|
|
1775
1826
|
}
|
|
1776
1827
|
}
|
|
1777
|
-
//compliting the rimainder
|
|
1778
1828
|
if (modulusByMax !== 0) {
|
|
1779
1829
|
try {
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
ids: currentIds,
|
|
1784
|
-
});
|
|
1785
|
-
res = [...res, ...subRes.value];
|
|
1830
|
+
const currentIds = workItemsArray.slice(workItemsArray.length - modulusByMax, workItemsArray.length);
|
|
1831
|
+
const subRes = await postBatch(currentIds);
|
|
1832
|
+
res.push(...subRes.value);
|
|
1786
1833
|
}
|
|
1787
1834
|
catch (error) {
|
|
1788
1835
|
logger_1.default.error(`error populating workitems array`);
|
|
1789
1836
|
logger_1.default.error(JSON.stringify(error));
|
|
1790
1837
|
return [];
|
|
1791
1838
|
}
|
|
1792
|
-
}
|
|
1839
|
+
}
|
|
1793
1840
|
return res;
|
|
1794
1841
|
}
|
|
1795
1842
|
async GetModeledQueryResults(results, project) {
|
|
@@ -2013,7 +2060,7 @@ class TicketsDataProvider {
|
|
|
2013
2060
|
* Recursively structures fetched queries into two hierarchical trees (tree1 and tree2)
|
|
2014
2061
|
* by matching WIQL against allowed Source/Target types and optional area filters.
|
|
2015
2062
|
* Supports leaf queries of type:
|
|
2016
|
-
* - oneHop (
|
|
2063
|
+
* - oneHop (when includeOneHopQueries === true)
|
|
2017
2064
|
* - tree (when includeTreeQueries === true)
|
|
2018
2065
|
* - flat (when includeFlatQueries === true)
|
|
2019
2066
|
*
|
|
@@ -2027,10 +2074,11 @@ class TicketsDataProvider {
|
|
|
2027
2074
|
* @param includeTreeQueries - Include 'tree' queries in addition to 'oneHop'. Defaults to `false`.
|
|
2028
2075
|
* @param excludedFolderNames - Optional list of folder names to skip entirely (case-insensitive exact match).
|
|
2029
2076
|
* @param includeFlatQueries - Include 'flat' queries (matched by [System.WorkItemType] and [System.AreaPath]). Defaults to `false`.
|
|
2077
|
+
* @param includeOneHopQueries - Include 'oneHop' queries. Defaults to `true` for legacy callers.
|
|
2030
2078
|
* @returns A promise resolving to an object with `tree1` and `tree2` nodes, or `null` for each when none match.
|
|
2031
2079
|
* @throws Logs an error if an exception occurs during processing.
|
|
2032
2080
|
*/
|
|
2033
|
-
async structureFetchedQueries(rootQuery, onlyTestReq, parentId = null, sources, targets, sourceAreaFilter, targetAreaFilter, includeTreeQueries = false, excludedFolderNames = [], includeFlatQueries = false, workItemTypeCache) {
|
|
2081
|
+
async structureFetchedQueries(rootQuery, onlyTestReq, parentId = null, sources, targets, sourceAreaFilter, targetAreaFilter, includeTreeQueries = false, excludedFolderNames = [], includeFlatQueries = false, includeOneHopQueries = true, workItemTypeCache) {
|
|
2034
2082
|
try {
|
|
2035
2083
|
// Per-invocation cache for ID->WorkItemType lookups; avoids global state and is safe for concurrency.
|
|
2036
2084
|
const typeCache = workItemTypeCache !== null && workItemTypeCache !== void 0 ? workItemTypeCache : new Map();
|
|
@@ -2041,7 +2089,7 @@ class TicketsDataProvider {
|
|
|
2041
2089
|
}
|
|
2042
2090
|
if (!rootQuery.hasChildren) {
|
|
2043
2091
|
const isLeafCandidate = !rootQuery.isFolder &&
|
|
2044
|
-
(rootQuery.queryType === 'oneHop' ||
|
|
2092
|
+
((includeOneHopQueries && rootQuery.queryType === 'oneHop') ||
|
|
2045
2093
|
(includeTreeQueries && rootQuery.queryType === 'tree') ||
|
|
2046
2094
|
(includeFlatQueries && rootQuery.queryType === 'flat'));
|
|
2047
2095
|
if (isLeafCandidate) {
|
|
@@ -2101,10 +2149,10 @@ class TicketsDataProvider {
|
|
|
2101
2149
|
if (!currentQuery) {
|
|
2102
2150
|
return { tree1: null, tree2: null };
|
|
2103
2151
|
}
|
|
2104
|
-
return await this.structureFetchedQueries(currentQuery, onlyTestReq, currentQuery.id, sources, targets, sourceAreaFilter, targetAreaFilter, includeTreeQueries, excludedFolderNames, includeFlatQueries, typeCache);
|
|
2152
|
+
return await this.structureFetchedQueries(currentQuery, onlyTestReq, currentQuery.id, sources, targets, sourceAreaFilter, targetAreaFilter, includeTreeQueries, excludedFolderNames, includeFlatQueries, includeOneHopQueries, typeCache);
|
|
2105
2153
|
}
|
|
2106
2154
|
// Process children recursively
|
|
2107
|
-
const childResults = await Promise.all(rootQuery.children.map((child) => this.structureFetchedQueries(child, onlyTestReq, rootQuery.id, sources, targets, sourceAreaFilter, targetAreaFilter, includeTreeQueries, excludedFolderNames, includeFlatQueries, typeCache)));
|
|
2155
|
+
const childResults = await Promise.all(rootQuery.children.map((child) => this.structureFetchedQueries(child, onlyTestReq, rootQuery.id, sources, targets, sourceAreaFilter, targetAreaFilter, includeTreeQueries, excludedFolderNames, includeFlatQueries, includeOneHopQueries, typeCache)));
|
|
2108
2156
|
// Build tree1
|
|
2109
2157
|
const tree1Children = childResults.map((res) => res.tree1).filter((child) => child !== null);
|
|
2110
2158
|
const tree1Node = tree1Children.length > 0
|