@elisra-devops/docgen-data-provider 1.137.0 → 1.138.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,6 +10,7 @@ export default class TicketsDataProvider {
10
10
  token: string;
11
11
  queriesList: Array<any>;
12
12
  private limit;
13
+ private historicalFieldsOverride;
13
14
  constructor(orgUrl: string, token: string);
14
15
  /**
15
16
  * Extracts the Azure DevOps project name from a WIQL URL.
@@ -214,6 +215,10 @@ export default class TicketsDataProvider {
214
215
  private normalizeHistoricalQueryRoot;
215
216
  private historicalErrorMessage;
216
217
  private isHistoricalMissingWorkItemError;
218
+ private isHistoricalUnknownFieldError;
219
+ private extractHistoricalUnknownFieldName;
220
+ private fetchHistoricalFieldsBatchAttempt;
221
+ private fetchHistoricalFieldsBatch;
217
222
  private fetchHistoricalWorkItemsBatch;
218
223
  private executeHistoricalQueryAtAsOf;
219
224
  private toHistoricalWorkItemSnapshot;
@@ -12,7 +12,7 @@ const logger_1 = require("../utils/logger");
12
12
  const pLimit = require('p-limit');
13
13
  const HISTORICAL_WIT_API_VERSIONS = ['7.1', '5.1', null];
14
14
  const HISTORICAL_BATCH_MAX_IDS = 200;
15
- const HISTORICAL_WORK_ITEM_FIELDS = [
15
+ const HISTORICAL_REQUIRED_WORK_ITEM_FIELDS = [
16
16
  'System.Id',
17
17
  'System.WorkItemType',
18
18
  'System.Title',
@@ -23,8 +23,19 @@ const HISTORICAL_WORK_ITEM_FIELDS = [
23
23
  'System.ChangedDate',
24
24
  'System.Description',
25
25
  'Microsoft.VSTS.TCM.Steps',
26
- 'Elisra.TestPhase',
27
- 'Custom.TestPhase',
26
+ ];
27
+ // Process-template-specific: not every project defines either of these.
28
+ // workitemsbatch's `fields` filter validates every requested reference name
29
+ // against the project's schema and throws TF51535 ("Cannot find field ...")
30
+ // if one is missing — unlike an unfiltered per-item GET, which just omits an
31
+ // unknown field from the response. toHistoricalWorkItemSnapshot already
32
+ // reads TestPhase via a 4-way fallback expecting it might be absent, so it's
33
+ // safe to drop these on a TF51535 and retry without them (see
34
+ // fetchHistoricalWorkItemsBatch).
35
+ const HISTORICAL_OPTIONAL_WORK_ITEM_FIELDS = ['Elisra.TestPhase', 'Custom.TestPhase'];
36
+ const HISTORICAL_WORK_ITEM_FIELDS = [
37
+ ...HISTORICAL_REQUIRED_WORK_ITEM_FIELDS,
38
+ ...HISTORICAL_OPTIONAL_WORK_ITEM_FIELDS,
28
39
  ];
29
40
  /** Default fields fetched per work item in tree/flat query parsing. */
30
41
  const WI_DEFAULT_FIELDS = 'System.Description,System.Title,System.WorkItemType,Microsoft.VSTS.TCM.ReproSteps,Microsoft.VSTS.CMMI.Symptom';
@@ -34,6 +45,9 @@ class TicketsDataProvider {
34
45
  this.token = '';
35
46
  this.queriesList = new Array();
36
47
  this.limit = pLimit(10);
48
+ // Set once a workitemsbatch call reports one of HISTORICAL_OPTIONAL_WORK_ITEM_FIELDS
49
+ // as unknown for this project, so only the first chunk pays the retry cost.
50
+ this.historicalFieldsOverride = null;
37
51
  this.orgUrl = orgUrl;
38
52
  this.token = token;
39
53
  }
@@ -1571,19 +1585,106 @@ class TicketsDataProvider {
1571
1585
  }
1572
1586
  return status === 404 || status === 410;
1573
1587
  }
