@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.
Files changed (94) hide show
  1. package/.github/workflows/ci.yml +26 -9
  2. package/.github/workflows/release.yml +9 -10
  3. package/bin/helpers/tfs.d.ts +3 -0
  4. package/bin/helpers/tfs.js +44 -7
  5. package/bin/helpers/tfs.js.map +1 -1
  6. package/bin/modules/GitDataProvider.d.ts +10 -0
  7. package/bin/modules/GitDataProvider.js +10 -0
  8. package/bin/modules/GitDataProvider.js.map +1 -1
  9. package/bin/modules/MangementDataProvider.js +7 -1
  10. package/bin/modules/MangementDataProvider.js.map +1 -1
  11. package/bin/modules/TestDataProvider.js +0 -1
  12. package/bin/modules/TestDataProvider.js.map +1 -1
  13. package/bin/modules/TicketsDataProvider.d.ts +63 -27
  14. package/bin/modules/TicketsDataProvider.js +226 -122
  15. package/bin/modules/TicketsDataProvider.js.map +1 -1
  16. package/bin/tests/helpers/helper.test.js +279 -0
  17. package/bin/tests/helpers/helper.test.js.map +1 -0
  18. package/bin/{helpers/test → tests/helpers}/tfs.test.js +312 -49
  19. package/bin/tests/helpers/tfs.test.js.map +1 -0
  20. package/bin/tests/index.test.js +25 -0
  21. package/bin/tests/index.test.js.map +1 -0
  22. package/bin/tests/models/tfs-data.test.js +160 -0
  23. package/bin/tests/models/tfs-data.test.js.map +1 -0
  24. package/bin/{modules/test → tests/modules}/JfrogDataProvider.test.js +9 -9
  25. package/bin/tests/modules/JfrogDataProvider.test.js.map +1 -0
  26. package/bin/tests/modules/ResultDataProvider.test.js +1942 -0
  27. package/bin/tests/modules/ResultDataProvider.test.js.map +1 -0
  28. package/bin/tests/modules/gitDataProvider.test.js +1888 -0
  29. package/bin/tests/modules/gitDataProvider.test.js.map +1 -0
  30. package/bin/{modules/test → tests/modules}/managmentDataProvider.test.js +39 -31
  31. package/bin/tests/modules/managmentDataProvider.test.js.map +1 -0
  32. package/bin/tests/modules/pipelineDataProvider.test.d.ts +1 -0
  33. package/bin/tests/modules/pipelineDataProvider.test.js +783 -0
  34. package/bin/tests/modules/pipelineDataProvider.test.js.map +1 -0
  35. package/bin/tests/modules/testDataProvider.test.d.ts +1 -0
  36. package/bin/tests/modules/testDataProvider.test.js +717 -0
  37. package/bin/tests/modules/testDataProvider.test.js.map +1 -0
  38. package/bin/tests/modules/ticketsDataProvider.test.d.ts +1 -0
  39. package/bin/tests/modules/ticketsDataProvider.test.js +1681 -0
  40. package/bin/tests/modules/ticketsDataProvider.test.js.map +1 -0
  41. package/bin/tests/utils/DataProviderUtils.test.d.ts +1 -0
  42. package/bin/tests/utils/DataProviderUtils.test.js +61 -0
  43. package/bin/tests/utils/DataProviderUtils.test.js.map +1 -0
  44. package/bin/tests/utils/testStepParserHelper.test.d.ts +1 -0
  45. package/bin/tests/utils/testStepParserHelper.test.js +359 -0
  46. package/bin/tests/utils/testStepParserHelper.test.js.map +1 -0
  47. package/package.json +10 -1
  48. package/src/helpers/tfs.ts +51 -7
  49. package/src/modules/GitDataProvider.ts +10 -0
  50. package/src/modules/MangementDataProvider.ts +6 -1
  51. package/src/modules/TestDataProvider.ts +0 -1
  52. package/src/modules/TicketsDataProvider.ts +311 -151
  53. package/src/tests/helpers/helper.test.ts +337 -0
  54. package/src/tests/helpers/tfs.test.ts +1092 -0
  55. package/src/tests/index.test.ts +28 -0
  56. package/src/tests/models/tfs-data.test.ts +203 -0
  57. package/src/tests/modules/JfrogDataProvider.test.ts +167 -0
  58. package/src/tests/modules/ResultDataProvider.test.ts +2571 -0
  59. package/src/tests/modules/gitDataProvider.test.ts +2628 -0
  60. package/src/{modules/test → tests/modules}/managmentDataProvider.test.ts +63 -32
  61. package/src/tests/modules/pipelineDataProvider.test.ts +1038 -0
  62. package/src/tests/modules/testDataProvider.test.ts +1046 -0
  63. package/src/tests/modules/ticketsDataProvider.test.ts +2204 -0
  64. package/src/tests/utils/DataProviderUtils.test.ts +76 -0
  65. package/src/tests/utils/testStepParserHelper.test.ts +437 -0
  66. package/tsconfig.json +1 -0
  67. package/bin/helpers/test/tfs.test.js.map +0 -1
  68. package/bin/modules/test/JfrogDataProvider.test.js.map +0 -1
  69. package/bin/modules/test/ResultDataProvider.test.js +0 -444
  70. package/bin/modules/test/ResultDataProvider.test.js.map +0 -1
  71. package/bin/modules/test/gitDataProvider.test.js +0 -433
  72. package/bin/modules/test/gitDataProvider.test.js.map +0 -1
  73. package/bin/modules/test/managmentDataProvider.test.js.map +0 -1
  74. package/bin/modules/test/pipelineDataProvider.test.js +0 -237
  75. package/bin/modules/test/pipelineDataProvider.test.js.map +0 -1
  76. package/bin/modules/test/testDataProvider.test.js +0 -234
  77. package/bin/modules/test/testDataProvider.test.js.map +0 -1
  78. package/bin/modules/test/ticketsDataProvider.test.js +0 -322
  79. package/bin/modules/test/ticketsDataProvider.test.js.map +0 -1
  80. package/src/helpers/test/tfs.test.ts +0 -748
  81. package/src/modules/test/JfrogDataProvider.test.ts +0 -171
  82. package/src/modules/test/ResultDataProvider.test.ts +0 -542
  83. package/src/modules/test/gitDataProvider.test.ts +0 -691
  84. package/src/modules/test/pipelineDataProvider.test.ts +0 -292
  85. package/src/modules/test/testDataProvider.test.ts +0 -318
  86. package/src/modules/test/ticketsDataProvider.test.ts +0 -434
  87. /package/bin/{helpers/test/tfs.test.d.ts → tests/helpers/helper.test.d.ts} +0 -0
  88. /package/bin/{modules/test/JfrogDataProvider.test.d.ts → tests/helpers/tfs.test.d.ts} +0 -0
  89. /package/bin/{modules/test/ResultDataProvider.test.d.ts → tests/index.test.d.ts} +0 -0
  90. /package/bin/{modules/test/gitDataProvider.test.d.ts → tests/models/tfs-data.test.d.ts} +0 -0
  91. /package/bin/{modules/test/managmentDataProvider.test.d.ts → tests/modules/JfrogDataProvider.test.d.ts} +0 -0
  92. /package/bin/{modules/test/pipelineDataProvider.test.d.ts → tests/modules/ResultDataProvider.test.d.ts} +0 -0
  93. /package/bin/{modules/test/testDataProvider.test.d.ts → tests/modules/gitDataProvider.test.d.ts} +0 -0
  94. /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
