@elisra-devops/docgen-data-provider 1.63.12 → 1.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +26 -9
- package/.github/workflows/release.yml +9 -10
- package/bin/helpers/tfs.d.ts +3 -0
- package/bin/helpers/tfs.js +44 -7
- package/bin/helpers/tfs.js.map +1 -1
- package/bin/modules/GitDataProvider.d.ts +10 -0
- package/bin/modules/GitDataProvider.js +10 -0
- package/bin/modules/GitDataProvider.js.map +1 -1
- package/bin/modules/MangementDataProvider.js +7 -1
- package/bin/modules/MangementDataProvider.js.map +1 -1
- package/bin/modules/TestDataProvider.js +0 -1
- package/bin/modules/TestDataProvider.js.map +1 -1
- package/bin/modules/TicketsDataProvider.d.ts +63 -27
- package/bin/modules/TicketsDataProvider.js +226 -122
- package/bin/modules/TicketsDataProvider.js.map +1 -1
- package/bin/tests/helpers/helper.test.js +279 -0
- package/bin/tests/helpers/helper.test.js.map +1 -0
- package/bin/{helpers/test → tests/helpers}/tfs.test.js +312 -49
- package/bin/tests/helpers/tfs.test.js.map +1 -0
- package/bin/tests/index.test.js +25 -0
- package/bin/tests/index.test.js.map +1 -0
- package/bin/tests/models/tfs-data.test.js +160 -0
- package/bin/tests/models/tfs-data.test.js.map +1 -0
- package/bin/{modules/test → tests/modules}/JfrogDataProvider.test.js +9 -9
- package/bin/tests/modules/JfrogDataProvider.test.js.map +1 -0
- package/bin/tests/modules/ResultDataProvider.test.js +1942 -0
- package/bin/tests/modules/ResultDataProvider.test.js.map +1 -0
- package/bin/tests/modules/gitDataProvider.test.js +1888 -0
- package/bin/tests/modules/gitDataProvider.test.js.map +1 -0
- package/bin/{modules/test → tests/modules}/managmentDataProvider.test.js +39 -31
- package/bin/tests/modules/managmentDataProvider.test.js.map +1 -0
- package/bin/tests/modules/pipelineDataProvider.test.d.ts +1 -0
- package/bin/tests/modules/pipelineDataProvider.test.js +783 -0
- package/bin/tests/modules/pipelineDataProvider.test.js.map +1 -0
- package/bin/tests/modules/testDataProvider.test.d.ts +1 -0
- package/bin/tests/modules/testDataProvider.test.js +717 -0
- package/bin/tests/modules/testDataProvider.test.js.map +1 -0
- package/bin/tests/modules/ticketsDataProvider.test.d.ts +1 -0
- package/bin/tests/modules/ticketsDataProvider.test.js +1681 -0
- package/bin/tests/modules/ticketsDataProvider.test.js.map +1 -0
- package/bin/tests/utils/DataProviderUtils.test.d.ts +1 -0
- package/bin/tests/utils/DataProviderUtils.test.js +61 -0
- package/bin/tests/utils/DataProviderUtils.test.js.map +1 -0
- package/bin/tests/utils/testStepParserHelper.test.d.ts +1 -0
- package/bin/tests/utils/testStepParserHelper.test.js +359 -0
- package/bin/tests/utils/testStepParserHelper.test.js.map +1 -0
- package/package.json +10 -1
- package/src/helpers/tfs.ts +51 -7
- package/src/modules/GitDataProvider.ts +10 -0
- package/src/modules/MangementDataProvider.ts +6 -1
- package/src/modules/TestDataProvider.ts +0 -1
- package/src/modules/TicketsDataProvider.ts +311 -151
- package/src/tests/helpers/helper.test.ts +337 -0
- package/src/tests/helpers/tfs.test.ts +1092 -0
- package/src/tests/index.test.ts +28 -0
- package/src/tests/models/tfs-data.test.ts +203 -0
- package/src/tests/modules/JfrogDataProvider.test.ts +167 -0
- package/src/tests/modules/ResultDataProvider.test.ts +2571 -0
- package/src/tests/modules/gitDataProvider.test.ts +2628 -0
- package/src/{modules/test → tests/modules}/managmentDataProvider.test.ts +63 -32
- package/src/tests/modules/pipelineDataProvider.test.ts +1038 -0
- package/src/tests/modules/testDataProvider.test.ts +1046 -0
- package/src/tests/modules/ticketsDataProvider.test.ts +2204 -0
- package/src/tests/utils/DataProviderUtils.test.ts +76 -0
- package/src/tests/utils/testStepParserHelper.test.ts +437 -0
- package/tsconfig.json +1 -0
- package/bin/helpers/test/tfs.test.js.map +0 -1
- package/bin/modules/test/JfrogDataProvider.test.js.map +0 -1
- package/bin/modules/test/ResultDataProvider.test.js +0 -444
- package/bin/modules/test/ResultDataProvider.test.js.map +0 -1
- package/bin/modules/test/gitDataProvider.test.js +0 -433
- package/bin/modules/test/gitDataProvider.test.js.map +0 -1
- package/bin/modules/test/managmentDataProvider.test.js.map +0 -1
- package/bin/modules/test/pipelineDataProvider.test.js +0 -237
- package/bin/modules/test/pipelineDataProvider.test.js.map +0 -1
- package/bin/modules/test/testDataProvider.test.js +0 -234
- package/bin/modules/test/testDataProvider.test.js.map +0 -1
- package/bin/modules/test/ticketsDataProvider.test.js +0 -322
- package/bin/modules/test/ticketsDataProvider.test.js.map +0 -1
- package/src/helpers/test/tfs.test.ts +0 -748
- package/src/modules/test/JfrogDataProvider.test.ts +0 -171
- package/src/modules/test/ResultDataProvider.test.ts +0 -542
- package/src/modules/test/gitDataProvider.test.ts +0 -691
- package/src/modules/test/pipelineDataProvider.test.ts +0 -292
- package/src/modules/test/testDataProvider.test.ts +0 -318
- package/src/modules/test/ticketsDataProvider.test.ts +0 -434
- /package/bin/{helpers/test/tfs.test.d.ts → tests/helpers/helper.test.d.ts} +0 -0
- /package/bin/{modules/test/JfrogDataProvider.test.d.ts → tests/helpers/tfs.test.d.ts} +0 -0
- /package/bin/{modules/test/ResultDataProvider.test.d.ts → tests/index.test.d.ts} +0 -0
- /package/bin/{modules/test/gitDataProvider.test.d.ts → tests/models/tfs-data.test.d.ts} +0 -0
- /package/bin/{modules/test/managmentDataProvider.test.d.ts → tests/modules/JfrogDataProvider.test.d.ts} +0 -0
- /package/bin/{modules/test/pipelineDataProvider.test.d.ts → tests/modules/ResultDataProvider.test.d.ts} +0 -0
- /package/bin/{modules/test/testDataProvider.test.d.ts → tests/modules/gitDataProvider.test.d.ts} +0 -0
- /package/bin/{modules/test/ticketsDataProvider.test.d.ts → tests/modules/managmentDataProvider.test.d.ts} +0 -0
|
@@ -200,38 +200,78 @@ export default class TicketsDataProvider {
|
|
|
200
200
|
* @returns Boolean indicating if the WIQL matches the area path conditions
|
|
201
201
|
*/
|
|
202
202
|
private matchesAreaPathCondition;
|
|
203
|
+
private matchesSourceTargetConditionAsync;
|
|
204
|
+
private matchesFlatWorkItemTypeConditionAsync;
|
|
205
|
+
private buildQueryNode;
|
|
206
|
+
private getProjectFromQueryNode;
|
|
207
|
+
/**
|
|
208
|
+
* Normalizes allowed work item types into a lowercase set.
|
|
209
|
+
*/
|
|
210
|
+
private normalizeAllowedTypes;
|
|
211
|
+
/**
|
|
212
|
+
* Returns true when every value in `found` exists in `allowed`.
|
|
213
|
+
*/
|
|
214
|
+
private areAllAllowed;
|
|
203
215
|
/**
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
216
|
+
* Builds a regex-ready field selector for WIQL.
|
|
217
|
+
* - Link queries: Source/Target can appear as `Source.[...]` or `[Source].[...]`
|
|
218
|
+
* - Flat queries: no owner prefix, e.g. `[System.WorkItemType]`
|
|
219
|
+
*/
|
|
220
|
+
private buildWiqlFieldPattern;
|
|
221
|
+
/**
|
|
222
|
+
* Extracts all quoted values used in `=` or `IN (...)` comparisons for a given field.
|
|
223
|
+
* Returned values are trimmed and lowercased.
|
|
207
224
|
*
|
|
208
|
-
*
|
|
209
|
-
* -
|
|
210
|
-
* -
|
|
225
|
+
* Examples:
|
|
226
|
+
* - `<field> = 'Epic'`
|
|
227
|
+
* - `<field> IN ('Epic','Feature')`
|
|
228
|
+
*/
|
|
229
|
+
private extractQuotedValuesForField;
|
|
230
|
+
/**
|
|
231
|
+
* Extracts all numeric values used in `=` or `IN (...)` comparisons for a given field.
|
|
232
|
+
* Supports both quoted and unquoted numbers.
|
|
211
233
|
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
* @returns A boolean indicating whether the WIQL includes at least one valid source work item type
|
|
216
|
-
* and at least one valid target work item type.
|
|
234
|
+
* Examples:
|
|
235
|
+
* - `<field> = 123`
|
|
236
|
+
* - `<field> IN (123, '456')`
|
|
217
237
|
*/
|
|
218
|
-
private
|
|
238
|
+
private extractNumericValuesForField;
|
|
219
239
|
/**
|
|
220
|
-
*
|
|
221
|
-
*
|
|
240
|
+
* Extracts work item IDs from WIQL.
|
|
241
|
+
* - Link queries: pass `context` to match `Source.[System.Id]` / `Target.[System.Id]` (with or without `[Source]`)
|
|
242
|
+
* - Flat queries: omit `context` to match `[System.Id]`
|
|
243
|
+
*/
|
|
244
|
+
private extractWorkItemIdsFromWiql;
|
|
245
|
+
/**
|
|
246
|
+
* Determines if a link-query side (Source/Target) is allowed based on WIQL constraints.
|
|
222
247
|
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
248
|
+
* Rules:
|
|
249
|
+
* - If `allowedTypes` is empty: preserve legacy behavior by requiring that WorkItemType is present for that side.
|
|
250
|
+
* - If WIQL contains a WorkItemType constraint for that side: all specified types must be within `allowedTypes`.
|
|
251
|
+
* - Otherwise, if WIQL contains a System.Id constraint for that side: fetch the work item type(s) by id and validate.
|
|
252
|
+
* - Otherwise: reject.
|
|
227
253
|
*/
|
|
228
|
-
private
|
|
229
|
-
private buildQueryNode;
|
|
254
|
+
private isLinkSideAllowedByTypeOrId;
|
|
230
255
|
/**
|
|
231
|
-
*
|
|
232
|
-
*
|
|
256
|
+
* Determines if a flat query is allowed based on WIQL constraints.
|
|
257
|
+
*
|
|
258
|
+
* Rules:
|
|
259
|
+
* - If `allowedTypes` is empty: preserve legacy behavior by requiring that `[System.WorkItemType]` appears in WIQL.
|
|
260
|
+
* - If WIQL contains a WorkItemType constraint: all specified types must be within `allowedTypes`.
|
|
261
|
+
* - Otherwise, if WIQL contains `[System.Id]`: fetch the work item type(s) by id and validate.
|
|
262
|
+
* - Otherwise: reject.
|
|
233
263
|
*/
|
|
234
|
-
private
|
|
264
|
+
private isFlatQueryAllowedByTypeOrId;
|
|
265
|
+
/**
|
|
266
|
+
* Fetches the work item type for a given work item ID.
|
|
267
|
+
* Uses caching to avoid repeated API calls.
|
|
268
|
+
*
|
|
269
|
+
* @param project The project name
|
|
270
|
+
* @param id The work item ID
|
|
271
|
+
* @param workItemTypeCache The cache map for work item types
|
|
272
|
+
* @returns The work item type or null if not found
|
|
273
|
+
*/
|
|
274
|
+
private getWorkItemTypeById;
|
|
235
275
|
/**
|
|
236
276
|
* Matches flat query WIQL against an area path filter by checking any referenced [System.AreaPath].
|
|
237
277
|
* Compares only the leaf segment of the path and performs a case-insensitive substring match.
|
|
@@ -259,8 +299,4 @@ export default class TicketsDataProvider {
|
|
|
259
299
|
* @returns An object containing the categorized requirements and total processed count.
|
|
260
300
|
*/
|
|
261
301
|
GetCategorizedRequirementsByType(wiqlHref: string): Promise<any>;
|
|
262
|
-
/**
|
|
263
|
-
* Helper method to flatten a tree structure into a flat array of work items
|
|
264
|
-
*/
|
|
265
|
-
private flattenTreeToWorkItems;
|
|
266
302
|
}
|
|
@@ -50,7 +50,9 @@ class TicketsDataProvider {
|
|
|
50
50
|
const fieldsArr = Array.isArray(fieldsResp === null || fieldsResp === void 0 ? void 0 : fieldsResp.value) ? fieldsResp.value : [];
|
|
51
51
|
const candidates = fieldsArr
|
|
52
52
|
.filter((f) => {
|
|
53
|
-
const nm = String((f === null || f === void 0 ? void 0 : f.name) || '')
|
|
53
|
+
const nm = String((f === null || f === void 0 ? void 0 : f.name) || '')
|
|
54
|
+
.toLowerCase()
|
|
55
|
+
.replace(/_/g, ' ');
|
|
54
56
|
return nm.includes('requirement type');
|
|
55
57
|
})
|
|
56
58
|
.map((f) => f === null || f === void 0 ? void 0 : f.referenceName)
|
|
@@ -469,7 +471,7 @@ class TicketsDataProvider {
|
|
|
469
471
|
* @returns An object containing `systemRequirementsQueryTree`.
|
|
470
472
|
*/
|
|
471
473
|
async fetchSystemRequirementQueries(queries, excludedFolderNames = []) {
|
|
472
|
-
const { tree1: systemRequirementsQueryTree } = await this.structureFetchedQueries(queries, false, null, ['
|
|
474
|
+
const { tree1: systemRequirementsQueryTree } = await this.structureFetchedQueries(queries, false, null, ['epic', 'feature', 'requirement'], [], undefined, undefined, true, // Enable processing of both tree and direct link queries, including flat queries
|
|
473
475
|
excludedFolderNames, true);
|
|
474
476
|
return { systemRequirementsQueryTree };
|
|
475
477
|
}
|
|
@@ -515,7 +517,7 @@ class TicketsDataProvider {
|
|
|
515
517
|
* - `SoftwareToSystemRequirementsTree`: The tree representing Software → System Requirements traceability.
|
|
516
518
|
*/
|
|
517
519
|
async fetchLinkedRequirementsTraceQueries(queries, onlySourceSide = false) {
|
|
518
|
-
const { tree1: SystemToSoftwareRequirementsTree, tree2: SoftwareToSystemRequirementsTree } = await this.structureFetchedQueries(queries, onlySourceSide, null, ['
|
|
520
|
+
const { tree1: SystemToSoftwareRequirementsTree, tree2: SoftwareToSystemRequirementsTree } = await this.structureFetchedQueries(queries, onlySourceSide, null, ['epic', 'feature', 'requirement'], ['epic', 'feature', 'requirement'], 'sys', // Source area filter for tree1: System area paths
|
|
519
521
|
'soft' // Target area filter for tree1: Software area paths (tree2 will be reversed automatically)
|
|
520
522
|
);
|
|
521
523
|
return { SystemToSoftwareRequirementsTree, SoftwareToSystemRequirementsTree };
|
|
@@ -524,7 +526,7 @@ class TicketsDataProvider {
|
|
|
524
526
|
if (!folder) {
|
|
525
527
|
return null;
|
|
526
528
|
}
|
|
527
|
-
const { tree1 } = await this.structureFetchedQueries(folder, false, null, ['
|
|
529
|
+
const { tree1 } = await this.structureFetchedQueries(folder, false, null, ['epic', 'feature', 'requirement'], ['epic', 'feature', 'requirement'], undefined, undefined, true);
|
|
528
530
|
return tree1;
|
|
529
531
|
}
|
|
530
532
|
async findQueryFolderByName(rootQuery, folderName) {
|
|
@@ -1418,8 +1420,10 @@ class TicketsDataProvider {
|
|
|
1418
1420
|
* @returns A promise resolving to an object with `tree1` and `tree2` nodes, or `null` for each when none match.
|
|
1419
1421
|
* @throws Logs an error if an exception occurs during processing.
|
|
1420
1422
|
*/
|
|
1421
|
-
async structureFetchedQueries(rootQuery, onlyTestReq, parentId = null, sources, targets, sourceAreaFilter, targetAreaFilter, includeTreeQueries = false, excludedFolderNames = [], includeFlatQueries = false) {
|
|
1423
|
+
async structureFetchedQueries(rootQuery, onlyTestReq, parentId = null, sources, targets, sourceAreaFilter, targetAreaFilter, includeTreeQueries = false, excludedFolderNames = [], includeFlatQueries = false, workItemTypeCache) {
|
|
1422
1424
|
try {
|
|
1425
|
+
// Per-invocation cache for ID->WorkItemType lookups; avoids global state and is safe for concurrency.
|
|
1426
|
+
const typeCache = workItemTypeCache !== null && workItemTypeCache !== void 0 ? workItemTypeCache : new Map();
|
|
1423
1427
|
const shouldSkipFolder = (rootQuery === null || rootQuery === void 0 ? void 0 : rootQuery.isFolder) &&
|
|
1424
1428
|
excludedFolderNames.some((folderName) => folderName.toLowerCase() === (rootQuery.name || '').toLowerCase());
|
|
1425
1429
|
if (shouldSkipFolder) {
|
|
@@ -1436,12 +1440,10 @@ class TicketsDataProvider {
|
|
|
1436
1440
|
let tree2Node = null;
|
|
1437
1441
|
if (rootQuery.queryType === 'flat' && includeFlatQueries) {
|
|
1438
1442
|
const allTypes = Array.from(new Set([...(sources || []), ...(targets || [])]));
|
|
1439
|
-
const typesOk = this.
|
|
1443
|
+
const typesOk = await this.matchesFlatWorkItemTypeConditionAsync(rootQuery, wiql, allTypes, typeCache);
|
|
1440
1444
|
if (typesOk) {
|
|
1441
1445
|
const allowTree1 = !onlyTestReq &&
|
|
1442
|
-
(sourceAreaFilter
|
|
1443
|
-
? this.matchesFlatAreaCondition(wiql, sourceAreaFilter || '')
|
|
1444
|
-
: true);
|
|
1446
|
+
(sourceAreaFilter ? this.matchesFlatAreaCondition(wiql, sourceAreaFilter || '') : true);
|
|
1445
1447
|
const allowTree2 = targetAreaFilter
|
|
1446
1448
|
? this.matchesFlatAreaCondition(wiql, targetAreaFilter || '')
|
|
1447
1449
|
: true;
|
|
@@ -1454,7 +1456,12 @@ class TicketsDataProvider {
|
|
|
1454
1456
|
}
|
|
1455
1457
|
}
|
|
1456
1458
|
else {
|
|
1457
|
-
|
|
1459
|
+
let matchesForward = false;
|
|
1460
|
+
if (!onlyTestReq) {
|
|
1461
|
+
matchesForward = await this.matchesSourceTargetConditionAsync(rootQuery, wiql, sources, targets, typeCache);
|
|
1462
|
+
}
|
|
1463
|
+
const matchesReverse = await this.matchesSourceTargetConditionAsync(rootQuery, wiql, targets, sources, typeCache);
|
|
1464
|
+
if (matchesForward) {
|
|
1458
1465
|
const matchesAreaPath = sourceAreaFilter || targetAreaFilter
|
|
1459
1466
|
? this.matchesAreaPathCondition(wiql, sourceAreaFilter || '', targetAreaFilter || '')
|
|
1460
1467
|
: true;
|
|
@@ -1462,7 +1469,7 @@ class TicketsDataProvider {
|
|
|
1462
1469
|
tree1Node = this.buildQueryNode(rootQuery, parentId);
|
|
1463
1470
|
}
|
|
1464
1471
|
}
|
|
1465
|
-
if (
|
|
1472
|
+
if (matchesReverse) {
|
|
1466
1473
|
const matchesReverseAreaPath = sourceAreaFilter || targetAreaFilter
|
|
1467
1474
|
? this.matchesAreaPathCondition(wiql, targetAreaFilter || '', sourceAreaFilter || '')
|
|
1468
1475
|
: true;
|
|
@@ -1481,12 +1488,13 @@ class TicketsDataProvider {
|
|
|
1481
1488
|
if (!rootQuery.children) {
|
|
1482
1489
|
const queryUrl = `${rootQuery.url}?$depth=2&$expand=all`;
|
|
1483
1490
|
const currentQuery = await tfs_1.TFSServices.getItemContent(queryUrl, this.token);
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1491
|
+
if (!currentQuery) {
|
|
1492
|
+
return { tree1: null, tree2: null };
|
|
1493
|
+
}
|
|
1494
|
+
return await this.structureFetchedQueries(currentQuery, onlyTestReq, currentQuery.id, sources, targets, sourceAreaFilter, targetAreaFilter, includeTreeQueries, excludedFolderNames, includeFlatQueries, typeCache);
|
|
1487
1495
|
}
|
|
1488
1496
|
// Process children recursively
|
|
1489
|
-
const childResults = await Promise.all(rootQuery.children.map((child) => this.structureFetchedQueries(child, onlyTestReq, rootQuery.id, sources, targets, sourceAreaFilter, targetAreaFilter, includeTreeQueries, excludedFolderNames, includeFlatQueries)));
|
|
1497
|
+
const childResults = await Promise.all(rootQuery.children.map((child) => this.structureFetchedQueries(child, onlyTestReq, rootQuery.id, sources, targets, sourceAreaFilter, targetAreaFilter, includeTreeQueries, excludedFolderNames, includeFlatQueries, typeCache)));
|
|
1490
1498
|
// Build tree1
|
|
1491
1499
|
const tree1Children = childResults.map((res) => res.tree1).filter((child) => child !== null);
|
|
1492
1500
|
const tree1Node = tree1Children.length > 0
|
|
@@ -1528,7 +1536,8 @@ class TicketsDataProvider {
|
|
|
1528
1536
|
const srcFilter = (sourceAreaFilter || '').toLowerCase().trim();
|
|
1529
1537
|
const tgtFilter = (targetAreaFilter || '').toLowerCase().trim();
|
|
1530
1538
|
const extractAreaPaths = (owner) => {
|
|
1531
|
-
const
|
|
1539
|
+
const ownerPattern = `(?:${owner}|\\[${owner}\\])`;
|
|
1540
|
+
const re = new RegExp(`${ownerPattern}\\.\\[system\\.areapath\\][^']*'([^']+)'`, 'gi');
|
|
1532
1541
|
const results = [];
|
|
1533
1542
|
let match;
|
|
1534
1543
|
while ((match = re.exec(wiqlLower)) !== null) {
|
|
@@ -1548,73 +1557,21 @@ class TicketsDataProvider {
|
|
|
1548
1557
|
const hasTargetAreaPath = !tgtFilter || targetAreaPaths.some((p) => getLeaf(p).includes(tgtFilter));
|
|
1549
1558
|
return hasSourceAreaPath && hasTargetAreaPath;
|
|
1550
1559
|
}
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
* @param wiql - The WIQL string to evaluate.
|
|
1561
|
-
* @param source - An array of source work item types to check for in the WIQL.
|
|
1562
|
-
* @param target - An array of target work item types to check for in the WIQL.
|
|
1563
|
-
* @returns A boolean indicating whether the WIQL includes at least one valid source work item type
|
|
1564
|
-
* and at least one valid target work item type.
|
|
1565
|
-
*/
|
|
1566
|
-
matchesSourceTargetCondition(wiql, source, target) {
|
|
1567
|
-
const isSourceIncluded = this.matchesWorkItemTypeCondition(wiql, 'Source', source);
|
|
1568
|
-
const isTargetIncluded = this.matchesWorkItemTypeCondition(wiql, 'Target', target);
|
|
1569
|
-
return isSourceIncluded && isTargetIncluded;
|
|
1570
|
-
}
|
|
1571
|
-
/**
|
|
1572
|
-
* Helper method to check if a WIQL contains valid work item types for a given context (Source/Target).
|
|
1573
|
-
* Supports both = and IN operators.
|
|
1574
|
-
*
|
|
1575
|
-
* @param wiql - The WIQL string to evaluate
|
|
1576
|
-
* @param context - Either 'Source' or 'Target'
|
|
1577
|
-
* @param allowedTypes - Array of allowed work item types
|
|
1578
|
-
* @returns true if all work item types in the WIQL are in the allowedTypes array
|
|
1579
|
-
*/
|
|
1580
|
-
matchesWorkItemTypeCondition(wiql, context, allowedTypes) {
|
|
1581
|
-
// If allowedTypes is empty, accept any work item type (for backward compatibility)
|
|
1582
|
-
if (allowedTypes.length === 0) {
|
|
1583
|
-
return wiql.includes(`${context}.[System.WorkItemType]`);
|
|
1584
|
-
}
|
|
1585
|
-
const fieldPattern = `${context}.\\[System.WorkItemType\\]`;
|
|
1586
|
-
// Pattern for equality: Source.[System.WorkItemType] = 'Epic'
|
|
1587
|
-
const equalityRegex = new RegExp(`${fieldPattern}\\s*=\\s*'([^']+)'`, 'gi');
|
|
1588
|
-
// Pattern for IN operator: Source.[System.WorkItemType] IN ('Epic', 'Feature', 'Requirement')
|
|
1589
|
-
const inRegex = new RegExp(`${fieldPattern}\\s+IN\\s*\\(([^)]+)\\)`, 'gi');
|
|
1590
|
-
const foundTypes = new Set();
|
|
1591
|
-
// Extract types from equality operators
|
|
1592
|
-
let match;
|
|
1593
|
-
while ((match = equalityRegex.exec(wiql)) !== null) {
|
|
1594
|
-
foundTypes.add(match[1].trim());
|
|
1595
|
-
}
|
|
1596
|
-
// Extract types from IN operators
|
|
1597
|
-
while ((match = inRegex.exec(wiql)) !== null) {
|
|
1598
|
-
const typesString = match[1];
|
|
1599
|
-
// Extract all quoted values from the IN clause
|
|
1600
|
-
const typeMatches = typesString.matchAll(/'([^']+)'/g);
|
|
1601
|
-
for (const typeMatch of typeMatches) {
|
|
1602
|
-
foundTypes.add(typeMatch[1].trim());
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
// If no work item types found in WIQL, return false
|
|
1606
|
-
if (foundTypes.size === 0) {
|
|
1560
|
+
async matchesSourceTargetConditionAsync(queryNode, wiql, source, target, workItemTypeCache) {
|
|
1561
|
+
/**
|
|
1562
|
+
* Matches source+target constraints for link WIQL.
|
|
1563
|
+
* For each side (Source/Target) we accept either:
|
|
1564
|
+
* - explicit `[System.WorkItemType]` constraints (all types must be within the allowed list), or
|
|
1565
|
+
* - explicit `[System.Id]` constraints when no type constraint exists (type is fetched by id and validated).
|
|
1566
|
+
*/
|
|
1567
|
+
const sourceOk = await this.isLinkSideAllowedByTypeOrId(queryNode, wiql, 'Source', source, workItemTypeCache);
|
|
1568
|
+
if (!sourceOk)
|
|
1607
1569
|
return false;
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
return false;
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1616
|
-
// All found types are valid
|
|
1617
|
-
return true;
|
|
1570
|
+
const targetOk = await this.isLinkSideAllowedByTypeOrId(queryNode, wiql, 'Target', target, workItemTypeCache);
|
|
1571
|
+
return targetOk;
|
|
1572
|
+
}
|
|
1573
|
+
async matchesFlatWorkItemTypeConditionAsync(queryNode, wiql, allowedTypes, workItemTypeCache) {
|
|
1574
|
+
return this.isFlatQueryAllowedByTypeOrId(queryNode, wiql, allowedTypes, workItemTypeCache);
|
|
1618
1575
|
}
|
|
1619
1576
|
// Build a normalized node object for tree outputs
|
|
1620
1577
|
buildQueryNode(rootQuery, parentId) {
|
|
@@ -1630,44 +1587,213 @@ class TicketsDataProvider {
|
|
|
1630
1587
|
isValidQuery: true,
|
|
1631
1588
|
};
|
|
1632
1589
|
}
|
|
1590
|
+
getProjectFromQueryNode(queryNode) {
|
|
1591
|
+
var _a, _b, _c, _d, _e, _f;
|
|
1592
|
+
const href = ((_b = (_a = queryNode === null || queryNode === void 0 ? void 0 : queryNode._links) === null || _a === void 0 ? void 0 : _a.wiql) === null || _b === void 0 ? void 0 : _b.href) ||
|
|
1593
|
+
((_d = (_c = queryNode === null || queryNode === void 0 ? void 0 : queryNode._links) === null || _c === void 0 ? void 0 : _c.self) === null || _d === void 0 ? void 0 : _d.href) ||
|
|
1594
|
+
(queryNode === null || queryNode === void 0 ? void 0 : queryNode.url) ||
|
|
1595
|
+
((_f = (_e = queryNode === null || queryNode === void 0 ? void 0 : queryNode._links) === null || _e === void 0 ? void 0 : _e.html) === null || _f === void 0 ? void 0 : _f.href);
|
|
1596
|
+
if (!href)
|
|
1597
|
+
return null;
|
|
1598
|
+
return this.getProjectFromWiqlHref(String(href));
|
|
1599
|
+
}
|
|
1633
1600
|
/**
|
|
1634
|
-
*
|
|
1635
|
-
* Accept when at least one type is present and all found types are within the allowed set.
|
|
1601
|
+
* Normalizes allowed work item types into a lowercase set.
|
|
1636
1602
|
*/
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1603
|
+
normalizeAllowedTypes(allowedTypes) {
|
|
1604
|
+
return new Set((allowedTypes || []).map((t) => String(t).trim().toLowerCase()).filter(Boolean));
|
|
1605
|
+
}
|
|
1606
|
+
/**
|
|
1607
|
+
* Returns true when every value in `found` exists in `allowed`.
|
|
1608
|
+
*/
|
|
1609
|
+
areAllAllowed(found, allowed) {
|
|
1610
|
+
for (const v of found) {
|
|
1611
|
+
if (!allowed.has(v))
|
|
1612
|
+
return false;
|
|
1641
1613
|
}
|
|
1614
|
+
return true;
|
|
1615
|
+
}
|
|
1616
|
+
/**
|
|
1617
|
+
* Builds a regex-ready field selector for WIQL.
|
|
1618
|
+
* - Link queries: Source/Target can appear as `Source.[...]` or `[Source].[...]`
|
|
1619
|
+
* - Flat queries: no owner prefix, e.g. `[System.WorkItemType]`
|
|
1620
|
+
*/
|
|
1621
|
+
buildWiqlFieldPattern(owner, fieldRef) {
|
|
1622
|
+
const escapedField = String(fieldRef).replace(/\./g, '\\.');
|
|
1623
|
+
if (!owner) {
|
|
1624
|
+
return `\\[${escapedField}\\]`;
|
|
1625
|
+
}
|
|
1626
|
+
const ownerPattern = `(?:${owner}|\\[${owner}\\])`;
|
|
1627
|
+
return `${ownerPattern}\\.\\[${escapedField}\\]`;
|
|
1628
|
+
}
|
|
1629
|
+
/**
|
|
1630
|
+
* Extracts all quoted values used in `=` or `IN (...)` comparisons for a given field.
|
|
1631
|
+
* Returned values are trimmed and lowercased.
|
|
1632
|
+
*
|
|
1633
|
+
* Examples:
|
|
1634
|
+
* - `<field> = 'Epic'`
|
|
1635
|
+
* - `<field> IN ('Epic','Feature')`
|
|
1636
|
+
*/
|
|
1637
|
+
extractQuotedValuesForField(wiql, fieldPattern) {
|
|
1642
1638
|
const wiqlStr = String(wiql || '');
|
|
1643
|
-
const eqRe = /\[System\.WorkItemType\]\s*=\s*'([^']+)'/gi;
|
|
1644
|
-
const inRe = /\[System\.WorkItemType\]\s+IN\s*\(([^)]+)\)/gi;
|
|
1645
1639
|
const found = new Set();
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1640
|
+
const eqRe = new RegExp(`${fieldPattern}\\s*=\\s*'([^']+)'`, 'gi');
|
|
1641
|
+
const inRe = new RegExp(`${fieldPattern}\\s+IN\\s*\\(([^)]+)\\)`, 'gi');
|
|
1642
|
+
let match;
|
|
1643
|
+
while ((match = eqRe.exec(wiqlStr)) !== null) {
|
|
1644
|
+
found.add(String(match[1]).trim().toLowerCase());
|
|
1649
1645
|
}
|
|
1650
|
-
while ((
|
|
1651
|
-
const inner =
|
|
1646
|
+
while ((match = inRe.exec(wiqlStr)) !== null) {
|
|
1647
|
+
const inner = String(match[1] || '');
|
|
1652
1648
|
for (const mm of inner.matchAll(/'([^']+)'/g)) {
|
|
1653
1649
|
found.add(String(mm[1]).trim().toLowerCase());
|
|
1654
1650
|
}
|
|
1655
1651
|
}
|
|
1656
|
-
|
|
1652
|
+
return found;
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* Extracts all numeric values used in `=` or `IN (...)` comparisons for a given field.
|
|
1656
|
+
* Supports both quoted and unquoted numbers.
|
|
1657
|
+
*
|
|
1658
|
+
* Examples:
|
|
1659
|
+
* - `<field> = 123`
|
|
1660
|
+
* - `<field> IN (123, '456')`
|
|
1661
|
+
*/
|
|
1662
|
+
extractNumericValuesForField(wiql, fieldPattern) {
|
|
1663
|
+
const wiqlStr = String(wiql || '');
|
|
1664
|
+
const found = new Set();
|
|
1665
|
+
const eqRe = new RegExp(`${fieldPattern}\\s*=\\s*'?([0-9]+)'?`, 'gi');
|
|
1666
|
+
const inRe = new RegExp(`${fieldPattern}\\s+IN\\s*\\(([^)]+)\\)`, 'gi');
|
|
1667
|
+
let match;
|
|
1668
|
+
while ((match = eqRe.exec(wiqlStr)) !== null) {
|
|
1669
|
+
found.add(String(match[1]));
|
|
1670
|
+
}
|
|
1671
|
+
while ((match = inRe.exec(wiqlStr)) !== null) {
|
|
1672
|
+
const inner = String(match[1] || '');
|
|
1673
|
+
for (const mm of inner.matchAll(/'?([0-9]+)'?/g)) {
|
|
1674
|
+
found.add(String(mm[1]));
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
return found;
|
|
1678
|
+
}
|
|
1679
|
+
/**
|
|
1680
|
+
* Extracts work item IDs from WIQL.
|
|
1681
|
+
* - Link queries: pass `context` to match `Source.[System.Id]` / `Target.[System.Id]` (with or without `[Source]`)
|
|
1682
|
+
* - Flat queries: omit `context` to match `[System.Id]`
|
|
1683
|
+
*/
|
|
1684
|
+
extractWorkItemIdsFromWiql(wiql, context) {
|
|
1685
|
+
const fieldPattern = this.buildWiqlFieldPattern(context !== null && context !== void 0 ? context : null, 'System.Id');
|
|
1686
|
+
return [...this.extractNumericValuesForField(String(wiql || ''), fieldPattern).values()];
|
|
1687
|
+
}
|
|
1688
|
+
/**
|
|
1689
|
+
* Determines if a link-query side (Source/Target) is allowed based on WIQL constraints.
|
|
1690
|
+
*
|
|
1691
|
+
* Rules:
|
|
1692
|
+
* - If `allowedTypes` is empty: preserve legacy behavior by requiring that WorkItemType is present for that side.
|
|
1693
|
+
* - If WIQL contains a WorkItemType constraint for that side: all specified types must be within `allowedTypes`.
|
|
1694
|
+
* - Otherwise, if WIQL contains a System.Id constraint for that side: fetch the work item type(s) by id and validate.
|
|
1695
|
+
* - Otherwise: reject.
|
|
1696
|
+
*/
|
|
1697
|
+
async isLinkSideAllowedByTypeOrId(queryNode, wiql, context, allowedTypes, workItemTypeCache) {
|
|
1698
|
+
const wiqlStr = String(wiql || '');
|
|
1699
|
+
// Preserve existing behavior when no allowedTypes are provided.
|
|
1700
|
+
if (!allowedTypes || allowedTypes.length === 0) {
|
|
1701
|
+
const fieldPresenceRegex = new RegExp(`${this.buildWiqlFieldPattern(context, 'System.WorkItemType')}`, 'i');
|
|
1702
|
+
return fieldPresenceRegex.test(wiqlStr);
|
|
1703
|
+
}
|
|
1704
|
+
const allowed = this.normalizeAllowedTypes(allowedTypes);
|
|
1705
|
+
const typeFieldPattern = this.buildWiqlFieldPattern(context, 'System.WorkItemType');
|
|
1706
|
+
const typesInWiql = this.extractQuotedValuesForField(wiqlStr, typeFieldPattern);
|
|
1707
|
+
if (typesInWiql.size > 0) {
|
|
1708
|
+
return this.areAllAllowed(typesInWiql, allowed);
|
|
1709
|
+
}
|
|
1710
|
+
const ids = this.extractWorkItemIdsFromWiql(wiqlStr, context);
|
|
1711
|
+
if (ids.length === 0)
|
|
1657
1712
|
return false;
|
|
1658
|
-
const
|
|
1659
|
-
|
|
1660
|
-
|
|
1713
|
+
const project = this.getProjectFromQueryNode(queryNode);
|
|
1714
|
+
if (!project)
|
|
1715
|
+
return false;
|
|
1716
|
+
for (const id of ids) {
|
|
1717
|
+
const wiType = await this.getWorkItemTypeById(project, id, workItemTypeCache);
|
|
1718
|
+
if (!wiType)
|
|
1719
|
+
return false;
|
|
1720
|
+
if (!allowed.has(String(wiType).toLowerCase()))
|
|
1661
1721
|
return false;
|
|
1662
1722
|
}
|
|
1663
1723
|
return true;
|
|
1664
1724
|
}
|
|
1725
|
+
/**
|
|
1726
|
+
* Determines if a flat query is allowed based on WIQL constraints.
|
|
1727
|
+
*
|
|
1728
|
+
* Rules:
|
|
1729
|
+
* - If `allowedTypes` is empty: preserve legacy behavior by requiring that `[System.WorkItemType]` appears in WIQL.
|
|
1730
|
+
* - If WIQL contains a WorkItemType constraint: all specified types must be within `allowedTypes`.
|
|
1731
|
+
* - Otherwise, if WIQL contains `[System.Id]`: fetch the work item type(s) by id and validate.
|
|
1732
|
+
* - Otherwise: reject.
|
|
1733
|
+
*/
|
|
1734
|
+
async isFlatQueryAllowedByTypeOrId(queryNode, wiql, allowedTypes, workItemTypeCache) {
|
|
1735
|
+
const wiqlStr = String(wiql || '');
|
|
1736
|
+
// Preserve existing behavior when no allowedTypes are provided.
|
|
1737
|
+
if (!allowedTypes || allowedTypes.length === 0) {
|
|
1738
|
+
return /\[System\.WorkItemType\]/i.test(wiqlStr);
|
|
1739
|
+
}
|
|
1740
|
+
const allowed = this.normalizeAllowedTypes(allowedTypes);
|
|
1741
|
+
const typeFieldPattern = this.buildWiqlFieldPattern(null, 'System.WorkItemType');
|
|
1742
|
+
const typesInWiql = this.extractQuotedValuesForField(wiqlStr, typeFieldPattern);
|
|
1743
|
+
if (typesInWiql.size > 0) {
|
|
1744
|
+
return this.areAllAllowed(typesInWiql, allowed);
|
|
1745
|
+
}
|
|
1746
|
+
const ids = this.extractWorkItemIdsFromWiql(wiqlStr);
|
|
1747
|
+
if (ids.length === 0)
|
|
1748
|
+
return false;
|
|
1749
|
+
const project = this.getProjectFromQueryNode(queryNode);
|
|
1750
|
+
if (!project)
|
|
1751
|
+
return false;
|
|
1752
|
+
for (const id of ids) {
|
|
1753
|
+
const wiType = await this.getWorkItemTypeById(project, id, workItemTypeCache);
|
|
1754
|
+
if (!wiType)
|
|
1755
|
+
return false;
|
|
1756
|
+
if (!allowed.has(String(wiType).toLowerCase()))
|
|
1757
|
+
return false;
|
|
1758
|
+
}
|
|
1759
|
+
return true;
|
|
1760
|
+
}
|
|
1761
|
+
/**
|
|
1762
|
+
* Fetches the work item type for a given work item ID.
|
|
1763
|
+
* Uses caching to avoid repeated API calls.
|
|
1764
|
+
*
|
|
1765
|
+
* @param project The project name
|
|
1766
|
+
* @param id The work item ID
|
|
1767
|
+
* @param workItemTypeCache The cache map for work item types
|
|
1768
|
+
* @returns The work item type or null if not found
|
|
1769
|
+
*/
|
|
1770
|
+
async getWorkItemTypeById(project, id, workItemTypeCache) {
|
|
1771
|
+
var _a, _b;
|
|
1772
|
+
const cacheKey = `${project}:${id}`;
|
|
1773
|
+
if (workItemTypeCache.has(cacheKey)) {
|
|
1774
|
+
return (_a = workItemTypeCache.get(cacheKey)) !== null && _a !== void 0 ? _a : null;
|
|
1775
|
+
}
|
|
1776
|
+
try {
|
|
1777
|
+
const url = `${this.orgUrl}${project}/_apis/wit/workitems/${id}?fields=System.WorkItemType`;
|
|
1778
|
+
const wi = await this.limit(() => tfs_1.TFSServices.getItemContent(url, this.token));
|
|
1779
|
+
const wiType = (_b = wi === null || wi === void 0 ? void 0 : wi.fields) === null || _b === void 0 ? void 0 : _b['System.WorkItemType'];
|
|
1780
|
+
const normalized = wiType ? String(wiType) : null;
|
|
1781
|
+
workItemTypeCache.set(cacheKey, normalized);
|
|
1782
|
+
return normalized;
|
|
1783
|
+
}
|
|
1784
|
+
catch (e) {
|
|
1785
|
+
workItemTypeCache.set(cacheKey, null);
|
|
1786
|
+
return null;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1665
1789
|
/**
|
|
1666
1790
|
* Matches flat query WIQL against an area path filter by checking any referenced [System.AreaPath].
|
|
1667
1791
|
* Compares only the leaf segment of the path and performs a case-insensitive substring match.
|
|
1668
1792
|
*/
|
|
1669
1793
|
matchesFlatAreaCondition(wiql, areaFilter) {
|
|
1670
|
-
const filter = String(areaFilter || '')
|
|
1794
|
+
const filter = String(areaFilter || '')
|
|
1795
|
+
.trim()
|
|
1796
|
+
.toLowerCase();
|
|
1671
1797
|
if (!filter)
|
|
1672
1798
|
return true;
|
|
1673
1799
|
const wiqlLower = String(wiql || '').toLowerCase();
|
|
@@ -1917,28 +2043,6 @@ class TicketsDataProvider {
|
|
|
1917
2043
|
throw err;
|
|
1918
2044
|
}
|
|
1919
2045
|
}
|
|
1920
|
-
/**
|
|
1921
|
-
* Helper method to flatten a tree structure into a flat array of work items
|
|
1922
|
-
*/
|
|
1923
|
-
flattenTreeToWorkItems(roots) {
|
|
1924
|
-
const result = [];
|
|
1925
|
-
const traverse = (node) => {
|
|
1926
|
-
if (!node)
|
|
1927
|
-
return;
|
|
1928
|
-
result.push({
|
|
1929
|
-
id: node.id,
|
|
1930
|
-
title: node.title,
|
|
1931
|
-
description: node.description,
|
|
1932
|
-
htmlUrl: node.htmlUrl,
|
|
1933
|
-
url: node.htmlUrl, // Some nodes might use url instead
|
|
1934
|
-
});
|
|
1935
|
-
if (Array.isArray(node.children)) {
|
|
1936
|
-
node.children.forEach(traverse);
|
|
1937
|
-
}
|
|
1938
|
-
};
|
|
1939
|
-
roots.forEach(traverse);
|
|
1940
|
-
return result;
|
|
1941
|
-
}
|
|
1942
2046
|
}
|
|
1943
2047
|
exports.default = TicketsDataProvider;
|
|
1944
2048
|
//# sourceMappingURL=TicketsDataProvider.js.map
|