@elisra-devops/docgen-data-provider 1.63.13 → 1.68.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/README.md +50 -24
- 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/TestDataProvider.js +0 -1
- package/bin/modules/TestDataProvider.js.map +1 -1
- package/bin/modules/TicketsDataProvider.d.ts +63 -24
- package/bin/modules/TicketsDataProvider.js +216 -114
- 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 +13 -1
- 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 +9 -1
- package/src/helpers/tfs.ts +51 -7
- package/src/modules/GitDataProvider.ts +10 -0
- package/src/modules/TestDataProvider.ts +0 -1
- package/src/modules/TicketsDataProvider.ts +298 -141
- 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 +33 -1
- 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 -428
- 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 -348
- 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 -645
- 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 -462
- /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,35 +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
|
-
*
|
|
234
|
+
* Examples:
|
|
235
|
+
* - `<field> = 123`
|
|
236
|
+
* - `<field> IN (123, '456')`
|
|
214
237
|
*/
|
|
215
|
-
private
|
|
238
|
+
private extractNumericValuesForField;
|
|
216
239
|
/**
|
|
217
|
-
*
|
|
218
|
-
*
|
|
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.
|
|
219
247
|
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
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.
|
|
224
253
|
*/
|
|
225
|
-
private
|
|
226
|
-
private buildQueryNode;
|
|
254
|
+
private isLinkSideAllowedByTypeOrId;
|
|
227
255
|
/**
|
|
228
|
-
*
|
|
229
|
-
*
|
|
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.
|
|
230
263
|
*/
|
|
231
|
-
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;
|
|
232
275
|
/**
|
|
233
276
|
* Matches flat query WIQL against an area path filter by checking any referenced [System.AreaPath].
|
|
234
277
|
* Compares only the leaf segment of the path and performs a case-insensitive substring match.
|
|
@@ -256,8 +299,4 @@ export default class TicketsDataProvider {
|
|
|
256
299
|
* @returns An object containing the categorized requirements and total processed count.
|
|
257
300
|
*/
|
|
258
301
|
GetCategorizedRequirementsByType(wiqlHref: string): Promise<any>;
|
|
259
|
-
/**
|
|
260
|
-
* Helper method to flatten a tree structure into a flat array of work items
|
|
261
|
-
*/
|
|
262
|
-
private flattenTreeToWorkItems;
|
|
263
302
|
}
|
|
@@ -1420,8 +1420,10 @@ class TicketsDataProvider {
|
|
|
1420
1420
|
* @returns A promise resolving to an object with `tree1` and `tree2` nodes, or `null` for each when none match.
|
|
1421
1421
|
* @throws Logs an error if an exception occurs during processing.
|
|
1422
1422
|
*/
|
|
1423
|
-
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) {
|
|
1424
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();
|
|
1425
1427
|
const shouldSkipFolder = (rootQuery === null || rootQuery === void 0 ? void 0 : rootQuery.isFolder) &&
|
|
1426
1428
|
excludedFolderNames.some((folderName) => folderName.toLowerCase() === (rootQuery.name || '').toLowerCase());
|
|
1427
1429
|
if (shouldSkipFolder) {
|
|
@@ -1438,7 +1440,7 @@ class TicketsDataProvider {
|
|
|
1438
1440
|
let tree2Node = null;
|
|
1439
1441
|
if (rootQuery.queryType === 'flat' && includeFlatQueries) {
|
|
1440
1442
|
const allTypes = Array.from(new Set([...(sources || []), ...(targets || [])]));
|
|
1441
|
-
const typesOk = this.
|
|
1443
|
+
const typesOk = await this.matchesFlatWorkItemTypeConditionAsync(rootQuery, wiql, allTypes, typeCache);
|
|
1442
1444
|
if (typesOk) {
|
|
1443
1445
|
const allowTree1 = !onlyTestReq &&
|
|
1444
1446
|
(sourceAreaFilter ? this.matchesFlatAreaCondition(wiql, sourceAreaFilter || '') : 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
|
-
* @returns A boolean indicating whether the WIQL includes at least one valid source work item type
|
|
1561
|
-
* and at least one valid target work item type.
|
|
1562
|
-
*/
|
|
1563
|
-
matchesSourceTargetCondition(wiql, source, target) {
|
|
1564
|
-
const isSourceIncluded = this.matchesWorkItemTypeCondition(wiql, 'Source', source);
|
|
1565
|
-
const isTargetIncluded = this.matchesWorkItemTypeCondition(wiql, 'Target', target);
|
|
1566
|
-
return isSourceIncluded && isTargetIncluded;
|
|
1567
|
-
}
|
|
1568
|
-
/**
|
|
1569
|
-
* Helper method to check if a WIQL contains valid work item types for a given context (Source/Target).
|
|
1570
|
-
* Supports both = and IN operators.
|
|
1571
|
-
*
|
|
1572
|
-
* @param wiql - The WIQL string to evaluate
|
|
1573
|
-
* @param context - Either 'Source' or 'Target'
|
|
1574
|
-
* @param allowedTypes - Array of allowed work item types
|
|
1575
|
-
* @returns true if all work item types in the WIQL are in the allowedTypes array
|
|
1576
|
-
*/
|
|
1577
|
-
matchesWorkItemTypeCondition(wiql, context, allowedTypes) {
|
|
1578
|
-
const wiqlStr = String(wiql || '');
|
|
1579
|
-
// If allowedTypes is empty, accept any work item type (for backward compatibility)
|
|
1580
|
-
if (!allowedTypes || allowedTypes.length === 0) {
|
|
1581
|
-
const fieldPresenceRegex = new RegExp(`${context}\\.\\[System.WorkItemType\\]`, 'i');
|
|
1582
|
-
return fieldPresenceRegex.test(wiqlStr);
|
|
1583
|
-
}
|
|
1584
|
-
const fieldPattern = `${context}.\\[System.WorkItemType\\]`;
|
|
1585
|
-
// Pattern for equality: Source.[System.WorkItemType] = 'Epic'
|
|
1586
|
-
const equalityRegex = new RegExp(`${fieldPattern}\\s*=\\s*'([^']+)'`, 'gi');
|
|
1587
|
-
// Pattern for IN operator: Source.[System.WorkItemType] IN ('Epic', 'Feature', 'Requirement')
|
|
1588
|
-
const inRegex = new RegExp(`${fieldPattern}\\s+IN\\s*\\(([^)]+)\\)`, 'gi');
|
|
1589
|
-
const foundTypes = new Set();
|
|
1590
|
-
let match;
|
|
1591
|
-
// Extract types from equality operators
|
|
1592
|
-
while ((match = equalityRegex.exec(wiqlStr)) !== null) {
|
|
1593
|
-
foundTypes.add(String(match[1]).trim().toLowerCase());
|
|
1594
|
-
}
|
|
1595
|
-
// Extract types from IN operators
|
|
1596
|
-
while ((match = inRegex.exec(wiqlStr)) !== null) {
|
|
1597
|
-
const typesString = match[1];
|
|
1598
|
-
// Extract all quoted values from the IN clause
|
|
1599
|
-
const typeMatches = typesString.matchAll(/'([^']+)'/g);
|
|
1600
|
-
for (const typeMatch of typeMatches) {
|
|
1601
|
-
foundTypes.add(String(typeMatch[1]).trim().toLowerCase());
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1604
|
-
// If no work item types found in WIQL, return false
|
|
1605
|
-
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)
|
|
1606
1569
|
return false;
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
// Found a type that's not in the allowed list - reject this query
|
|
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,38 +1587,205 @@ 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;
|
|
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}\\]`;
|
|
1641
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)
|
|
1712
|
+
return false;
|
|
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()))
|
|
1721
|
+
return false;
|
|
1722
|
+
}
|
|
1723
|
+
return true;
|
|
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)
|
|
1657
1748
|
return false;
|
|
1658
|
-
const
|
|
1659
|
-
|
|
1660
|
-
|
|
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()))
|
|
1661
1757
|
return false;
|
|
1662
1758
|
}
|
|
1663
1759
|
return true;
|
|
1664
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.
|
|
@@ -1919,28 +2043,6 @@ class TicketsDataProvider {
|
|
|
1919
2043
|
throw err;
|
|
1920
2044
|
}
|
|
1921
2045
|
}
|
|
1922
|
-
/**
|
|
1923
|
-
* Helper method to flatten a tree structure into a flat array of work items
|
|
1924
|
-
*/
|
|
1925
|
-
flattenTreeToWorkItems(roots) {
|
|
1926
|
-
const result = [];
|
|
1927
|
-
const traverse = (node) => {
|
|
1928
|
-
if (!node)
|
|
1929
|
-
return;
|
|
1930
|
-
result.push({
|
|
1931
|
-
id: node.id,
|
|
1932
|
-
title: node.title,
|
|
1933
|
-
description: node.description,
|
|
1934
|
-
htmlUrl: node.htmlUrl,
|
|
1935
|
-
url: node.htmlUrl, // Some nodes might use url instead
|
|
1936
|
-
});
|
|
1937
|
-
if (Array.isArray(node.children)) {
|
|
1938
|
-
node.children.forEach(traverse);
|
|
1939
|
-
}
|
|
1940
|
-
};
|
|
1941
|
-
roots.forEach(traverse);
|
|
1942
|
-
return result;
|
|
1943
|
-
}
|
|
1944
2046
|
}
|
|
1945
2047
|
exports.default = TicketsDataProvider;
|
|
1946
2048
|
//# sourceMappingURL=TicketsDataProvider.js.map
|