@elisra-devops/docgen-data-provider 1.139.0 → 1.140.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/bin/modules/TicketsDataProvider.d.ts +3 -3
- package/bin/modules/TicketsDataProvider.js +67 -44
- package/bin/modules/TicketsDataProvider.js.map +1 -1
- package/bin/tests/modules/ticketsDataProvider.historical.test.js +111 -42
- package/bin/tests/modules/ticketsDataProvider.historical.test.js.map +1 -1
- package/package.json +1 -1
- package/src/modules/TicketsDataProvider.ts +78 -60
- package/src/tests/modules/ticketsDataProvider.historical.test.ts +128 -43
|
@@ -10,7 +10,7 @@ export default class TicketsDataProvider {
|
|
|
10
10
|
token: string;
|
|
11
11
|
queriesList: Array<any>;
|
|
12
12
|
private limit;
|
|
13
|
-
private
|
|
13
|
+
private historicalTestPhaseFieldByProject;
|
|
14
14
|
constructor(orgUrl: string, token: string);
|
|
15
15
|
/**
|
|
16
16
|
* Extracts the Azure DevOps project name from a WIQL URL.
|
|
@@ -235,8 +235,8 @@ export default class TicketsDataProvider {
|
|
|
235
235
|
private historicalErrorMessage;
|
|
236
236
|
private isHistoricalMissingWorkItemError;
|
|
237
237
|
private isHistoricalUnknownFieldError;
|
|
238
|
-
private
|
|
239
|
-
private
|
|
238
|
+
private resolveHistoricalTestPhaseField;
|
|
239
|
+
private discoverHistoricalTestPhaseField;
|
|
240
240
|
private fetchHistoricalFieldsBatch;
|
|
241
241
|
private fetchHistoricalWorkItemsBatch;
|
|
242
242
|
private executeHistoricalQueryAtAsOf;
|
|
@@ -46,9 +46,11 @@ class TicketsDataProvider {
|
|
|
46
46
|
this.token = '';
|
|
47
47
|
this.queriesList = new Array();
|
|
48
48
|
this.limit = pLimit(10);
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
49
|
+
// Resolved once per project via resolveHistoricalTestPhaseField, so a
|
|
50
|
+
// compare's baseline and compareTo snapshots (and repeated queries against
|
|
51
|
+
// the same provider instance) only pay the discovery call once. Value is
|
|
52
|
+
// the real reference name, or null if the project defines no such field.
|
|
53
|
+
this.historicalTestPhaseFieldByProject = new Map();
|
|
52
54
|
this.orgUrl = orgUrl;
|
|
53
55
|
this.token = token;
|
|
54
56
|
}
|
|
@@ -1636,59 +1638,80 @@ class TicketsDataProvider {
|
|
|
1636
1638
|
const message = this.historicalErrorMessage(error);
|
|
1637
1639
|
return /TF51535|Cannot find field/i.test(message);
|
|
1638
1640
|
}
|
|
1639
|
-
//
|
|
1640
|
-
//
|
|
1641
|
-
//
|
|
1642
|
-
//
|
|
1643
|
-
//
|
|
1644
|
-
//
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1641
|
+
// Different networks define the "Test Phase" custom field under different
|
|
1642
|
+
// reference names (Elisra.TestPhase vs Custom.TestPhase, or something else
|
|
1643
|
+
// entirely on a third network) — hardcoding candidate names and guessing is
|
|
1644
|
+
// inherently fragile. Resolve the project's real field list once (Fields -
|
|
1645
|
+
// List: GET {org}/{project}/_apis/wit/fields, supported since old TFS API
|
|
1646
|
+
// versions) and use whichever reference name actually matches, by
|
|
1647
|
+
// reference name or by friendly name. Resolves to null if the project
|
|
1648
|
+
// defines no such field at all.
|
|
1649
|
+
//
|
|
1650
|
+
// Caches the in-flight promise, not just the resolved value: a compare's
|
|
1651
|
+
// baseline and compareTo snapshots run concurrently against the same
|
|
1652
|
+
// provider instance, and both would otherwise read the cache before either
|
|
1653
|
+
// had a value to store, issuing the discovery request twice.
|
|
1654
|
+
resolveHistoricalTestPhaseField(project, apiVersion) {
|
|
1655
|
+
const cached = this.historicalTestPhaseFieldByProject.get(project);
|
|
1656
|
+
if (cached)
|
|
1657
|
+
return cached;
|
|
1658
|
+
const promise = this.discoverHistoricalTestPhaseField(project, apiVersion);
|
|
1659
|
+
this.historicalTestPhaseFieldByProject.set(project, promise);
|
|
1660
|
+
return promise;
|
|
1661
|
+
}
|
|
1662
|
+
async discoverHistoricalTestPhaseField(project, apiVersion) {
|
|
1663
|
+
const fieldsUrl = this.appendApiVersion(`${this.orgUrl}${project}/_apis/wit/fields`, apiVersion);
|
|
1664
|
+
try {
|
|
1665
|
+
const response = await tfs_1.TFSServices.getItemContent(fieldsUrl, this.token);
|
|
1666
|
+
const fields = Array.isArray(response === null || response === void 0 ? void 0 : response.value) ? response.value : [];
|
|
1667
|
+
const match = fields.find((field) => HISTORICAL_OPTIONAL_WORK_ITEM_FIELDS.some((candidate) => String((field === null || field === void 0 ? void 0 : field.referenceName) || '').toLowerCase() === candidate.toLowerCase())) || fields.find((field) => /test\s*phase/i.test(String((field === null || field === void 0 ? void 0 : field.name) || '')));
|
|
1668
|
+
const resolved = (match === null || match === void 0 ? void 0 : match.referenceName) ? String(match.referenceName) : null;
|
|
1669
|
+
if (!resolved) {
|
|
1670
|
+
logger_1.default.warn(`[historical-workitems] no Test Phase field defined for project '${project}'`);
|
|
1671
|
+
}
|
|
1672
|
+
return resolved;
|
|
1673
|
+
}
|
|
1674
|
+
catch (error) {
|
|
1675
|
+
logger_1.default.warn(`[historical-workitems] could not list fields for project '${project}' to resolve the ` +
|
|
1676
|
+
`Test Phase field name, omitting it: ${this.historicalErrorMessage(error)}`);
|
|
1677
|
+
return null;
|
|
1678
|
+
}
|
|
1651
1679
|
}
|
|
1652
|
-
|
|
1680
|
+
fetchHistoricalFieldsBatch(workItemsBatchUrl, idChunk, asOf, project, fieldsForRequest) {
|
|
1653
1681
|
return this.limit(() => tfs_1.TFSServices.getItemContent(workItemsBatchUrl, this.token, 'post', {
|
|
1654
1682
|
ids: idChunk,
|
|
1655
1683
|
asOf,
|
|
1656
1684
|
fields: fieldsForRequest,
|
|
1657
1685
|
errorPolicy: 'Omit',
|
|
1658
1686
|
})).catch((error) => {
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
// required field, so an unrelated schema issue still surfaces as an
|
|
1667
|
-
// error instead of silently losing data.
|
|
1668
|
-
const reducedFields = unknownField
|
|
1669
|
-
? fieldsForRequest.filter((field) => field !== unknownField)
|
|
1670
|
-
: fieldsForRequest.filter((field) => !HISTORICAL_OPTIONAL_WORK_ITEM_FIELDS.includes(field));
|
|
1671
|
-
if (reducedFields.length === fieldsForRequest.length) {
|
|
1687
|
+
// Defensive net: the resolved field name came from the project's own
|
|
1688
|
+
// field list, so this shouldn't fire, but if it somehow still does,
|
|
1689
|
+
// fall back to the required fields and poison the cache so later
|
|
1690
|
+
// chunks/queries for this project stop requesting it too.
|
|
1691
|
+
const hasOptionalField = HISTORICAL_OPTIONAL_WORK_ITEM_FIELDS.some((f) => fieldsForRequest.includes(f))
|
|
1692
|
+
|| fieldsForRequest.some((f) => !HISTORICAL_REQUIRED_WORK_ITEM_FIELDS.includes(f));
|
|
1693
|
+
if (!hasOptionalField || !this.isHistoricalUnknownFieldError(error)) {
|
|
1672
1694
|
throw error;
|
|
1673
1695
|
}
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
:
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
return this.fetchHistoricalFieldsBatchAttempt(workItemsBatchUrl, idChunk, asOf, reducedFields, attemptsRemaining - 1);
|
|
1696
|
+
logger_1.default.warn(`[historical-workitems] resolved Test Phase field was rejected by workitemsbatch for ` +
|
|
1697
|
+
`project '${project}'; retrying without it: ${this.historicalErrorMessage(error)}`);
|
|
1698
|
+
this.historicalTestPhaseFieldByProject.set(project, Promise.resolve(null));
|
|
1699
|
+
return this.limit(() => tfs_1.TFSServices.getItemContent(workItemsBatchUrl, this.token, 'post', {
|
|
1700
|
+
ids: idChunk,
|
|
1701
|
+
asOf,
|
|
1702
|
+
fields: HISTORICAL_REQUIRED_WORK_ITEM_FIELDS,
|
|
1703
|
+
errorPolicy: 'Omit',
|
|
1704
|
+
}));
|
|
1684
1705
|
});
|
|
1685
1706
|
}
|
|
1686
|
-
fetchHistoricalFieldsBatch(workItemsBatchUrl, idChunk, asOf) {
|
|
1687
|
-
const fieldsForRequest = this.historicalFieldsOverride || HISTORICAL_WORK_ITEM_FIELDS;
|
|
1688
|
-
return this.fetchHistoricalFieldsBatchAttempt(workItemsBatchUrl, idChunk, asOf, fieldsForRequest, HISTORICAL_OPTIONAL_WORK_ITEM_FIELDS.length);
|
|
1689
|
-
}
|
|
1690
1707
|
async fetchHistoricalWorkItemsBatch(project, ids, asOf, apiVersion) {
|
|
1691
1708
|
const workItemsBatchUrl = this.appendApiVersion(`${this.orgUrl}${project}/_apis/wit/workitemsbatch`, apiVersion);
|
|
1709
|
+
// Resolved once per project, before any chunk starts, so every chunk
|
|
1710
|
+
// shares the same field list — no per-request guessing or retry race.
|
|
1711
|
+
const testPhaseField = await this.resolveHistoricalTestPhaseField(project, apiVersion);
|
|
1712
|
+
const fieldsForRequest = testPhaseField
|
|
1713
|
+
? [...HISTORICAL_REQUIRED_WORK_ITEM_FIELDS, testPhaseField]
|
|
1714
|
+
: HISTORICAL_REQUIRED_WORK_ITEM_FIELDS;
|
|
1692
1715
|
const idChunks = this.chunkHistoricalWorkItemIds(ids);
|
|
1693
1716
|
try {
|
|
1694
1717
|
const chunkResults = await Promise.all(idChunks.map((idChunk) =>
|
|
@@ -1704,7 +1727,7 @@ class TicketsDataProvider {
|
|
|
1704
1727
|
// two units against the shared concurrency cap, not one — otherwise
|
|
1705
1728
|
// the real ceiling on simultaneous ADO calls would silently double.
|
|
1706
1729
|
Promise.all([
|
|
1707
|
-
this.fetchHistoricalFieldsBatch(workItemsBatchUrl, idChunk, asOf),
|
|
1730
|
+
this.fetchHistoricalFieldsBatch(workItemsBatchUrl, idChunk, asOf, project, fieldsForRequest),
|
|
1708
1731
|
this.limit(() => tfs_1.TFSServices.getItemContent(workItemsBatchUrl, this.token, 'post', {
|
|
1709
1732
|
ids: idChunk,
|
|
1710
1733
|
asOf,
|