@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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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(
|
|
130
|
-
case 'svd':
|
|
131
|
-
|
|
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 =
|
|
400
|
-
if (!
|
|
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;
|
|
827
|
+
continue;
|
|
405
828
|
}
|
|
406
829
|
if (!relation.target) {
|
|
407
830
|
throw new Error('Target relation is missing');
|
|
408
831
|
}
|
|
409
|
-
|
|
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 =
|
|
415
|
-
|
|
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
|
-
//
|
|
888
|
+
// Fetch all work items in parallel with concurrency limit
|
|
464
889
|
const wiSet = new Set();
|
|
465
890
|
if (workItems) {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
|
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
|
|
1302
|
-
const rawRequirementType = fullWi.fields['
|
|
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
|
|
1309
|
-
const categoryHeader = trimmedType
|
|
1310
|
-
? typeToHeaderMap[trimmedType]
|
|
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 = {
|