@elisra-devops/docgen-data-provider 1.62.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.
|
@@ -10,6 +10,23 @@ import { value } from '../models/tfs-data';
|
|
|
10
10
|
import logger from '../utils/logger';
|
|
11
11
|
const pLimit = require('p-limit');
|
|
12
12
|
|
|
13
|
+
type FallbackFetchOutcome = {
|
|
14
|
+
result: any;
|
|
15
|
+
usedFolder: any;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type DocTypeBranchConfig = {
|
|
19
|
+
id: string;
|
|
20
|
+
label: string;
|
|
21
|
+
// Candidate folder names (case-insensitive). If none resolve, fallback starts from `fallbackStart`.
|
|
22
|
+
folderNames?: string[];
|
|
23
|
+
fetcher: (folder: any) => Promise<any>;
|
|
24
|
+
// Optional validator to determine whether the fetch produced any usable queries.
|
|
25
|
+
validator?: (result: any) => boolean;
|
|
26
|
+
// Optional explicit starting folder for the fallback chain.
|
|
27
|
+
fallbackStart?: any;
|
|
28
|
+
};
|
|
29
|
+
|
|
13
30
|
export default class TicketsDataProvider {
|
|
14
31
|
orgUrl: string = '';
|
|
15
32
|
token: string = '';
|
|
@@ -120,22 +137,197 @@ export default class TicketsDataProvider {
|
|
|
120
137
|
else url = `${this.orgUrl}${project}/_apis/wit/queries/${path}?$depth=2&$expand=all`;
|
|
121
138
|
let queries: any = await TFSServices.getItemContent(url, this.token);
|
|
122
139
|
logger.debug(`doctype: ${docType}`);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
140
|
+
const normalizedDocType = (docType || '').toLowerCase();
|
|
141
|
+
const queriesWithChildren = await this.ensureQueryChildren(queries);
|
|
142
|
+
|
|
143
|
+
switch (normalizedDocType) {
|
|
144
|
+
case 'std': {
|
|
145
|
+
const { root: stdRoot, found: stdRootFound } = await this.getDocTypeRoot(
|
|
146
|
+
queriesWithChildren,
|
|
147
|
+
'std'
|
|
148
|
+
);
|
|
149
|
+
logger.debug(`[GetSharedQueries][std] using ${stdRootFound ? 'dedicated folder' : 'root queries'}`);
|
|
150
|
+
// Each branch describes the dedicated folder names, the fetch routine, and how to validate results.
|
|
151
|
+
const stdBranches = await this.fetchDocTypeBranches(queriesWithChildren, stdRoot, [
|
|
152
|
+
{
|
|
153
|
+
id: 'reqToTest',
|
|
154
|
+
label: '[GetSharedQueries][std][req-to-test]',
|
|
155
|
+
folderNames: [
|
|
156
|
+
'requirement - test',
|
|
157
|
+
'requirement to test case',
|
|
158
|
+
'requirement to test',
|
|
159
|
+
'req to test',
|
|
160
|
+
],
|
|
161
|
+
fetcher: (folder: any) => this.fetchLinkedReqTestQueries(folder, false),
|
|
162
|
+
validator: (result: any) => this.hasAnyQueryTree(result?.reqTestTree),
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: 'testToReq',
|
|
166
|
+
label: '[GetSharedQueries][std][test-to-req]',
|
|
167
|
+
folderNames: [
|
|
168
|
+
'test - requirement',
|
|
169
|
+
'test to requirement',
|
|
170
|
+
'test case to requirement',
|
|
171
|
+
'test to req',
|
|
172
|
+
],
|
|
173
|
+
fetcher: (folder: any) => this.fetchLinkedReqTestQueries(folder, true),
|
|
174
|
+
validator: (result: any) => this.hasAnyQueryTree(result?.testReqTree),
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
id: 'mom',
|
|
178
|
+
label: '[GetSharedQueries][std][mom]',
|
|
179
|
+
folderNames: ['linked mom', 'mom'],
|
|
180
|
+
fetcher: (folder: any) => this.fetchLinkedMomQueries(folder),
|
|
181
|
+
validator: (result: any) => this.hasAnyQueryTree(result?.linkedMomTree),
|
|
182
|
+
},
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
const reqToTestResult = stdBranches['reqToTest'];
|
|
186
|
+
const testToReqResult = stdBranches['testToReq'];
|
|
187
|
+
const momResult = stdBranches['mom'];
|
|
188
|
+
|
|
189
|
+
const reqTestQueries = {
|
|
190
|
+
reqTestTree: reqToTestResult?.result?.reqTestTree ?? null,
|
|
191
|
+
testReqTree: testToReqResult?.result?.testReqTree ?? reqToTestResult?.result?.testReqTree ?? null,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const linkedMomQueries = {
|
|
195
|
+
linkedMomTree: momResult?.result?.linkedMomTree ?? null,
|
|
196
|
+
};
|
|
197
|
+
|
|
127
198
|
return { reqTestQueries, linkedMomQueries };
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
199
|
+
}
|
|
200
|
+
case 'str': {
|
|
201
|
+
const { root: strRoot, found: strRootFound } = await this.getDocTypeRoot(
|
|
202
|
+
queriesWithChildren,
|
|
203
|
+
'str'
|
|
204
|
+
);
|
|
205
|
+
logger.debug(`[GetSharedQueries][str] using ${strRootFound ? 'dedicated folder' : 'root queries'}`);
|
|
206
|
+
const strBranches = await this.fetchDocTypeBranches(queriesWithChildren, strRoot, [
|
|
207
|
+
{
|
|
208
|
+
id: 'reqToTest',
|
|
209
|
+
label: '[GetSharedQueries][str][req-to-test]',
|
|
210
|
+
folderNames: [
|
|
211
|
+
'requirement - test',
|
|
212
|
+
'requirement to test case',
|
|
213
|
+
'requirement to test',
|
|
214
|
+
'req to test',
|
|
215
|
+
],
|
|
216
|
+
fetcher: (folder: any) => this.fetchLinkedReqTestQueries(folder, false),
|
|
217
|
+
validator: (result: any) => this.hasAnyQueryTree(result?.reqTestTree),
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
id: 'testToReq',
|
|
221
|
+
label: '[GetSharedQueries][str][test-to-req]',
|
|
222
|
+
folderNames: [
|
|
223
|
+
'test - requirement',
|
|
224
|
+
'test to requirement',
|
|
225
|
+
'test case to requirement',
|
|
226
|
+
'test to req',
|
|
227
|
+
],
|
|
228
|
+
fetcher: (folder: any) => this.fetchLinkedReqTestQueries(folder, true),
|
|
229
|
+
validator: (result: any) => this.hasAnyQueryTree(result?.testReqTree),
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
id: 'openPcrToTest',
|
|
233
|
+
label: '[GetSharedQueries][str][open-pcr-to-test]',
|
|
234
|
+
folderNames: ['open pcr to test case', 'open pcr to test', 'open pcr - test', 'open pcr'],
|
|
235
|
+
fetcher: (folder: any) => this.fetchLinkedOpenPcrTestQueries(folder, false),
|
|
236
|
+
validator: (result: any) => this.hasAnyQueryTree(result?.OpenPcrToTestTree),
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
id: 'testToOpenPcr',
|
|
240
|
+
label: '[GetSharedQueries][str][test-to-open-pcr]',
|
|
241
|
+
folderNames: ['test case to open pcr', 'test to open pcr', 'test - open pcr', 'open pcr'],
|
|
242
|
+
fetcher: (folder: any) => this.fetchLinkedOpenPcrTestQueries(folder, true),
|
|
243
|
+
validator: (result: any) => this.hasAnyQueryTree(result?.TestToOpenPcrTree),
|
|
244
|
+
},
|
|
245
|
+
]);
|
|
246
|
+
|
|
247
|
+
const strReqToTest = strBranches['reqToTest'];
|
|
248
|
+
const strTestToReq = strBranches['testToReq'];
|
|
249
|
+
const strOpenPcrToTest = strBranches['openPcrToTest'];
|
|
250
|
+
const strTestToOpenPcr = strBranches['testToOpenPcr'];
|
|
251
|
+
|
|
252
|
+
const reqTestTrees = {
|
|
253
|
+
reqTestTree: strReqToTest?.result?.reqTestTree ?? null,
|
|
254
|
+
testReqTree: strTestToReq?.result?.testReqTree ?? strReqToTest?.result?.testReqTree ?? null,
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const openPcrTestTrees = {
|
|
258
|
+
OpenPcrToTestTree: strOpenPcrToTest?.result?.OpenPcrToTestTree ?? null,
|
|
259
|
+
TestToOpenPcrTree:
|
|
260
|
+
strTestToOpenPcr?.result?.TestToOpenPcrTree ??
|
|
261
|
+
strOpenPcrToTest?.result?.TestToOpenPcrTree ??
|
|
262
|
+
null,
|
|
263
|
+
};
|
|
264
|
+
|
|
131
265
|
return { reqTestTrees, openPcrTestTrees };
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
266
|
+
}
|
|
267
|
+
case 'test-reporter': {
|
|
268
|
+
const { root: testReporterRoot, found: testReporterFound } = await this.getDocTypeRoot(
|
|
269
|
+
queriesWithChildren,
|
|
270
|
+
'test-reporter'
|
|
271
|
+
);
|
|
272
|
+
logger.debug(
|
|
273
|
+
`[GetSharedQueries][test-reporter] using ${
|
|
274
|
+
testReporterFound ? 'dedicated folder' : 'root queries'
|
|
275
|
+
}`
|
|
276
|
+
);
|
|
277
|
+
const testReporterBranches = await this.fetchDocTypeBranches(
|
|
278
|
+
queriesWithChildren,
|
|
279
|
+
testReporterRoot,
|
|
280
|
+
[
|
|
281
|
+
{
|
|
282
|
+
id: 'testReporter',
|
|
283
|
+
label: '[GetSharedQueries][test-reporter]',
|
|
284
|
+
folderNames: ['test reporter', 'test-reporter'],
|
|
285
|
+
fetcher: (folder: any) => this.fetchTestReporterQueries(folder),
|
|
286
|
+
validator: (result: any) => this.hasAnyQueryTree(result?.testAssociatedTree),
|
|
287
|
+
},
|
|
288
|
+
]
|
|
289
|
+
);
|
|
290
|
+
const testReporterFetch = testReporterBranches['testReporter'];
|
|
291
|
+
return testReporterFetch?.result ?? { testAssociatedTree: null };
|
|
292
|
+
}
|
|
135
293
|
case 'srs':
|
|
136
|
-
return await this.fetchSrsQueries(
|
|
137
|
-
case 'svd':
|
|
138
|
-
|
|
294
|
+
return await this.fetchSrsQueries(queriesWithChildren);
|
|
295
|
+
case 'svd': {
|
|
296
|
+
const { root: svdRoot, found } = await this.getDocTypeRoot(queriesWithChildren, 'svd');
|
|
297
|
+
if (!found) {
|
|
298
|
+
logger.debug('[GetSharedQueries][svd] dedicated folder not found, using fallback tree');
|
|
299
|
+
}
|
|
300
|
+
const svdBranches = await this.fetchDocTypeBranches(queriesWithChildren, svdRoot, [
|
|
301
|
+
{
|
|
302
|
+
id: 'systemOverview',
|
|
303
|
+
label: '[GetSharedQueries][svd][system-overview]',
|
|
304
|
+
folderNames: ['system overview'],
|
|
305
|
+
fetcher: async (folder: any) => {
|
|
306
|
+
const { tree1 } = await this.structureAllQueryPath(folder);
|
|
307
|
+
return tree1;
|
|
308
|
+
},
|
|
309
|
+
validator: (result: any) => !!result,
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
id: 'knownBugs',
|
|
313
|
+
label: '[GetSharedQueries][svd][known-bugs]',
|
|
314
|
+
folderNames: ['known bugs', 'known bug'],
|
|
315
|
+
fetcher: async (folder: any) => {
|
|
316
|
+
const { tree2 } = await this.structureAllQueryPath(folder);
|
|
317
|
+
return tree2;
|
|
318
|
+
},
|
|
319
|
+
validator: (result: any) => !!result,
|
|
320
|
+
},
|
|
321
|
+
]);
|
|
322
|
+
|
|
323
|
+
const systemOverviewFetch = svdBranches['systemOverview'];
|
|
324
|
+
const knownBugsFetch = svdBranches['knownBugs'];
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
systemOverviewQueryTree: systemOverviewFetch?.result ?? null,
|
|
328
|
+
knownBugsQueryTree: knownBugsFetch?.result ?? null,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
139
331
|
default:
|
|
140
332
|
break;
|
|
141
333
|
}
|
|
@@ -231,6 +423,38 @@ export default class TicketsDataProvider {
|
|
|
231
423
|
return { linkedMomTree };
|
|
232
424
|
}
|
|
233
425
|
|
|
426
|
+
private hasAnyQueryTree(result: any): boolean {
|
|
427
|
+
const inspect = (value: any): boolean => {
|
|
428
|
+
if (!value) {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (Array.isArray(value)) {
|
|
433
|
+
return value.some(inspect);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (typeof value === 'object') {
|
|
437
|
+
if (value.isValidQuery || value.wiql || value.queryType) {
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if ('roots' in value && Array.isArray(value.roots) && value.roots.length > 0) {
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if ('children' in value && Array.isArray(value.children) && value.children.length > 0) {
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return Object.values(value).some(inspect);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return false;
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
return inspect(result);
|
|
456
|
+
}
|
|
457
|
+
|
|
234
458
|
/**
|
|
235
459
|
* Fetches and structures linked queries related to open PCR (Problem Change Request) tests.
|
|
236
460
|
*
|
|
@@ -271,13 +495,6 @@ export default class TicketsDataProvider {
|
|
|
271
495
|
return { testAssociatedTree };
|
|
272
496
|
}
|
|
273
497
|
|
|
274
|
-
private async fetchAnyQueries(queries: any) {
|
|
275
|
-
const { tree1: systemOverviewQueryTree, tree2: knownBugsQueryTree } = await this.structureAllQueryPath(
|
|
276
|
-
queries
|
|
277
|
-
);
|
|
278
|
-
return { systemOverviewQueryTree, knownBugsQueryTree };
|
|
279
|
-
}
|
|
280
|
-
|
|
281
498
|
private async fetchSystemRequirementQueries(queries: any, excludedFolderNames: string[] = []) {
|
|
282
499
|
const { tree1: systemRequirementsQueryTree } = await this.structureFetchedQueries(
|
|
283
500
|
queries,
|
|
@@ -429,6 +646,278 @@ export default class TicketsDataProvider {
|
|
|
429
646
|
);
|
|
430
647
|
}
|
|
431
648
|
|
|
649
|
+
/**
|
|
650
|
+
* Performs a breadth-first walk starting at `parent` to locate the nearest folder whose
|
|
651
|
+
* name matches any of the provided candidates (case-insensitive). Exact matches win; if none
|
|
652
|
+
* are found the first partial match encountered is returned. When no candidates are located,
|
|
653
|
+
* the method yields `null`.
|
|
654
|
+
*/
|
|
655
|
+
private async findChildFolderByPossibleNames(parent: any, possibleNames: string[]): Promise<any | null> {
|
|
656
|
+
if (!parent || !possibleNames?.length) {
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const normalizedNames = possibleNames.map((name) => name.toLowerCase());
|
|
661
|
+
|
|
662
|
+
const isMatch = (candidate: string, value: string) => value === candidate;
|
|
663
|
+
const isPartialMatch = (candidate: string, value: string) => value.includes(candidate);
|
|
664
|
+
|
|
665
|
+
const tryMatch = (folder: any, matcher: (candidate: string, value: string) => boolean) => {
|
|
666
|
+
const folderName = (folder?.name || '').toLowerCase();
|
|
667
|
+
return normalizedNames.some((candidate) => matcher(candidate, folderName));
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
const parentWithChildren = await this.ensureQueryChildren(parent);
|
|
671
|
+
if (!parentWithChildren?.children?.length) {
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const queue: any[] = [];
|
|
676
|
+
const visited = new Set<string>();
|
|
677
|
+
let partialCandidate: any = null;
|
|
678
|
+
|
|
679
|
+
// Seed the queue with direct children so we prefer closer matches before walking deeper.
|
|
680
|
+
for (const child of parentWithChildren.children) {
|
|
681
|
+
if (!child?.isFolder) {
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
const childId = child.id ?? `${child.name}-${Math.random()}`;
|
|
685
|
+
queue.push(child);
|
|
686
|
+
visited.add(childId);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const considerFolder = async (folder: any): Promise<any | null> => {
|
|
690
|
+
if (tryMatch(folder, isMatch)) {
|
|
691
|
+
return await this.ensureQueryChildren(folder);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (!partialCandidate && tryMatch(folder, isPartialMatch)) {
|
|
695
|
+
partialCandidate = await this.ensureQueryChildren(folder);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
return null;
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
for (const child of queue) {
|
|
702
|
+
const match = await considerFolder(child);
|
|
703
|
+
if (match) {
|
|
704
|
+
return match;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
while (queue.length > 0) {
|
|
709
|
+
const current = queue.shift();
|
|
710
|
+
if (!current) {
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const currentWithChildren = await this.ensureQueryChildren(current);
|
|
715
|
+
if (!currentWithChildren) {
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const match = await considerFolder(currentWithChildren);
|
|
720
|
+
if (match) {
|
|
721
|
+
return match;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Breadth-first expansion so we climb the hierarchy gradually.
|
|
725
|
+
if (currentWithChildren.children?.length) {
|
|
726
|
+
for (const child of currentWithChildren.children) {
|
|
727
|
+
if (!child?.isFolder) {
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
const childId = child.id ?? `${child.name}-${Math.random()}`;
|
|
731
|
+
if (!visited.has(childId)) {
|
|
732
|
+
visited.add(childId);
|
|
733
|
+
queue.push(child);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
return partialCandidate;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Executes `fetcher` against `startingFolder` and, if the validator deems the result empty,
|
|
744
|
+
* climbs ancestor folders toward `rootQueries` until a satisfactory result is produced.
|
|
745
|
+
* The first successful folder short-circuits the search; otherwise the final attempt is
|
|
746
|
+
* returned to preserve legacy behavior.
|
|
747
|
+
*/
|
|
748
|
+
private async fetchWithAncestorFallback(
|
|
749
|
+
rootQueries: any,
|
|
750
|
+
startingFolder: any,
|
|
751
|
+
fetcher: (folder: any) => Promise<any>,
|
|
752
|
+
logContext: string,
|
|
753
|
+
validator?: (result: any) => boolean
|
|
754
|
+
): Promise<{ result: any; usedFolder: any }> {
|
|
755
|
+
const rootWithChildren = await this.ensureQueryChildren(rootQueries);
|
|
756
|
+
const candidates = await this.buildFallbackChain(rootWithChildren, startingFolder);
|
|
757
|
+
const evaluate = validator ?? ((res: any) => this.hasAnyQueryTree(res));
|
|
758
|
+
|
|
759
|
+
let lastResult: any = null;
|
|
760
|
+
let lastFolder: any = startingFolder ?? rootWithChildren;
|
|
761
|
+
|
|
762
|
+
for (const candidate of candidates) {
|
|
763
|
+
const enrichedCandidate = await this.ensureQueryChildren(candidate);
|
|
764
|
+
const candidateName = enrichedCandidate?.name ?? '<root>';
|
|
765
|
+
logger.debug(`${logContext} trying folder: ${candidateName}`);
|
|
766
|
+
lastResult = await fetcher(enrichedCandidate);
|
|
767
|
+
lastFolder = enrichedCandidate;
|
|
768
|
+
if (evaluate(lastResult)) {
|
|
769
|
+
logger.debug(`${logContext} using folder: ${candidateName}`);
|
|
770
|
+
return { result: lastResult, usedFolder: enrichedCandidate };
|
|
771
|
+
}
|
|
772
|
+
logger.debug(`${logContext} folder ${candidateName} produced no results, ascending`);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
logger.debug(`${logContext} no folders yielded results, returning last attempt`);
|
|
776
|
+
return { result: lastResult, usedFolder: lastFolder };
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Applies `fetchWithAncestorFallback` to each configured branch, resolving dedicated folders
|
|
781
|
+
* when available and emitting a map keyed by branch id. Each outcome includes both the
|
|
782
|
+
* resulting payload and the specific folder that satisfied the fallback chain.
|
|
783
|
+
*/
|
|
784
|
+
private async fetchDocTypeBranches(
|
|
785
|
+
queriesWithChildren: any,
|
|
786
|
+
docRoot: any,
|
|
787
|
+
branches: DocTypeBranchConfig[]
|
|
788
|
+
): Promise<Record<string, FallbackFetchOutcome>> {
|
|
789
|
+
const results: Record<string, FallbackFetchOutcome> = {};
|
|
790
|
+
const effectiveDocRoot = docRoot ?? queriesWithChildren;
|
|
791
|
+
|
|
792
|
+
for (const branch of branches) {
|
|
793
|
+
const fallbackStart = branch.fallbackStart ?? effectiveDocRoot;
|
|
794
|
+
let startingFolder = fallbackStart;
|
|
795
|
+
let startingName = startingFolder?.name ?? '<root>';
|
|
796
|
+
|
|
797
|
+
// Attempt to locate a more specific child folder, falling back to the provided root if absent.
|
|
798
|
+
if (branch.folderNames?.length && effectiveDocRoot) {
|
|
799
|
+
const resolvedFolder = await this.findChildFolderByPossibleNames(
|
|
800
|
+
effectiveDocRoot,
|
|
801
|
+
branch.folderNames
|
|
802
|
+
);
|
|
803
|
+
if (resolvedFolder) {
|
|
804
|
+
startingFolder = resolvedFolder;
|
|
805
|
+
startingName = resolvedFolder?.name ?? '<root>';
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
logger.debug(`${branch.label} starting folder: ${startingName}`);
|
|
810
|
+
|
|
811
|
+
const fetchOutcome = await this.fetchWithAncestorFallback(
|
|
812
|
+
queriesWithChildren,
|
|
813
|
+
startingFolder,
|
|
814
|
+
branch.fetcher,
|
|
815
|
+
branch.label,
|
|
816
|
+
branch.validator
|
|
817
|
+
);
|
|
818
|
+
|
|
819
|
+
logger.debug(`${branch.label} final folder: ${fetchOutcome.usedFolder?.name ?? '<root>'}`);
|
|
820
|
+
|
|
821
|
+
results[branch.id] = fetchOutcome;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
return results;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Constructs an ordered list of folders to probe during fallback. The sequence starts at
|
|
829
|
+
* `startingFolder` (if provided) and walks upward through ancestors to the root query tree,
|
|
830
|
+
* ensuring no folder id appears twice.
|
|
831
|
+
*/
|
|
832
|
+
private async buildFallbackChain(rootQueries: any, startingFolder: any): Promise<any[]> {
|
|
833
|
+
const chain: any[] = [];
|
|
834
|
+
const seen = new Set<string>();
|
|
835
|
+
const pushUnique = (node: any) => {
|
|
836
|
+
if (!node) {
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
const id = node.id ?? '__root__';
|
|
840
|
+
if (seen.has(id)) {
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
seen.add(id);
|
|
844
|
+
chain.push(node);
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
if (startingFolder?.id) {
|
|
848
|
+
const path = await this.findPathToNode(rootQueries, startingFolder.id);
|
|
849
|
+
if (path) {
|
|
850
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
851
|
+
pushUnique(path[i]);
|
|
852
|
+
}
|
|
853
|
+
} else {
|
|
854
|
+
pushUnique(startingFolder);
|
|
855
|
+
}
|
|
856
|
+
} else if (startingFolder) {
|
|
857
|
+
pushUnique(startingFolder);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
pushUnique(rootQueries);
|
|
861
|
+
return chain;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Recursively searches the query tree for the node with the provided id and returns the
|
|
866
|
+
* path (root → target). Nodes are enriched with children on demand and a visited set guards
|
|
867
|
+
* against cycles within malformed data.
|
|
868
|
+
*/
|
|
869
|
+
private async findPathToNode(
|
|
870
|
+
currentNode: any,
|
|
871
|
+
targetId: string,
|
|
872
|
+
visited: Set<string> = new Set<string>()
|
|
873
|
+
): Promise<any[] | null> {
|
|
874
|
+
if (!currentNode) {
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const currentId = currentNode.id ?? '__root__';
|
|
879
|
+
if (visited.has(currentId)) {
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
882
|
+
visited.add(currentId);
|
|
883
|
+
|
|
884
|
+
if (currentNode.id === targetId) {
|
|
885
|
+
return [currentNode];
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const enrichedNode = await this.ensureQueryChildren(currentNode);
|
|
889
|
+
const children = enrichedNode?.children;
|
|
890
|
+
if (!children?.length) {
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
for (const child of children) {
|
|
895
|
+
const path = await this.findPathToNode(child, targetId, visited);
|
|
896
|
+
if (path) {
|
|
897
|
+
return [enrichedNode, ...path];
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
return null;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
private async getDocTypeRoot(
|
|
905
|
+
rootQueries: any,
|
|
906
|
+
docTypeName: string
|
|
907
|
+
): Promise<{ root: any; found: boolean }> {
|
|
908
|
+
if (!rootQueries) {
|
|
909
|
+
return { root: rootQueries, found: false };
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const docTypeFolder = await this.findQueryFolderByName(rootQueries, docTypeName);
|
|
913
|
+
if (docTypeFolder) {
|
|
914
|
+
const folderWithChildren = await this.ensureQueryChildren(docTypeFolder);
|
|
915
|
+
return { root: folderWithChildren, found: true };
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
return { root: rootQueries, found: false };
|
|
919
|
+
}
|
|
920
|
+
|
|
432
921
|
private async ensureQueryChildren(node: any): Promise<any> {
|
|
433
922
|
if (!node || !node.hasChildren || node.children) {
|
|
434
923
|
return node;
|
|
@@ -507,12 +996,12 @@ export default class TicketsDataProvider {
|
|
|
507
996
|
// Initialize maps
|
|
508
997
|
const sourceTargetsMap: Map<any, any[]> = new Map();
|
|
509
998
|
const lookupMap: Map<number, any> = new Map();
|
|
510
|
-
|
|
999
|
+
|
|
511
1000
|
if (workItemRelations) {
|
|
512
1001
|
// Step 1: Collect all unique work item IDs that need to be fetched
|
|
513
1002
|
const sourceIds = new Set<number>();
|
|
514
1003
|
const targetIds = new Set<number>();
|
|
515
|
-
|
|
1004
|
+
|
|
516
1005
|
for (const relation of workItemRelations) {
|
|
517
1006
|
if (!relation.source) {
|
|
518
1007
|
// Root link - target is actually the source
|
|
@@ -526,18 +1015,18 @@ export default class TicketsDataProvider {
|
|
|
526
1015
|
}
|
|
527
1016
|
|
|
528
1017
|
// Step 2: Fetch all work items in parallel with concurrency limit
|
|
529
|
-
const allSourcePromises = Array.from(sourceIds).map(id =>
|
|
1018
|
+
const allSourcePromises = Array.from(sourceIds).map((id) =>
|
|
530
1019
|
this.limit(() => {
|
|
531
|
-
const relation = workItemRelations.find(
|
|
532
|
-
(!r.source && r.target.id === id) ||
|
|
1020
|
+
const relation = workItemRelations.find(
|
|
1021
|
+
(r) => (!r.source && r.target.id === id) || r.source?.id === id
|
|
533
1022
|
);
|
|
534
1023
|
return this.fetchWIForQueryResult(relation, columnsToShowMap, columnSourceMap, true);
|
|
535
1024
|
})
|
|
536
1025
|
);
|
|
537
1026
|
|
|
538
|
-
const allTargetPromises = Array.from(targetIds).map(id =>
|
|
1027
|
+
const allTargetPromises = Array.from(targetIds).map((id) =>
|
|
539
1028
|
this.limit(() => {
|
|
540
|
-
const relation = workItemRelations.find(r => r.target?.id === id);
|
|
1029
|
+
const relation = workItemRelations.find((r) => r.target?.id === id);
|
|
541
1030
|
return this.fetchWIForQueryResult(relation, columnsToShowMap, columnTargetsMap, true);
|
|
542
1031
|
})
|
|
543
1032
|
);
|
|
@@ -545,12 +1034,12 @@ export default class TicketsDataProvider {
|
|
|
545
1034
|
// Wait for all fetches to complete in parallel (with concurrency control)
|
|
546
1035
|
const [sourceWorkItems, targetWorkItems] = await Promise.all([
|
|
547
1036
|
Promise.all(allSourcePromises),
|
|
548
|
-
Promise.all(allTargetPromises)
|
|
1037
|
+
Promise.all(allTargetPromises),
|
|
549
1038
|
]);
|
|
550
1039
|
|
|
551
1040
|
// Build lookup maps
|
|
552
1041
|
const sourceWorkItemMap = new Map<number, any>();
|
|
553
|
-
sourceWorkItems.forEach(wi => {
|
|
1042
|
+
sourceWorkItems.forEach((wi) => {
|
|
554
1043
|
sourceWorkItemMap.set(wi.id, wi);
|
|
555
1044
|
if (!lookupMap.has(wi.id)) {
|
|
556
1045
|
lookupMap.set(wi.id, wi);
|
|
@@ -558,7 +1047,7 @@ export default class TicketsDataProvider {
|
|
|
558
1047
|
});
|
|
559
1048
|
|
|
560
1049
|
const targetWorkItemMap = new Map<number, any>();
|
|
561
|
-
targetWorkItems.forEach(wi => {
|
|
1050
|
+
targetWorkItems.forEach((wi) => {
|
|
562
1051
|
targetWorkItemMap.set(wi.id, wi);
|
|
563
1052
|
if (!lookupMap.has(wi.id)) {
|
|
564
1053
|
lookupMap.set(wi.id, wi);
|
|
@@ -595,7 +1084,7 @@ export default class TicketsDataProvider {
|
|
|
595
1084
|
|
|
596
1085
|
// In case of target is a test case
|
|
597
1086
|
this.mapTestCaseToRelatedItem(targetWi, sourceWorkItem, testCaseToRelatedWiMap);
|
|
598
|
-
|
|
1087
|
+
|
|
599
1088
|
const targets: any = sourceTargetsMap.get(sourceWorkItem) || [];
|
|
600
1089
|
targets.push(targetWi);
|
|
601
1090
|
sourceTargetsMap.set(sourceWorkItem, targets);
|
|
@@ -654,14 +1143,12 @@ export default class TicketsDataProvider {
|
|
|
654
1143
|
// Fetch all work items in parallel with concurrency limit
|
|
655
1144
|
const wiSet: Set<any> = new Set();
|
|
656
1145
|
if (workItems) {
|
|
657
|
-
const fetchPromises = workItems.map(workItem =>
|
|
658
|
-
this.limit(() =>
|
|
659
|
-
this.fetchWIForQueryResult(workItem, columnsToShowMap, fieldsToIncludeMap, false)
|
|
660
|
-
)
|
|
1146
|
+
const fetchPromises = workItems.map((workItem) =>
|
|
1147
|
+
this.limit(() => this.fetchWIForQueryResult(workItem, columnsToShowMap, fieldsToIncludeMap, false))
|
|
661
1148
|
);
|
|
662
|
-
|
|
1149
|
+
|
|
663
1150
|
const fetchedWorkItems = await Promise.all(fetchPromises);
|
|
664
|
-
fetchedWorkItems.forEach(wi => wiSet.add(wi));
|
|
1151
|
+
fetchedWorkItems.forEach((wi) => wiSet.add(wi));
|
|
665
1152
|
}
|
|
666
1153
|
|
|
667
1154
|
columnsToShowMap.clear();
|
|
@@ -1647,9 +2134,7 @@ export default class TicketsDataProvider {
|
|
|
1647
2134
|
}
|
|
1648
2135
|
|
|
1649
2136
|
// Get the requirement type field
|
|
1650
|
-
const rawRequirementType: string =
|
|
1651
|
-
fullWi.fields['Microsoft.VSTS.CMMI.RequirementType'] ||
|
|
1652
|
-
'';
|
|
2137
|
+
const rawRequirementType: string = fullWi.fields['Microsoft.VSTS.CMMI.RequirementType'] || '';
|
|
1653
2138
|
|
|
1654
2139
|
// Normalize and trim the requirement type
|
|
1655
2140
|
const trimmedType = String(rawRequirementType).trim();
|