@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.
@@ -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 (always)
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 systemRequirementsQueries = await this.fetchSystemRequirementQueries(sysRsRoot, [
557
- 'System To Customer',
558
- 'System To Subsystem',
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
- subsystemToSystemRequirementsQueries,
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
- let url = `${this.orgUrl}${projectName}/_apis/wit/workitemsbatch`;
1756
- let res = [];
1757
- let divByMax = Math.floor(workItemsArray.length / 200);
1758
- let modulusByMax = workItemsArray.length % 200;
1759
- //iterating
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
- let from = i * 200;
1762
- let to = (i + 1) * 200;
1763
- let currentIds = workItemsArray.slice(from, to);
1815
+ const from = i * 200;
1816
+ const to = (i + 1) * 200;
1817
+ const currentIds = workItemsArray.slice(from, to);
1764
1818
  try {
1765
- let subRes = await tfs_1.TFSServices.getItemContent(url, this.token, 'post', {
1766
- $expand: 'Relations',
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
- let currentIds = workItemsArray.slice(workItemsArray.length - modulusByMax, workItemsArray.length);
1781
- let subRes = await tfs_1.TFSServices.getItemContent(url, this.token, 'post', {
1782
- $expand: 'Relations',
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
- } //if
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 (always)
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