1588
+ isHistoricalUnknownFieldError(error) {
1589
+ const message = this.historicalErrorMessage(error);
1590
+ return /TF51535|Cannot find field/i.test(message);
1591
+ }
1592
+ // ADO's TF51535 names the specific field it couldn't find, e.g.
1593
+ // "TF51535: Cannot find field 'Elisra.TestPhase'." — different networks
1594
+ // define this optional field under different reference names (Elisra.* vs
1595
+ // Custom.*), so drop only the one ADO actually rejected rather than every
1596
+ // optional field, or a network that defines Custom.TestPhase but not
1597
+ // Elisra.TestPhase would lose Test Phase comparison entirely for no reason.
1598
+ extractHistoricalUnknownFieldName(error) {
1599
+ const message = this.historicalErrorMessage(error);
1600
+ const quoted = message.match(/Cannot find field\s*'([^']+)'/i);
1601
+ if (quoted === null || quoted === void 0 ? void 0 : quoted[1])
1602
+ return quoted[1];
1603
+ return HISTORICAL_OPTIONAL_WORK_ITEM_FIELDS.find((field) => message.includes(field)) || null;
1604
+ }
1605
+ fetchHistoricalFieldsBatchAttempt(workItemsBatchUrl, idChunk, asOf, fieldsForRequest, attemptsRemaining) {
1606
+ return this.limit(() => tfs_1.TFSServices.getItemContent(workItemsBatchUrl, this.token, 'post', {
1607
+ ids: idChunk,
1608
+ asOf,
1609
+ fields: fieldsForRequest,
1610
+ errorPolicy: 'Omit',
1611
+ })).catch((error) => {
1612
+ if (attemptsRemaining <= 0 || !this.isHistoricalUnknownFieldError(error)) {
1613
+ throw error;
1614
+ }
1615
+ const unknownField = this.extractHistoricalUnknownFieldName(error);
1616
+ // Only strip fields this call actually recognizes as droppable
1617
+ // (named explicitly, or one of the known optional fields as a
1618
+ // last-resort fallback if the message doesn't parse) — never strip a
1619
+ // required field, so an unrelated schema issue still surfaces as an
1620
+ // error instead of silently losing data.
1621
+ const reducedFields = unknownField
1622
+ ? fieldsForRequest.filter((field) => field !== unknownField)
1623
+ : fieldsForRequest.filter((field) => !HISTORICAL_OPTIONAL_WORK_ITEM_FIELDS.includes(field));
1624
+ if (reducedFields.length === fieldsForRequest.length) {
1625
+ throw error;
1626
+ }
1627
+ // Concurrent chunks can each discover a different bad field before any
1628
+ // of them updates the shared override — intersect rather than
1629
+ // overwrite, so one chunk's retry never re-requests a field another
1630
+ // chunk already proved invalid for this project.
1631
+ this.historicalFieldsOverride = this.historicalFieldsOverride
1632
+ ? this.historicalFieldsOverride.filter((field) => reducedFields.includes(field))
1633
+ : reducedFields;
1634
+ logger_1.default.warn(`[historical-workitems] field${unknownField ? ` '${unknownField}'` : '(s)'} not defined ` +
1635
+ `for this project; retrying workitemsbatch without it: ${this.historicalErrorMessage(error)}`);
1636
+ return this.fetchHistoricalFieldsBatchAttempt(workItemsBatchUrl, idChunk, asOf, reducedFields, attemptsRemaining - 1);
1637
+ });
1638
+ }
1639
+ fetchHistoricalFieldsBatch(workItemsBatchUrl, idChunk, asOf) {
1640
+ const fieldsForRequest = this.historicalFieldsOverride || HISTORICAL_WORK_ITEM_FIELDS;
1641
+ return this.fetchHistoricalFieldsBatchAttempt(workItemsBatchUrl, idChunk, asOf, fieldsForRequest, HISTORICAL_OPTIONAL_WORK_ITEM_FIELDS.length);
1642
+ }
1574
1643
  async fetchHistoricalWorkItemsBatch(project, ids, asOf, apiVersion) {
1575
1644
  const workItemsBatchUrl = this.appendApiVersion(`${this.orgUrl}${project}/_apis/wit/workitemsbatch`, apiVersion);
1576
1645
  const idChunks = this.chunkHistoricalWorkItemIds(ids);
1577
1646
  try {
1578
- const batchResponses = await Promise.all(idChunks.map((idChunk) => this.limit(() => tfs_1.TFSServices.getItemContent(workItemsBatchUrl, this.token, 'post', {
1579
- ids: idChunk,
1580
- asOf,
1581
- $expand: 'Relations',
1582
- fields: HISTORICAL_WORK_ITEM_FIELDS,
1583
- }))));
1647
+ const chunkResults = await Promise.all(idChunks.map((idChunk) =>
1648
+ // ADO rejects a workitemsbatch request that combines `fields` with
1649
+ // `$expand` in the same call ("the expand parameter can not be used
1650
+ // with the fields parameter"), so fields and relations are fetched
1651
+ // in two separate batched passes and merged by id below.
1652
+ // errorPolicy: 'Omit' keeps one inaccessible/deleted work item from
1653
+ // failing the whole chunk (ADO's default is 'Fail'), matching the
1654
+ // per-item fallback's tolerance for missing items.
1655
+ // Each pass gets its own this.limit(...) slot (rather than wrapping
1656
+ // both in one) so the two concurrent calls per chunk still count as
1657
+ // two units against the shared concurrency cap, not one — otherwise
1658
+ // the real ceiling on simultaneous ADO calls would silently double.
1659
+ Promise.all([
1660
+ this.fetchHistoricalFieldsBatch(workItemsBatchUrl, idChunk, asOf),
1661
+ this.limit(() => tfs_1.TFSServices.getItemContent(workItemsBatchUrl, this.token, 'post', {
1662
+ ids: idChunk,
1663
+ asOf,
1664
+ $expand: 'Relations',
1665
+ errorPolicy: 'Omit',
1666
+ })),
1667
+ ]).then(([fieldsBatch, relationsBatch]) => {
1668
+ const relationsById = new Map();
1669
+ (Array.isArray(relationsBatch === null || relationsBatch === void 0 ? void 0 : relationsBatch.value) ? relationsBatch.value : []).forEach((item) => {
1670
+ const relId = Number(item === null || item === void 0 ? void 0 : item.id);
1671
+ if (Number.isFinite(relId)) {
1672
+ relationsById.set(relId, Array.isArray(item === null || item === void 0 ? void 0 : item.relations) ? item.relations : []);
1673
+ }
1674
+ });
1675
+ const fieldsValue = Array.isArray(fieldsBatch === null || fieldsBatch === void 0 ? void 0 : fieldsBatch.value) ? fieldsBatch.value : [];
1676
+ const returnedIds = new Set(fieldsValue.map((item) => Number(item === null || item === void 0 ? void 0 : item.id)));
1677
+ const items = fieldsValue.map((item) => (Object.assign(Object.assign({}, item), { relations: relationsById.get(Number(item === null || item === void 0 ? void 0 : item.id)) || [] })));
1678
+ const skippedWorkItemIds = idChunk.filter((id) => !returnedIds.has(id));
1679
+ if (skippedWorkItemIds.length > 0) {
1680
+ logger_1.default.warn(`[historical-workitems] workitemsbatch omitted ${skippedWorkItemIds.length} ` +
1681
+ `inaccessible/missing work item(s) for asOf ${asOf}: ${skippedWorkItemIds.join(', ')}`);
1682
+ }
1683
+ return { items, skippedWorkItemIds };
1684
+ })));
1584
1685
  return {
1585
- items: batchResponses.flatMap((batch) => (Array.isArray(batch === null || batch === void 0 ? void 0 : batch.value) ? batch.value : [])),
1586
- skippedWorkItemIds: [],
1686
+ items: chunkResults.flatMap((chunk) => chunk.items),
1687
+ skippedWorkItemIds: chunkResults.flatMap((chunk) => chunk.skippedWorkItemIds),
1587
1688
  };
1588
1689
  }
1589
1690
  catch (error) {