- * 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
- * @param wiql - The WIQL string to evaluate.
213
- * @param source - An array of source work item types to check for in the WIQL.
214
- * @param target - An array of target work item types to check for in the WIQL.
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 matchesSourceTargetCondition;
238
+ private extractNumericValuesForField;
219
239
  /**
220
- * Helper method to check if a WIQL contains valid work item types for a given context (Source/Target).
221
- * 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.
222
247
  *
223
- * @param wiql - The WIQL string to evaluate
224
- * @param context - Either 'Source' or 'Target'
225
- * @param allowedTypes - Array of allowed work item types
226
- * @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.
227
253
  */
228
- private matchesWorkItemTypeCondition;
229
- private buildQueryNode;
254
+ private isLinkSideAllowedByTypeOrId;
230
255
  /**
231
- * Matches flat query WIQL against allowed work item types.
232
- * 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.
233
263
  */
234
- 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;
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) || '').toLowerCase().replace(/_/g, ' ');
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, ['Epic', 'Feature', 'Requirement'], [], undefined, undefined, true, // Enable processing of both tree and direct link queries, including flat queries
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, ['Epic', 'Feature', 'Requirement'], ['Epic', 'Feature', 'Requirement'], 'sys', // Source area filter for tree1: System area paths
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, ['Epic', 'Feature', 'Requirement'], ['Epic', 'Feature', 'Requirement'], undefined, undefined, true);
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.matchesFlatWorkItemTypeCondition(wiql, allTypes);
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
- 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
- * @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
- // Check if all found types are in the allowedTypes array
1610
- for (const type of foundTypes) {
1611
- if (!allowedTypes.includes(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,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
- * 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;
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
- 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)
1657
1712
  return false;
1658
- const allowed = new Set(allowedTypes.map((t) => String(t).toLowerCase()));
1659
- for (const t of found) {
1660
- if (!allowed.has(t))
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 || '').trim().toLowerCase();
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