@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.
Files changed (92) hide show
  1. package/.github/workflows/ci.yml +26 -9
  2. package/.github/workflows/release.yml +9 -10
  3. package/README.md +50 -24
  4. package/bin/helpers/tfs.d.ts +3 -0
  5. package/bin/helpers/tfs.js +44 -7
  6. package/bin/helpers/tfs.js.map +1 -1
  7. package/bin/modules/GitDataProvider.d.ts +10 -0
  8. package/bin/modules/GitDataProvider.js +10 -0
  9. package/bin/modules/GitDataProvider.js.map +1 -1
  10. package/bin/modules/TestDataProvider.js +0 -1
  11. package/bin/modules/TestDataProvider.js.map +1 -1
  12. package/bin/modules/TicketsDataProvider.d.ts +63 -24
  13. package/bin/modules/TicketsDataProvider.js +216 -114
  14. package/bin/modules/TicketsDataProvider.js.map +1 -1
  15. package/bin/tests/helpers/helper.test.js +279 -0
  16. package/bin/tests/helpers/helper.test.js.map +1 -0
  17. package/bin/{helpers/test → tests/helpers}/tfs.test.js +312 -49
  18. package/bin/tests/helpers/tfs.test.js.map +1 -0
  19. package/bin/tests/index.test.js +25 -0
  20. package/bin/tests/index.test.js.map +1 -0
  21. package/bin/tests/models/tfs-data.test.js +160 -0
  22. package/bin/tests/models/tfs-data.test.js.map +1 -0
  23. package/bin/{modules/test → tests/modules}/JfrogDataProvider.test.js +9 -9
  24. package/bin/tests/modules/JfrogDataProvider.test.js.map +1 -0
  25. package/bin/tests/modules/ResultDataProvider.test.js +1942 -0
  26. package/bin/tests/modules/ResultDataProvider.test.js.map +1 -0
  27. package/bin/tests/modules/gitDataProvider.test.js +1888 -0
  28. package/bin/tests/modules/gitDataProvider.test.js.map +1 -0
  29. package/bin/{modules/test → tests/modules}/managmentDataProvider.test.js +13 -1
  30. package/bin/tests/modules/managmentDataProvider.test.js.map +1 -0
  31. package/bin/tests/modules/pipelineDataProvider.test.d.ts +1 -0
  32. package/bin/tests/modules/pipelineDataProvider.test.js +783 -0
  33. package/bin/tests/modules/pipelineDataProvider.test.js.map +1 -0
  34. package/bin/tests/modules/testDataProvider.test.d.ts +1 -0
  35. package/bin/tests/modules/testDataProvider.test.js +717 -0
  36. package/bin/tests/modules/testDataProvider.test.js.map +1 -0
  37. package/bin/tests/modules/ticketsDataProvider.test.d.ts +1 -0
  38. package/bin/tests/modules/ticketsDataProvider.test.js +1681 -0
  39. package/bin/tests/modules/ticketsDataProvider.test.js.map +1 -0
  40. package/bin/tests/utils/DataProviderUtils.test.d.ts +1 -0
  41. package/bin/tests/utils/DataProviderUtils.test.js +61 -0
  42. package/bin/tests/utils/DataProviderUtils.test.js.map +1 -0
  43. package/bin/tests/utils/testStepParserHelper.test.d.ts +1 -0
  44. package/bin/tests/utils/testStepParserHelper.test.js +359 -0
  45. package/bin/tests/utils/testStepParserHelper.test.js.map +1 -0
  46. package/package.json +9 -1
  47. package/src/helpers/tfs.ts +51 -7
  48. package/src/modules/GitDataProvider.ts +10 -0
  49. package/src/modules/TestDataProvider.ts +0 -1
  50. package/src/modules/TicketsDataProvider.ts +298 -141
  51. package/src/tests/helpers/helper.test.ts +337 -0
  52. package/src/tests/helpers/tfs.test.ts +1092 -0
  53. package/src/tests/index.test.ts +28 -0
  54. package/src/tests/models/tfs-data.test.ts +203 -0
  55. package/src/tests/modules/JfrogDataProvider.test.ts +167 -0
  56. package/src/tests/modules/ResultDataProvider.test.ts +2571 -0
  57. package/src/tests/modules/gitDataProvider.test.ts +2628 -0
  58. package/src/{modules/test → tests/modules}/managmentDataProvider.test.ts +33 -1
  59. package/src/tests/modules/pipelineDataProvider.test.ts +1038 -0
  60. package/src/tests/modules/testDataProvider.test.ts +1046 -0
  61. package/src/tests/modules/ticketsDataProvider.test.ts +2204 -0
  62. package/src/tests/utils/DataProviderUtils.test.ts +76 -0
  63. package/src/tests/utils/testStepParserHelper.test.ts +437 -0
  64. package/tsconfig.json +1 -0
  65. package/bin/helpers/test/tfs.test.js.map +0 -1
  66. package/bin/modules/test/JfrogDataProvider.test.js.map +0 -1
  67. package/bin/modules/test/ResultDataProvider.test.js +0 -444
  68. package/bin/modules/test/ResultDataProvider.test.js.map +0 -1
  69. package/bin/modules/test/gitDataProvider.test.js +0 -428
  70. package/bin/modules/test/gitDataProvider.test.js.map +0 -1
  71. package/bin/modules/test/managmentDataProvider.test.js.map +0 -1
  72. package/bin/modules/test/pipelineDataProvider.test.js +0 -237
  73. package/bin/modules/test/pipelineDataProvider.test.js.map +0 -1
  74. package/bin/modules/test/testDataProvider.test.js +0 -234
  75. package/bin/modules/test/testDataProvider.test.js.map +0 -1
  76. package/bin/modules/test/ticketsDataProvider.test.js +0 -348
  77. package/bin/modules/test/ticketsDataProvider.test.js.map +0 -1
  78. package/src/helpers/test/tfs.test.ts +0 -748
  79. package/src/modules/test/JfrogDataProvider.test.ts +0 -171
  80. package/src/modules/test/ResultDataProvider.test.ts +0 -542
  81. package/src/modules/test/gitDataProvider.test.ts +0 -645
  82. package/src/modules/test/pipelineDataProvider.test.ts +0 -292
  83. package/src/modules/test/testDataProvider.test.ts +0 -318
  84. package/src/modules/test/ticketsDataProvider.test.ts +0 -462
  85. /package/bin/{helpers/test/tfs.test.d.ts → tests/helpers/helper.test.d.ts} +0 -0
  86. /package/bin/{modules/test/JfrogDataProvider.test.d.ts → tests/helpers/tfs.test.d.ts} +0 -0
  87. /package/bin/{modules/test/ResultDataProvider.test.d.ts → tests/index.test.d.ts} +0 -0
  88. /package/bin/{modules/test/gitDataProvider.test.d.ts → tests/models/tfs-data.test.d.ts} +0 -0
  89. /package/bin/{modules/test/managmentDataProvider.test.d.ts → tests/modules/JfrogDataProvider.test.d.ts} +0 -0
  90. /package/bin/{modules/test/pipelineDataProvider.test.d.ts → tests/modules/ResultDataProvider.test.d.ts} +0 -0
  91. /package/bin/{modules/test/testDataProvider.test.d.ts → tests/modules/gitDataProvider.test.d.ts} +0 -0
  92. /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
