@elisra-devops/docgen-data-provider 1.61.0 → 1.63.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.
@@ -9,11 +9,13 @@ const tfs_data_4 = require("../models/tfs-data");
9
9
  const tfs_data_5 = require("../models/tfs-data");
10
10
  const tfs_data_6 = require("../models/tfs-data");
11
11
  const logger_1 = require("../utils/logger");
12
+ const pLimit = require('p-limit');
12
13
  class TicketsDataProvider {
13
14
  constructor(orgUrl, token) {
14
15
  this.orgUrl = '';
15
16
  this.token = '';
16
17
  this.queriesList = new Array();
18
+ this.limit = pLimit(10);
17
19
  this.orgUrl = orgUrl;
18
20
  this.token = token;
19
21
  }
@@ -105,6 +107,7 @@ class TicketsDataProvider {
105
107
  * @returns
106
108
  */
107
109
  async GetSharedQueries(project, path, docType = '') {
110
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
108
111
  let url;
109
112
  try {
110
113
  if (path === '')
@@ -113,22 +116,166 @@ class TicketsDataProvider {
113
116
  url = `${this.orgUrl}${project}/_apis/wit/queries/${path}?$depth=2&$expand=all`;
114
117
  let queries = await tfs_1.TFSServices.getItemContent(url, this.token);
115
118
  logger_1.default.debug(`doctype: ${docType}`);
116
- switch (docType === null || docType === void 0 ? void 0 : docType.toLowerCase()) {
117
- case 'std':
118
- const reqTestQueries = await this.fetchLinkedReqTestQueries(queries, false);
119
- const linkedMomQueries = await this.fetchLinkedMomQueries(queries);
119
+ const normalizedDocType = (docType || '').toLowerCase();
120
+ const queriesWithChildren = await this.ensureQueryChildren(queries);
121
+ switch (normalizedDocType) {
122
+ case 'std': {
123
+ const { root: stdRoot, found: stdRootFound } = await this.getDocTypeRoot(queriesWithChildren, 'std');
124
+ logger_1.default.debug(`[GetSharedQueries][std] using ${stdRootFound ? 'dedicated folder' : 'root queries'}`);
125
+ // Each branch describes the dedicated folder names, the fetch routine, and how to validate results.
126
+ const stdBranches = await this.fetchDocTypeBranches(queriesWithChildren, stdRoot, [
127
+ {
128
+ id: 'reqToTest',
129
+ label: '[GetSharedQueries][std][req-to-test]',
130
+ folderNames: [
131
+ 'requirement - test',
132
+ 'requirement to test case',
133
+ 'requirement to test',
134
+ 'req to test',
135
+ ],
136
+ fetcher: (folder) => this.fetchLinkedReqTestQueries(folder, false),
137
+ validator: (result) => this.hasAnyQueryTree(result === null || result === void 0 ? void 0 : result.reqTestTree),
138
+ },
139
+ {
140
+ id: 'testToReq',
141
+ label: '[GetSharedQueries][std][test-to-req]',
142
+ folderNames: [
143
+ 'test - requirement',
144
+ 'test to requirement',
145
+ 'test case to requirement',
146
+ 'test to req',
147
+ ],
148
+ fetcher: (folder) => this.fetchLinkedReqTestQueries(folder, true),
149
+ validator: (result) => this.hasAnyQueryTree(result === null || result === void 0 ? void 0 : result.testReqTree),
150
+ },
151
+ {
152
+ id: 'mom',
153
+ label: '[GetSharedQueries][std][mom]',
154
+ folderNames: ['linked mom', 'mom'],
155
+ fetcher: (folder) => this.fetchLinkedMomQueries(folder),
156
+ validator: (result) => this.hasAnyQueryTree(result === null || result === void 0 ? void 0 : result.linkedMomTree),
157
+ },
158
+ ]);
159
+ const reqToTestResult = stdBranches['reqToTest'];
160
+ const testToReqResult = stdBranches['testToReq'];
161
+ const momResult = stdBranches['mom'];
162
+ const reqTestQueries = {
163
+ reqTestTree: (_b = (_a = reqToTestResult === null || reqToTestResult === void 0 ? void 0 : reqToTestResult.result) === null || _a === void 0 ? void 0 : _a.reqTestTree) !== null && _b !== void 0 ? _b : null,
164
+ testReqTree: (_f = (_d = (_c = testToReqResult === null || testToReqResult === void 0 ? void 0 : testToReqResult.result) === null || _c === void 0 ? void 0 : _c.testReqTree) !== null && _d !== void 0 ? _d : (_e = reqToTestResult === null || reqToTestResult === void 0 ? void 0 : reqToTestResult.result) === null || _e === void 0 ? void 0 : _e.testReqTree) !== null && _f !== void 0 ? _f : null,
165
+ };
166
+ const linkedMomQueries = {
167
+ linkedMomTree: (_h = (_g = momResult === null || momResult === void 0 ? void 0 : momResult.result) === null || _g === void 0 ? void 0 : _g.linkedMomTree) !== null && _h !== void 0 ? _h : null,
168
+ };
120
169
  return { reqTestQueries, linkedMomQueries };
121
- case 'str':
122
- const reqTestTrees = await this.fetchLinkedReqTestQueries(queries, false);
123
- const openPcrTestTrees = await this.fetchLinkedOpenPcrTestQueries(queries, false);
170
+ }
171
+ case 'str': {
172
+ const { root: strRoot, found: strRootFound } = await this.getDocTypeRoot(queriesWithChildren, 'str');
173
+ logger_1.default.debug(`[GetSharedQueries][str] using ${strRootFound ? 'dedicated folder' : 'root queries'}`);
174
+ const strBranches = await this.fetchDocTypeBranches(queriesWithChildren, strRoot, [
175
+ {
176
+ id: 'reqToTest',
177
+ label: '[GetSharedQueries][str][req-to-test]',
178
+ folderNames: [
179
+ 'requirement - test',
180
+ 'requirement to test case',
181
+ 'requirement to test',
182
+ 'req to test',
183
+ ],
184
+ fetcher: (folder) => this.fetchLinkedReqTestQueries(folder, false),
185
+ validator: (result) => this.hasAnyQueryTree(result === null || result === void 0 ? void 0 : result.reqTestTree),
186
+ },
187
+ {
188
+ id: 'testToReq',
189
+ label: '[GetSharedQueries][str][test-to-req]',
190
+ folderNames: [
191
+ 'test - requirement',
192
+ 'test to requirement',
193
+ 'test case to requirement',
194
+ 'test to req',
195
+ ],
196
+ fetcher: (folder) => this.fetchLinkedReqTestQueries(folder, true),
197
+ validator: (result) => this.hasAnyQueryTree(result === null || result === void 0 ? void 0 : result.testReqTree),
198
+ },
199
+ {
200
+ id: 'openPcrToTest',
201
+ label: '[GetSharedQueries][str][open-pcr-to-test]',
202
+ folderNames: ['open pcr to test case', 'open pcr to test', 'open pcr - test', 'open pcr'],
203
+ fetcher: (folder) => this.fetchLinkedOpenPcrTestQueries(folder, false),
204
+ validator: (result) => this.hasAnyQueryTree(result === null || result === void 0 ? void 0 : result.OpenPcrToTestTree),
205
+ },
206
+ {
207
+ id: 'testToOpenPcr',
208
+ label: '[GetSharedQueries][str][test-to-open-pcr]',
209
+ folderNames: ['test case to open pcr', 'test to open pcr', 'test - open pcr', 'open pcr'],
210
+ fetcher: (folder) => this.fetchLinkedOpenPcrTestQueries(folder, true),
211
+ validator: (result) => this.hasAnyQueryTree(result === null || result === void 0 ? void 0 : result.TestToOpenPcrTree),
212
+ },
213
+ ]);
214
+ const strReqToTest = strBranches['reqToTest'];
215
+ const strTestToReq = strBranches['testToReq'];
216
+ const strOpenPcrToTest = strBranches['openPcrToTest'];
217
+ const strTestToOpenPcr = strBranches['testToOpenPcr'];
218
+ const reqTestTrees = {
219
+ reqTestTree: (_k = (_j = strReqToTest === null || strReqToTest === void 0 ? void 0 : strReqToTest.result) === null || _j === void 0 ? void 0 : _j.reqTestTree) !== null && _k !== void 0 ? _k : null,
220
+ testReqTree: (_p = (_m = (_l = strTestToReq === null || strTestToReq === void 0 ? void 0 : strTestToReq.result) === null || _l === void 0 ? void 0 : _l.testReqTree) !== null && _m !== void 0 ? _m : (_o = strReqToTest === null || strReqToTest === void 0 ? void 0 : strReqToTest.result) === null || _o === void 0 ? void 0 : _o.testReqTree) !== null && _p !== void 0 ? _p : null,
221
+ };
222
+ const openPcrTestTrees = {
223
+ OpenPcrToTestTree: (_r = (_q = strOpenPcrToTest === null || strOpenPcrToTest === void 0 ? void 0 : strOpenPcrToTest.result) === null || _q === void 0 ? void 0 : _q.OpenPcrToTestTree) !== null && _r !== void 0 ? _r : null,
224
+ TestToOpenPcrTree: (_v = (_t = (_s = strTestToOpenPcr === null || strTestToOpenPcr === void 0 ? void 0 : strTestToOpenPcr.result) === null || _s === void 0 ? void 0 : _s.TestToOpenPcrTree) !== null && _t !== void 0 ? _t : (_u = strOpenPcrToTest === null || strOpenPcrToTest === void 0 ? void 0 : strOpenPcrToTest.result) === null || _u === void 0 ? void 0 : _u.TestToOpenPcrTree) !== null && _v !== void 0 ? _v : null,
225
+ };
124
226
  return { reqTestTrees, openPcrTestTrees };
125
- case 'test-reporter':
126
- const testAssociatedTree = await this.fetchTestReporterQueries(queries);
127
- return { testAssociatedTree };
227
+ }
228
+ case 'test-reporter': {
229
+ const { root: testReporterRoot, found: testReporterFound } = await this.getDocTypeRoot(queriesWithChildren, 'test-reporter');
230
+ logger_1.default.debug(`[GetSharedQueries][test-reporter] using ${testReporterFound ? 'dedicated folder' : 'root queries'}`);
231
+ const testReporterBranches = await this.fetchDocTypeBranches(queriesWithChildren, testReporterRoot, [
232
+ {
233
+ id: 'testReporter',
234
+ label: '[GetSharedQueries][test-reporter]',
235
+ folderNames: ['test reporter', 'test-reporter'],
236
+ fetcher: (folder) => this.fetchTestReporterQueries(folder),
237
+ validator: (result) => this.hasAnyQueryTree(result === null || result === void 0 ? void 0 : result.testAssociatedTree),
238
+ },
239
+ ]);
240
+ const testReporterFetch = testReporterBranches['testReporter'];
241
+ return (_w = testReporterFetch === null || testReporterFetch === void 0 ? void 0 : testReporterFetch.result) !== null && _w !== void 0 ? _w : { testAssociatedTree: null };
242
+ }
128
243
  case 'srs':
129
- return await this.fetchSrsQueries(queries);
130
- case 'svd':
131
- return await this.fetchAnyQueries(queries);
244
+ return await this.fetchSrsQueries(queriesWithChildren);
245
+ case 'svd': {
246
+ const { root: svdRoot, found } = await this.getDocTypeRoot(queriesWithChildren, 'svd');
247
+ if (!found) {
248
+ logger_1.default.debug('[GetSharedQueries][svd] dedicated folder not found, using fallback tree');
249
+ }
250
+ const svdBranches = await this.fetchDocTypeBranches(queriesWithChildren, svdRoot, [
251
+ {
252
+ id: 'systemOverview',
253
+ label: '[GetSharedQueries][svd][system-overview]',
254
+ folderNames: ['system overview'],
255
+ fetcher: async (folder) => {
256
+ const { tree1 } = await this.structureAllQueryPath(folder);
257
+ return tree1;
258
+ },
259
+ validator: (result) => !!result,
260
+ },
261
+ {
262
+ id: 'knownBugs',
263
+ label: '[GetSharedQueries][svd][known-bugs]',
264
+ folderNames: ['known bugs', 'known bug'],
265
+ fetcher: async (folder) => {
266
+ const { tree2 } = await this.structureAllQueryPath(folder);
267
+ return tree2;
268
+ },
269
+ validator: (result) => !!result,
270
+ },
271
+ ]);
272
+ const systemOverviewFetch = svdBranches['systemOverview'];
273
+ const knownBugsFetch = svdBranches['knownBugs'];
274
+ return {
275
+ systemOverviewQueryTree: (_x = systemOverviewFetch === null || systemOverviewFetch === void 0 ? void 0 : systemOverviewFetch.result) !== null && _x !== void 0 ? _x : null,
276
+ knownBugsQueryTree: (_y = knownBugsFetch === null || knownBugsFetch === void 0 ? void 0 : knownBugsFetch.result) !== null && _y !== void 0 ? _y : null,
277
+ };
278
+ }
132
279
  default:
133
280
  break;
134
281
  }
@@ -203,6 +350,30 @@ class TicketsDataProvider {
203
350
  ]);
204
351
  return { linkedMomTree };
205
352
  }
353
+ hasAnyQueryTree(result) {
354
+ const inspect = (value) => {
355
+ if (!value) {
356
+ return false;
357
+ }
358
+ if (Array.isArray(value)) {
359
+ return value.some(inspect);
360
+ }
361
+ if (typeof value === 'object') {
362
+ if (value.isValidQuery || value.wiql || value.queryType) {
363
+ return true;
364
+ }
365
+ if ('roots' in value && Array.isArray(value.roots) && value.roots.length > 0) {
366
+ return true;
367
+ }
368
+ if ('children' in value && Array.isArray(value.children) && value.children.length > 0) {
369
+ return true;
370
+ }
371
+ return Object.values(value).some(inspect);
372
+ }
373
+ return false;
374
+ };
375
+ return inspect(result);
376
+ }
206
377
  /**
207
378
  * Fetches and structures linked queries related to open PCR (Problem Change Request) tests.
208
379
  *
@@ -229,10 +400,6 @@ class TicketsDataProvider {
229
400
  const { tree1: tree1, tree2: testAssociatedTree } = await this.structureFetchedQueries(queries, true, null, ['Requirement', 'Bug', 'Change Request'], ['Test Case']);
230
401
  return { testAssociatedTree };
231
402
  }
232
- async fetchAnyQueries(queries) {
233
- const { tree1: systemOverviewQueryTree, tree2: knownBugsQueryTree } = await this.structureAllQueryPath(queries);
234
- return { systemOverviewQueryTree, knownBugsQueryTree };
235
- }
236
403
  async fetchSystemRequirementQueries(queries, excludedFolderNames = []) {
237
404
  const { tree1: systemRequirementsQueryTree } = await this.structureFetchedQueries(queries, false, null, ['Epic', 'Feature', 'Requirement'], [], undefined, undefined, true, // Enable processing of both tree and direct link queries (excluding flat queries)
238
405
  excludedFolderNames);
@@ -328,6 +495,219 @@ class TicketsDataProvider {
328
495
  const normalizedName = childName.toLowerCase();
329
496
  return (parentWithChildren.children.find((child) => child.isFolder && (child.name || '').toLowerCase() === normalizedName) || null);
330
497
  }
498
+ /**
499
+ * Performs a breadth-first walk starting at `parent` to locate the nearest folder whose
500
+ * name matches any of the provided candidates (case-insensitive). Exact matches win; if none
501
+ * are found the first partial match encountered is returned. When no candidates are located,
502
+ * the method yields `null`.
503
+ */
504
+ async findChildFolderByPossibleNames(parent, possibleNames) {
505
+ var _a, _b, _c, _d;
506
+ if (!parent || !(possibleNames === null || possibleNames === void 0 ? void 0 : possibleNames.length)) {
507
+ return null;
508
+ }
509
+ const normalizedNames = possibleNames.map((name) => name.toLowerCase());
510
+ const isMatch = (candidate, value) => value === candidate;
511
+ const isPartialMatch = (candidate, value) => value.includes(candidate);
512
+ const tryMatch = (folder, matcher) => {
513
+ const folderName = ((folder === null || folder === void 0 ? void 0 : folder.name) || '').toLowerCase();
514
+ return normalizedNames.some((candidate) => matcher(candidate, folderName));
515
+ };
516
+ const parentWithChildren = await this.ensureQueryChildren(parent);
517
+ if (!((_a = parentWithChildren === null || parentWithChildren === void 0 ? void 0 : parentWithChildren.children) === null || _a === void 0 ? void 0 : _a.length)) {
518
+ return null;
519
+ }
520
+ const queue = [];
521
+ const visited = new Set();
522
+ let partialCandidate = null;
523
+ // Seed the queue with direct children so we prefer closer matches before walking deeper.
524
+ for (const child of parentWithChildren.children) {
525
+ if (!(child === null || child === void 0 ? void 0 : child.isFolder)) {
526
+ continue;
527
+ }
528
+ const childId = (_b = child.id) !== null && _b !== void 0 ? _b : `${child.name}-${Math.random()}`;
529
+ queue.push(child);
530
+ visited.add(childId);
531
+ }
532
+ const considerFolder = async (folder) => {
533
+ if (tryMatch(folder, isMatch)) {
534
+ return await this.ensureQueryChildren(folder);
535
+ }
536
+ if (!partialCandidate && tryMatch(folder, isPartialMatch)) {
537
+ partialCandidate = await this.ensureQueryChildren(folder);
538
+ }
539
+ return null;
540
+ };
541
+ for (const child of queue) {
542
+ const match = await considerFolder(child);
543
+ if (match) {
544
+ return match;
545
+ }
546
+ }
547
+ while (queue.length > 0) {
548
+ const current = queue.shift();
549
+ if (!current) {
550
+ continue;
551
+ }
552
+ const currentWithChildren = await this.ensureQueryChildren(current);
553
+ if (!currentWithChildren) {
554
+ continue;
555
+ }
556
+ const match = await considerFolder(currentWithChildren);
557
+ if (match) {
558
+ return match;
559
+ }
560
+ // Breadth-first expansion so we climb the hierarchy gradually.
561
+ if ((_c = currentWithChildren.children) === null || _c === void 0 ? void 0 : _c.length) {
562
+ for (const child of currentWithChildren.children) {
563
+ if (!(child === null || child === void 0 ? void 0 : child.isFolder)) {
564
+ continue;
565
+ }
566
+ const childId = (_d = child.id) !== null && _d !== void 0 ? _d : `${child.name}-${Math.random()}`;
567
+ if (!visited.has(childId)) {
568
+ visited.add(childId);
569
+ queue.push(child);
570
+ }
571
+ }
572
+ }
573
+ }
574
+ return partialCandidate;
575
+ }
576
+ /**
577
+ * Executes `fetcher` against `startingFolder` and, if the validator deems the result empty,
578
+ * climbs ancestor folders toward `rootQueries` until a satisfactory result is produced.
579
+ * The first successful folder short-circuits the search; otherwise the final attempt is
580
+ * returned to preserve legacy behavior.
581
+ */
582
+ async fetchWithAncestorFallback(rootQueries, startingFolder, fetcher, logContext, validator) {
583
+ var _a;
584
+ const rootWithChildren = await this.ensureQueryChildren(rootQueries);
585
+ const candidates = await this.buildFallbackChain(rootWithChildren, startingFolder);
586
+ const evaluate = validator !== null && validator !== void 0 ? validator : ((res) => this.hasAnyQueryTree(res));
587
+ let lastResult = null;
588
+ let lastFolder = startingFolder !== null && startingFolder !== void 0 ? startingFolder : rootWithChildren;
589
+ for (const candidate of candidates) {
590
+ const enrichedCandidate = await this.ensureQueryChildren(candidate);
591
+ const candidateName = (_a = enrichedCandidate === null || enrichedCandidate === void 0 ? void 0 : enrichedCandidate.name) !== null && _a !== void 0 ? _a : '<root>';
592
+ logger_1.default.debug(`${logContext} trying folder: ${candidateName}`);
593
+ lastResult = await fetcher(enrichedCandidate);
594
+ lastFolder = enrichedCandidate;
595
+ if (evaluate(lastResult)) {
596
+ logger_1.default.debug(`${logContext} using folder: ${candidateName}`);
597
+ return { result: lastResult, usedFolder: enrichedCandidate };
598
+ }
599
+ logger_1.default.debug(`${logContext} folder ${candidateName} produced no results, ascending`);
600
+ }
601
+ logger_1.default.debug(`${logContext} no folders yielded results, returning last attempt`);
602
+ return { result: lastResult, usedFolder: lastFolder };
603
+ }
604
+ /**
605
+ * Applies `fetchWithAncestorFallback` to each configured branch, resolving dedicated folders
606
+ * when available and emitting a map keyed by branch id. Each outcome includes both the
607
+ * resulting payload and the specific folder that satisfied the fallback chain.
608
+ */
609
+ async fetchDocTypeBranches(queriesWithChildren, docRoot, branches) {
610
+ var _a, _b, _c, _d, _e, _f;
611
+ const results = {};
612
+ const effectiveDocRoot = docRoot !== null && docRoot !== void 0 ? docRoot : queriesWithChildren;
613
+ for (const branch of branches) {
614
+ const fallbackStart = (_a = branch.fallbackStart) !== null && _a !== void 0 ? _a : effectiveDocRoot;
615
+ let startingFolder = fallbackStart;
616
+ let startingName = (_b = startingFolder === null || startingFolder === void 0 ? void 0 : startingFolder.name) !== null && _b !== void 0 ? _b : '<root>';
617
+ // Attempt to locate a more specific child folder, falling back to the provided root if absent.
618
+ if (((_c = branch.folderNames) === null || _c === void 0 ? void 0 : _c.length) && effectiveDocRoot) {
619
+ const resolvedFolder = await this.findChildFolderByPossibleNames(effectiveDocRoot, branch.folderNames);
620
+ if (resolvedFolder) {
621
+ startingFolder = resolvedFolder;
622
+ startingName = (_d = resolvedFolder === null || resolvedFolder === void 0 ? void 0 : resolvedFolder.name) !== null && _d !== void 0 ? _d : '<root>';
623
+ }
624
+ }
625
+ logger_1.default.debug(`${branch.label} starting folder: ${startingName}`);
626
+ const fetchOutcome = await this.fetchWithAncestorFallback(queriesWithChildren, startingFolder, branch.fetcher, branch.label, branch.validator);
627
+ logger_1.default.debug(`${branch.label} final folder: ${(_f = (_e = fetchOutcome.usedFolder) === null || _e === void 0 ? void 0 : _e.name) !== null && _f !== void 0 ? _f : '<root>'}`);
628
+ results[branch.id] = fetchOutcome;
629
+ }
630
+ return results;
631
+ }
632
+ /**
633
+ * Constructs an ordered list of folders to probe during fallback. The sequence starts at
634
+ * `startingFolder` (if provided) and walks upward through ancestors to the root query tree,
635
+ * ensuring no folder id appears twice.
636
+ */
637
+ async buildFallbackChain(rootQueries, startingFolder) {
638
+ const chain = [];
639
+ const seen = new Set();
640
+ const pushUnique = (node) => {
641
+ var _a;
642
+ if (!node) {
643
+ return;
644
+ }
645
+ const id = (_a = node.id) !== null && _a !== void 0 ? _a : '__root__';
646
+ if (seen.has(id)) {
647
+ return;
648
+ }
649
+ seen.add(id);
650
+ chain.push(node);
651
+ };
652
+ if (startingFolder === null || startingFolder === void 0 ? void 0 : startingFolder.id) {
653
+ const path = await this.findPathToNode(rootQueries, startingFolder.id);
654
+ if (path) {
655
+ for (let i = path.length - 1; i >= 0; i--) {
656
+ pushUnique(path[i]);
657
+ }
658
+ }
659
+ else {
660
+ pushUnique(startingFolder);
661
+ }
662
+ }
663
+ else if (startingFolder) {
664
+ pushUnique(startingFolder);
665
+ }
666
+ pushUnique(rootQueries);
667
+ return chain;
668
+ }
669
+ /**
670
+ * Recursively searches the query tree for the node with the provided id and returns the
671
+ * path (root → target). Nodes are enriched with children on demand and a visited set guards
672
+ * against cycles within malformed data.
673
+ */
674
+ async findPathToNode(currentNode, targetId, visited = new Set()) {
675
+ var _a;
676
+ if (!currentNode) {
677
+ return null;
678
+ }
679
+ const currentId = (_a = currentNode.id) !== null && _a !== void 0 ? _a : '__root__';
680
+ if (visited.has(currentId)) {
681
+ return null;
682
+ }
683
+ visited.add(currentId);
684
+ if (currentNode.id === targetId) {
685
+ return [currentNode];
686
+ }
687
+ const enrichedNode = await this.ensureQueryChildren(currentNode);
688
+ const children = enrichedNode === null || enrichedNode === void 0 ? void 0 : enrichedNode.children;
689
+ if (!(children === null || children === void 0 ? void 0 : children.length)) {
690
+ return null;
691
+ }
692
+ for (const child of children) {
693
+ const path = await this.findPathToNode(child, targetId, visited);
694
+ if (path) {
695
+ return [enrichedNode, ...path];
696
+ }
697
+ }
698
+ return null;
699
+ }
700
+ async getDocTypeRoot(rootQueries, docTypeName) {
701
+ if (!rootQueries) {
702
+ return { root: rootQueries, found: false };
703
+ }
704
+ const docTypeFolder = await this.findQueryFolderByName(rootQueries, docTypeName);
705
+ if (docTypeFolder) {
706
+ const folderWithChildren = await this.ensureQueryChildren(docTypeFolder);
707
+ return { root: folderWithChildren, found: true };
708
+ }
709
+ return { root: rootQueries, found: false };
710
+ }
331
711
  async ensureQueryChildren(node) {
332
712
  if (!node || !node.hasChildren || node.children) {
333
713
  return node;
@@ -392,29 +772,74 @@ class TicketsDataProvider {
392
772
  const sourceTargetsMap = new Map();
393
773
  const lookupMap = new Map();
394
774
  if (workItemRelations) {
775
+ // Step 1: Collect all unique work item IDs that need to be fetched
776
+ const sourceIds = new Set();
777
+ const targetIds = new Set();
778
+ for (const relation of workItemRelations) {
779
+ if (!relation.source) {
780
+ // Root link - target is actually the source
781
+ sourceIds.add(relation.target.id);
782
+ }
783
+ else {
784
+ sourceIds.add(relation.source.id);
785
+ if (relation.target) {
786
+ targetIds.add(relation.target.id);
787
+ }
788
+ }
789
+ }
790
+ // Step 2: Fetch all work items in parallel with concurrency limit
791
+ const allSourcePromises = Array.from(sourceIds).map((id) => this.limit(() => {
792
+ const relation = workItemRelations.find((r) => { var _a; return (!r.source && r.target.id === id) || ((_a = r.source) === null || _a === void 0 ? void 0 : _a.id) === id; });
793
+ return this.fetchWIForQueryResult(relation, columnsToShowMap, columnSourceMap, true);
794
+ }));
795
+ const allTargetPromises = Array.from(targetIds).map((id) => this.limit(() => {
796
+ const relation = workItemRelations.find((r) => { var _a; return ((_a = r.target) === null || _a === void 0 ? void 0 : _a.id) === id; });
797
+ return this.fetchWIForQueryResult(relation, columnsToShowMap, columnTargetsMap, true);
798
+ }));
799
+ // Wait for all fetches to complete in parallel (with concurrency control)
800
+ const [sourceWorkItems, targetWorkItems] = await Promise.all([
801
+ Promise.all(allSourcePromises),
802
+ Promise.all(allTargetPromises),
803
+ ]);
804
+ // Build lookup maps
805
+ const sourceWorkItemMap = new Map();
806
+ sourceWorkItems.forEach((wi) => {
807
+ sourceWorkItemMap.set(wi.id, wi);
808
+ if (!lookupMap.has(wi.id)) {
809
+ lookupMap.set(wi.id, wi);
810
+ }
811
+ });
812
+ const targetWorkItemMap = new Map();
813
+ targetWorkItems.forEach((wi) => {
814
+ targetWorkItemMap.set(wi.id, wi);
815
+ if (!lookupMap.has(wi.id)) {
816
+ lookupMap.set(wi.id, wi);
817
+ }
818
+ });
819
+ // Step 3: Build the sourceTargetsMap using the fetched work items
395
820
  for (const relation of workItemRelations) {
396
- //if relation.Source is null and target has a valid value then the target is the source
397
821
  if (!relation.source) {
398
822
  // Root link
399
- const wi = await this.fetchWIForQueryResult(relation, columnsToShowMap, columnSourceMap, true);
400
- if (!lookupMap.has(wi.id)) {
823
+ const wi = sourceWorkItemMap.get(relation.target.id);
824
+ if (wi && !sourceTargetsMap.has(wi)) {
401
825
  sourceTargetsMap.set(wi, []);
402
- lookupMap.set(wi.id, wi);
403
826
  }
404
- continue; // Move to the next relation
827
+ continue;
405
828
  }
406
829
  if (!relation.target) {
407
830
  throw new Error('Target relation is missing');
408
831
  }
409
- // Get relation source from lookup
410
- const sourceWorkItem = lookupMap.get(relation.source.id);
832
+ const sourceWorkItem = sourceWorkItemMap.get(relation.source.id);
411
833
  if (!sourceWorkItem) {
412
834
  throw new Error('Source relation has no mapping');
413
835
  }
414
- const targetWi = await this.fetchWIForQueryResult(relation, columnsToShowMap, columnTargetsMap, true);
415
- //In case if source is a test case
836
+ const targetWi = targetWorkItemMap.get(relation.target.id);
837
+ if (!targetWi) {
838
+ throw new Error('Target work item not found');
839
+ }
840
+ // In case if source is a test case
416
841
  this.mapTestCaseToRelatedItem(sourceWorkItem, targetWi, testCaseToRelatedWiMap);
417
- //In case of target is a test case
842
+ // In case of target is a test case
418
843
  this.mapTestCaseToRelatedItem(targetWi, sourceWorkItem, testCaseToRelatedWiMap);
419
844
  const targets = sourceTargetsMap.get(sourceWorkItem) || [];
420
845
  targets.push(targetWi);
@@ -460,13 +885,12 @@ class TicketsDataProvider {
460
885
  columnsToShowMap.set(referenceName, name);
461
886
  }
462
887
  });
463
- // Initialize maps
888
+ // Fetch all work items in parallel with concurrency limit
464
889
  const wiSet = new Set();
465
890
  if (workItems) {
466
- for (const workItem of workItems) {
467
- const wi = await this.fetchWIForQueryResult(workItem, columnsToShowMap, fieldsToIncludeMap, false);
468
- wiSet.add(wi);
469
- }
891
+ const fetchPromises = workItems.map((workItem) => this.limit(() => this.fetchWIForQueryResult(workItem, columnsToShowMap, fieldsToIncludeMap, false)));
892
+ const fetchedWorkItems = await Promise.all(fetchPromises);
893
+ fetchedWorkItems.forEach((wi) => wiSet.add(wi));
470
894
  }
471
895
  columnsToShowMap.clear();
472
896
  return {
@@ -1238,7 +1662,6 @@ class TicketsDataProvider {
1238
1662
  logger_1.default.warn('No work item IDs extracted from query result');
1239
1663
  return { categories: {}, totalCount: 0 };
1240
1664
  }
1241
- logger_1.default.debug(`Found ${workItemIds.length} work items to categorize`);
1242
1665
  // Define the mapping from requirement type keys to standard headers
1243
1666
  const typeToHeaderMap = {
1244
1667
  Adaptation: 'Adaptation Requirements',
@@ -1289,25 +1712,20 @@ class TicketsDataProvider {
1289
1712
  for (const workItemId of workItemIds) {
1290
1713
  try {
1291
1714
  // Fetch full work item with all fields
1292
- const wiUrl = `${this.orgUrl}_apis/wit/workitems/${workItemId}?$expand=All&api-version=6.0`;
1715
+ const wiUrl = `${this.orgUrl}_apis/wit/workitems/${workItemId}?$expand=All`;
1293
1716
  const fullWi = await tfs_1.TFSServices.getItemContent(wiUrl, this.token);
1294
1717
  // Check if it's a Requirement work item type
1295
1718
  const workItemType = fullWi.fields['System.WorkItemType'];
1296
- logger_1.default.debug(`Work item ${workItemId} type: ${workItemType}`);
1297
1719
  if (workItemType !== 'Requirement') {
1298
- logger_1.default.debug(`Skipping work item ${workItemId} - not a Requirement type`);
1299
1720
  continue; // Skip non-requirement work items
1300
1721
  }
1301
- // Get the requirement type field (check both possible reference names)
1302
- const rawRequirementType = fullWi.fields['Custom.Requirement_Type'] ||
1303
- fullWi.fields['Microsoft.VSTS.CMMI.RequirementType'] ||
1304
- '';
1305
- logger_1.default.debug(`Work item ${workItemId} requirement type: "${rawRequirementType}"`);
1722
+ // Get the requirement type field
1723
+ const rawRequirementType = fullWi.fields['Microsoft.VSTS.CMMI.RequirementType'] || '';
1306
1724
  // Normalize and trim the requirement type
1307
1725
  const trimmedType = String(rawRequirementType).trim();
1308
- // Map to standard header or use "Other Requirements" as default
1309
- const categoryHeader = trimmedType
1310
- ? typeToHeaderMap[trimmedType] || 'Other Requirements'
1726
+ // Map to the standard category header
1727
+ const categoryHeader = typeToHeaderMap[trimmedType]
1728
+ ? typeToHeaderMap[trimmedType]
1311
1729
  : 'Other Requirements';
1312
1730
  // Create the requirement object
1313
1731
  const requirementItem = {