- * Determines whether the given WIQL (Work Item Query Language) string matches the specified
205
- * source and target conditions. It checks if the WIQL contains references to the specified
206
- * source and target work item types.
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
- * Supports both equality (=) and IN operators:
209
- * - Source.[System.WorkItemType] = 'Epic'
210
- * - Source.[System.WorkItemType] IN ('Epic', 'Feature', 'Requirement')
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
- * @returns A boolean indicating whether the WIQL includes at least one valid source work item type
213
- * and at least one valid target work item type.
234
+ * Examples:
235
+ * - `<field> = 123`
236
+ * - `<field> IN (123, '456')`
214
237
  */
215
- private matchesSourceTargetCondition;
238
+ private extractNumericValuesForField;
216
239
  /**
217
- * Helper method to check if a WIQL contains valid work item types for a given context (Source/Target).
218
- * Supports both = and IN operators.
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
- * @param wiql - The WIQL string to evaluate
221
- * @param context - Either 'Source' or 'Target'
222
- * @param allowedTypes - Array of allowed work item types
223
- * @returns true if all work item types in the WIQL are in the allowedTypes array
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 matchesWorkItemTypeCondition;
226
- private buildQueryNode;
254
+ private isLinkSideAllowedByTypeOrId;
227
255
  /**
228
- * Matches flat query WIQL against allowed work item types.
229
- * Accept when at least one type is present and all found types are within the allowed set.
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 matchesFlatWorkItemTypeCondition;
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.matchesFlatWorkItemTypeCondition(wiql, allTypes);
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
- if (!onlyTestReq && this.matchesSourceTargetCondition(wiql, sources, targets)) {
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 (this.matchesSourceTargetCondition(wiql, targets, sources)) {
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
- return currentQuery
1485
- ? await this.structureFetchedQueries(currentQuery, onlyTestReq, currentQuery.id, sources, targets, sourceAreaFilter, targetAreaFilter, includeTreeQueries, excludedFolderNames, includeFlatQueries)
1486
- : { tree1: null, tree2: null };
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 re = new RegExp(`${owner}\\.\\[system\\.areapath\\][^']*'([^']+)'`, 'gi');
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
- * Determines whether the given WIQL (Work Item Query Language) string matches the specified
1553
- * source and target conditions. It checks if the WIQL contains references to the specified
1554
- * source and target work item types.
1555
- *
1556
- * Supports both equality (=) and IN operators:
1557
- * - Source.[System.WorkItemType] = 'Epic'
1558
- * - Source.[System.WorkItemType] IN ('Epic', 'Feature', 'Requirement')
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
- // Check if all found types are in the allowedTypes array (case-insensitive)
1609
- const allowedSet = new Set(allowedTypes.map((t) => String(t).toLowerCase()));
1610
- for (const type of foundTypes) {
1611
- if (!allowedSet.has(type)) {
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
- * Matches flat query WIQL against allowed work item types.
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
- matchesFlatWorkItemTypeCondition(wiql, allowedTypes) {
1638
- // If allowedTypes is empty, accept any work item type reference
1639
- if (!allowedTypes || allowedTypes.length === 0) {
1640
- return /\[System\.WorkItemType\]/i.test(wiql || '');
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
- let m;
1647
- while ((m = eqRe.exec(wiqlStr)) !== null) {
1648
- found.add(m[1].trim().toLowerCase());
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 ((m = inRe.exec(wiqlStr)) !== null) {
1651
- const inner = m[1];
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
- if (found.size === 0)
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 allowed = new Set(allowedTypes.map((t) => String(t).toLowerCase()));
1659
- for (const t of found) {
1660
- if (!allowed.has(t))
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