@elisra-devops/docgen-data-provider 1.106.0 → 1.108.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/release.yml +28 -8
- package/README.md +7 -0
- package/bin/modules/TicketsDataProvider.d.ts +39 -0
- package/bin/modules/TicketsDataProvider.js +564 -2
- package/bin/modules/TicketsDataProvider.js.map +1 -1
- package/bin/tests/modules/ticketsDataProvider.historical.test.d.ts +1 -0
- package/bin/tests/modules/ticketsDataProvider.historical.test.js +741 -0
- package/bin/tests/modules/ticketsDataProvider.historical.test.js.map +1 -0
- package/bin/tests/modules/ticketsDataProvider.test.js +34 -1
- package/bin/tests/modules/ticketsDataProvider.test.js.map +1 -1
- package/package.json +1 -1
- package/src/modules/TicketsDataProvider.ts +719 -2
- package/src/tests/modules/ticketsDataProvider.historical.test.ts +876 -0
- package/src/tests/modules/ticketsDataProvider.test.ts +50 -1
|
@@ -10,6 +10,22 @@ const tfs_data_5 = require("../models/tfs-data");
|
|
|
10
10
|
const tfs_data_6 = require("../models/tfs-data");
|
|
11
11
|
const logger_1 = require("../utils/logger");
|
|
12
12
|
const pLimit = require('p-limit');
|
|
13
|
+
const HISTORICAL_WIT_API_VERSIONS = ['7.1', '5.1', null];
|
|
14
|
+
const HISTORICAL_BATCH_MAX_IDS = 200;
|
|
15
|
+
const HISTORICAL_WORK_ITEM_FIELDS = [
|
|
16
|
+
'System.Id',
|
|
17
|
+
'System.WorkItemType',
|
|
18
|
+
'System.Title',
|
|
19
|
+
'System.State',
|
|
20
|
+
'System.AreaPath',
|
|
21
|
+
'System.IterationPath',
|
|
22
|
+
'System.Rev',
|
|
23
|
+
'System.ChangedDate',
|
|
24
|
+
'System.Description',
|
|
25
|
+
'Microsoft.VSTS.TCM.Steps',
|
|
26
|
+
'Elisra.TestPhase',
|
|
27
|
+
'Custom.TestPhase',
|
|
28
|
+
];
|
|
13
29
|
/** Default fields fetched per work item in tree/flat query parsing. */
|
|
14
30
|
const WI_DEFAULT_FIELDS = 'System.Description,System.Title,Microsoft.VSTS.TCM.ReproSteps,Microsoft.VSTS.CMMI.Symptom';
|
|
15
31
|
class TicketsDataProvider {
|
|
@@ -165,14 +181,24 @@ class TicketsDataProvider {
|
|
|
165
181
|
* @param docType document type
|
|
166
182
|
* @returns
|
|
167
183
|
*/
|
|
184
|
+
normalizeSharedQueriesPath(path) {
|
|
185
|
+
const raw = String(path || '').trim();
|
|
186
|
+
if (!raw)
|
|
187
|
+
return '';
|
|
188
|
+
const normalized = raw.toLowerCase();
|
|
189
|
+
if (normalized === 'shared' || normalized === 'shared queries')
|
|
190
|
+
return '';
|
|
191
|
+
return raw;
|
|
192
|
+
}
|
|
168
193
|
async GetSharedQueries(project, path, docType = '') {
|
|
169
194
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
|
|
170
195
|
let url;
|
|
171
196
|
try {
|
|
172
|
-
|
|
197
|
+
const normalizedPath = this.normalizeSharedQueriesPath(path);
|
|
198
|
+
if (normalizedPath === '')
|
|
173
199
|
url = `${this.orgUrl}${project}/_apis/wit/queries/Shared%20Queries?$depth=2&$expand=all`;
|
|
174
200
|
else
|
|
175
|
-
url = `${this.orgUrl}${project}/_apis/wit/queries/${
|
|
201
|
+
url = `${this.orgUrl}${project}/_apis/wit/queries/${normalizedPath}?$depth=2&$expand=all`;
|
|
176
202
|
let queries = await tfs_1.TFSServices.getItemContent(url, this.token);
|
|
177
203
|
logger_1.default.debug(`doctype: ${docType}`);
|
|
178
204
|
const normalizedDocType = (docType || '').toLowerCase();
|
|
@@ -348,6 +374,13 @@ class TicketsDataProvider {
|
|
|
348
374
|
knownBugsQueryTree: (_y = knownBugsFetch === null || knownBugsFetch === void 0 ? void 0 : knownBugsFetch.result) !== null && _y !== void 0 ? _y : null,
|
|
349
375
|
};
|
|
350
376
|
}
|
|
377
|
+
case 'historical-query':
|
|
378
|
+
case 'historical': {
|
|
379
|
+
const { tree1 } = await this.structureAllQueryPath(queriesWithChildren);
|
|
380
|
+
return {
|
|
381
|
+
historicalQueryTree: tree1 ? [tree1] : [],
|
|
382
|
+
};
|
|
383
|
+
}
|
|
351
384
|
default:
|
|
352
385
|
break;
|
|
353
386
|
}
|
|
@@ -1189,6 +1222,535 @@ class TicketsDataProvider {
|
|
|
1189
1222
|
var wiql = querie._links.wiql;
|
|
1190
1223
|
return await this.GetQueryResultsByWiqlHref(wiql.href, project);
|
|
1191
1224
|
}
|
|
1225
|
+
normalizeHistoricalAsOf(value) {
|
|
1226
|
+
const raw = String(value || '').trim();
|
|
1227
|
+
if (!raw) {
|
|
1228
|
+
throw new Error('asOf date-time is required');
|
|
1229
|
+
}
|
|
1230
|
+
const parsed = new Date(raw);
|
|
1231
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
1232
|
+
throw new Error(`Invalid date-time value: ${raw}`);
|
|
1233
|
+
}
|
|
1234
|
+
return parsed.toISOString();
|
|
1235
|
+
}
|
|
1236
|
+
normalizeHistoricalCompareValue(value) {
|
|
1237
|
+
if (value == null) {
|
|
1238
|
+
return '';
|
|
1239
|
+
}
|
|
1240
|
+
if (typeof value === 'string') {
|
|
1241
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
1242
|
+
}
|
|
1243
|
+
if (Array.isArray(value)) {
|
|
1244
|
+
return value
|
|
1245
|
+
.map((item) => this.normalizeHistoricalCompareValue(item))
|
|
1246
|
+
.filter((item) => item !== '')
|
|
1247
|
+
.sort((a, b) => a.localeCompare(b))
|
|
1248
|
+
.join('; ');
|
|
1249
|
+
}
|
|
1250
|
+
if (typeof value === 'object') {
|
|
1251
|
+
const fallback = value;
|
|
1252
|
+
if (typeof fallback.displayName === 'string') {
|
|
1253
|
+
return this.normalizeHistoricalCompareValue(fallback.displayName);
|
|
1254
|
+
}
|
|
1255
|
+
if (typeof fallback.name === 'string') {
|
|
1256
|
+
return this.normalizeHistoricalCompareValue(fallback.name);
|
|
1257
|
+
}
|
|
1258
|
+
return this.normalizeHistoricalCompareValue(JSON.stringify(value));
|
|
1259
|
+
}
|
|
1260
|
+
return String(value).trim();
|
|
1261
|
+
}
|
|
1262
|
+
normalizeTestPhaseValue(value) {
|
|
1263
|
+
const rendered = this.normalizeHistoricalCompareValue(value);
|
|
1264
|
+
if (!rendered) {
|
|
1265
|
+
return '';
|
|
1266
|
+
}
|
|
1267
|
+
const parts = rendered
|
|
1268
|
+
.split(/[;,]/)
|
|
1269
|
+
.map((part) => part.trim())
|
|
1270
|
+
.filter((part) => part.length > 0);
|
|
1271
|
+
if (parts.length <= 1) {
|
|
1272
|
+
return rendered;
|
|
1273
|
+
}
|
|
1274
|
+
const unique = Array.from(new Set(parts));
|
|
1275
|
+
unique.sort((a, b) => a.localeCompare(b));
|
|
1276
|
+
return unique.join('; ');
|
|
1277
|
+
}
|
|
1278
|
+
isTestCaseType(workItemType) {
|
|
1279
|
+
const normalized = String(workItemType || '')
|
|
1280
|
+
.trim()
|
|
1281
|
+
.toLowerCase();
|
|
1282
|
+
return normalized === 'test case' || normalized === 'testcase';
|
|
1283
|
+
}
|
|
1284
|
+
toHistoricalRevision(value) {
|
|
1285
|
+
const n = Number(value);
|
|
1286
|
+
if (!Number.isFinite(n)) {
|
|
1287
|
+
return null;
|
|
1288
|
+
}
|
|
1289
|
+
return n;
|
|
1290
|
+
}
|
|
1291
|
+
extractHistoricalWorkItemIds(queryResult) {
|
|
1292
|
+
var _a, _b;
|
|
1293
|
+
const ids = new Set();
|
|
1294
|
+
const pushId = (candidate) => {
|
|
1295
|
+
const n = Number(candidate);
|
|
1296
|
+
if (Number.isFinite(n)) {
|
|
1297
|
+
ids.add(n);
|
|
1298
|
+
}
|
|
1299
|
+
};
|
|
1300
|
+
const workItems = Array.isArray(queryResult === null || queryResult === void 0 ? void 0 : queryResult.workItems) ? queryResult.workItems : [];
|
|
1301
|
+
for (const workItem of workItems) {
|
|
1302
|
+
pushId(workItem === null || workItem === void 0 ? void 0 : workItem.id);
|
|
1303
|
+
}
|
|
1304
|
+
const workItemRelations = Array.isArray(queryResult === null || queryResult === void 0 ? void 0 : queryResult.workItemRelations)
|
|
1305
|
+
? queryResult.workItemRelations
|
|
1306
|
+
: [];
|
|
1307
|
+
for (const relation of workItemRelations) {
|
|
1308
|
+
pushId((_a = relation === null || relation === void 0 ? void 0 : relation.source) === null || _a === void 0 ? void 0 : _a.id);
|
|
1309
|
+
pushId((_b = relation === null || relation === void 0 ? void 0 : relation.target) === null || _b === void 0 ? void 0 : _b.id);
|
|
1310
|
+
}
|
|
1311
|
+
return Array.from(ids.values()).sort((a, b) => a - b);
|
|
1312
|
+
}
|
|
1313
|
+
appendAsOfToWiql(wiql, asOfIso) {
|
|
1314
|
+
const raw = String(wiql || '').trim();
|
|
1315
|
+
if (!raw) {
|
|
1316
|
+
return '';
|
|
1317
|
+
}
|
|
1318
|
+
const withoutAsOf = raw.replace(/\s+ASOF\s+'[^']*'/i, '');
|
|
1319
|
+
return `${withoutAsOf} ASOF '${asOfIso}'`;
|
|
1320
|
+
}
|
|
1321
|
+
appendApiVersion(url, apiVersion) {
|
|
1322
|
+
if (!apiVersion) {
|
|
1323
|
+
return url;
|
|
1324
|
+
}
|
|
1325
|
+
const separator = url.includes('?') ? '&' : '?';
|
|
1326
|
+
return `${url}${separator}api-version=${encodeURIComponent(apiVersion)}`;
|
|
1327
|
+
}
|
|
1328
|
+
shouldRetryHistoricalWithLowerVersion(error) {
|
|
1329
|
+
var _a, _b, _c;
|
|
1330
|
+
const status = Number(((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status) || 0);
|
|
1331
|
+
if (status === 401 || status === 403) {
|
|
1332
|
+
return false;
|
|
1333
|
+
}
|
|
1334
|
+
if (status >= 500) {
|
|
1335
|
+
return true;
|
|
1336
|
+
}
|
|
1337
|
+
if ([400, 404, 405, 406, 410].includes(status)) {
|
|
1338
|
+
return true;
|
|
1339
|
+
}
|
|
1340
|
+
const message = String(((_c = (_b = error === null || error === void 0 ? void 0 : error.response) === null || _b === void 0 ? void 0 : _b.data) === null || _c === void 0 ? void 0 : _c.message) || (error === null || error === void 0 ? void 0 : error.message) || '');
|
|
1341
|
+
return /api[- ]?version/i.test(message);
|
|
1342
|
+
}
|
|
1343
|
+
async withHistoricalApiVersionFallback(operation, task) {
|
|
1344
|
+
var _a;
|
|
1345
|
+
let lastError = null;
|
|
1346
|
+
for (const apiVersion of HISTORICAL_WIT_API_VERSIONS) {
|
|
1347
|
+
try {
|
|
1348
|
+
const result = await task(apiVersion);
|
|
1349
|
+
return { apiVersion, result };
|
|
1350
|
+
}
|
|
1351
|
+
catch (error) {
|
|
1352
|
+
lastError = error;
|
|
1353
|
+
const status = Number(((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status) || 0);
|
|
1354
|
+
const apiVersionLabel = apiVersion || 'default';
|
|
1355
|
+
logger_1.default.warn(`[${operation}] failed with api-version=${apiVersionLabel}${status ? ` (status ${status})` : ''}`);
|
|
1356
|
+
if (!this.shouldRetryHistoricalWithLowerVersion(error) ||
|
|
1357
|
+
apiVersion === HISTORICAL_WIT_API_VERSIONS[HISTORICAL_WIT_API_VERSIONS.length - 1]) {
|
|
1358
|
+
throw error;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
throw lastError || new Error(`[${operation}] Failed to execute historical request`);
|
|
1363
|
+
}
|
|
1364
|
+
chunkHistoricalWorkItemIds(ids) {
|
|
1365
|
+
const chunks = [];
|
|
1366
|
+
for (let i = 0; i < ids.length; i += HISTORICAL_BATCH_MAX_IDS) {
|
|
1367
|
+
chunks.push(ids.slice(i, i + HISTORICAL_BATCH_MAX_IDS));
|
|
1368
|
+
}
|
|
1369
|
+
return chunks;
|
|
1370
|
+
}
|
|
1371
|
+
normalizeHistoricalQueryPath(path) {
|
|
1372
|
+
const rawPath = String(path || '').trim();
|
|
1373
|
+
const normalizedRoot = rawPath.toLowerCase();
|
|
1374
|
+
if (rawPath === '' || normalizedRoot === 'shared' || normalizedRoot === 'shared queries') {
|
|
1375
|
+
return 'Shared%20Queries';
|
|
1376
|
+
}
|
|
1377
|
+
const segments = rawPath
|
|
1378
|
+
.replace(/\\/g, '/')
|
|
1379
|
+
.split('/')
|
|
1380
|
+
.filter((segment) => segment.trim() !== '')
|
|
1381
|
+
.map((segment) => {
|
|
1382
|
+
try {
|
|
1383
|
+
return encodeURIComponent(decodeURIComponent(segment));
|
|
1384
|
+
}
|
|
1385
|
+
catch (error) {
|
|
1386
|
+
return encodeURIComponent(segment);
|
|
1387
|
+
}
|
|
1388
|
+
});
|
|
1389
|
+
return segments.join('/');
|
|
1390
|
+
}
|
|
1391
|
+
normalizeHistoricalQueryRoot(root) {
|
|
1392
|
+
if (!root)
|
|
1393
|
+
return root;
|
|
1394
|
+
if (Array.isArray(root === null || root === void 0 ? void 0 : root.children) || typeof (root === null || root === void 0 ? void 0 : root.isFolder) === 'boolean') {
|
|
1395
|
+
return root;
|
|
1396
|
+
}
|
|
1397
|
+
if (Array.isArray(root === null || root === void 0 ? void 0 : root.value)) {
|
|
1398
|
+
return {
|
|
1399
|
+
id: 'root',
|
|
1400
|
+
name: 'Shared Queries',
|
|
1401
|
+
isFolder: true,
|
|
1402
|
+
children: root.value,
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
return root;
|
|
1406
|
+
}
|
|
1407
|
+
historicalErrorMessage(error) {
|
|
1408
|
+
var _a, _b;
|
|
1409
|
+
return String(((_b = (_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.message) || (error === null || error === void 0 ? void 0 : error.message) || error || '').trim();
|
|
1410
|
+
}
|
|
1411
|
+
isHistoricalMissingWorkItemError(error, workItemId) {
|
|
1412
|
+
var _a;
|
|
1413
|
+
const status = Number(((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status) || (error === null || error === void 0 ? void 0 : error.status) || 0);
|
|
1414
|
+
const message = this.historicalErrorMessage(error);
|
|
1415
|
+
if (!message) {
|
|
1416
|
+
return false;
|
|
1417
|
+
}
|
|
1418
|
+
const mentionsWorkItem = /\bwork\s*item\b/i.test(message);
|
|
1419
|
+
const mentionsId = message.includes(String(workItemId));
|
|
1420
|
+
const missingPattern = /does not exist(?: at time)?|not found|has been deleted|was deleted/i;
|
|
1421
|
+
const hasMissingIndicator = missingPattern.test(message);
|
|
1422
|
+
if (!mentionsWorkItem || !mentionsId || !hasMissingIndicator) {
|
|
1423
|
+
return false;
|
|
1424
|
+
}
|
|
1425
|
+
if (/at time/i.test(message)) {
|
|
1426
|
+
return true;
|
|
1427
|
+
}
|
|
1428
|
+
return status === 404 || status === 410;
|
|
1429
|
+
}
|
|
1430
|
+
async fetchHistoricalWorkItemsBatch(project, ids, asOf, apiVersion) {
|
|
1431
|
+
const workItemsBatchUrl = this.appendApiVersion(`${this.orgUrl}${project}/_apis/wit/workitemsbatch`, apiVersion);
|
|
1432
|
+
const idChunks = this.chunkHistoricalWorkItemIds(ids);
|
|
1433
|
+
try {
|
|
1434
|
+
const batchResponses = await Promise.all(idChunks.map((idChunk) => this.limit(() => tfs_1.TFSServices.getItemContent(workItemsBatchUrl, this.token, 'post', {
|
|
1435
|
+
ids: idChunk,
|
|
1436
|
+
asOf,
|
|
1437
|
+
$expand: 'Relations',
|
|
1438
|
+
fields: HISTORICAL_WORK_ITEM_FIELDS,
|
|
1439
|
+
}))));
|
|
1440
|
+
return {
|
|
1441
|
+
items: batchResponses.flatMap((batch) => (Array.isArray(batch === null || batch === void 0 ? void 0 : batch.value) ? batch.value : [])),
|
|
1442
|
+
skippedWorkItemIds: [],
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
catch (error) {
|
|
1446
|
+
logger_1.default.warn(`[historical-workitems] workitemsbatch failed${apiVersion ? ` (api-version=${apiVersion})` : ''}, falling back to per-item retrieval: ${(error === null || error === void 0 ? void 0 : error.message) || error}`);
|
|
1447
|
+
const asOfParam = encodeURIComponent(asOf);
|
|
1448
|
+
const responses = await Promise.all(ids.map((id) => this.limit(async () => {
|
|
1449
|
+
const itemUrl = this.appendApiVersion(`${this.orgUrl}${project}/_apis/wit/workitems/${id}?$expand=Relations&asOf=${asOfParam}`, apiVersion);
|
|
1450
|
+
try {
|
|
1451
|
+
const item = await tfs_1.TFSServices.getItemContent(itemUrl, this.token);
|
|
1452
|
+
return { id, item, skipped: false };
|
|
1453
|
+
}
|
|
1454
|
+
catch (itemError) {
|
|
1455
|
+
if (this.isHistoricalMissingWorkItemError(itemError, id)) {
|
|
1456
|
+
logger_1.default.warn(`[historical-workitems] skipping work item ${id} for asOf ${asOf}: ${this.historicalErrorMessage(itemError)}`);
|
|
1457
|
+
return { id, item: null, skipped: true };
|
|
1458
|
+
}
|
|
1459
|
+
throw itemError;
|
|
1460
|
+
}
|
|
1461
|
+
})));
|
|
1462
|
+
return {
|
|
1463
|
+
items: responses
|
|
1464
|
+
.filter((entry) => !entry.skipped && entry.item && typeof entry.item === 'object')
|
|
1465
|
+
.map((entry) => entry.item),
|
|
1466
|
+
skippedWorkItemIds: responses.filter((entry) => entry.skipped).map((entry) => entry.id),
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
async executeHistoricalQueryAtAsOf(project, queryId, asOfIso) {
|
|
1471
|
+
const { apiVersion, result } = await this.withHistoricalApiVersionFallback('historical-query-execution', async (resolvedApiVersion) => {
|
|
1472
|
+
const queryDefUrl = this.appendApiVersion(`${this.orgUrl}${project}/_apis/wit/queries/${encodeURIComponent(queryId)}?$expand=all`, resolvedApiVersion);
|
|
1473
|
+
const queryDefinition = await tfs_1.TFSServices.getItemContent(queryDefUrl, this.token);
|
|
1474
|
+
const wiqlText = String((queryDefinition === null || queryDefinition === void 0 ? void 0 : queryDefinition.wiql) || '').trim();
|
|
1475
|
+
let queryResult = null;
|
|
1476
|
+
try {
|
|
1477
|
+
if (!wiqlText) {
|
|
1478
|
+
throw new Error(`Could not resolve WIQL text for query ${queryId}`);
|
|
1479
|
+
}
|
|
1480
|
+
const wiqlWithAsOf = this.appendAsOfToWiql(wiqlText, asOfIso);
|
|
1481
|
+
if (!wiqlWithAsOf) {
|
|
1482
|
+
throw new Error(`Could not build WIQL for historical query ${queryId}`);
|
|
1483
|
+
}
|
|
1484
|
+
const executeUrl = this.appendApiVersion(`${this.orgUrl}${project}/_apis/wit/wiql?$top=2147483646&timePrecision=true`, resolvedApiVersion);
|
|
1485
|
+
queryResult = await tfs_1.TFSServices.getItemContent(executeUrl, this.token, 'post', {
|
|
1486
|
+
query: wiqlWithAsOf,
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
catch (inlineWiqlError) {
|
|
1490
|
+
logger_1.default.warn(`[historical-query-execution] inline WIQL failed for query ${queryId}${resolvedApiVersion ? ` (api-version=${resolvedApiVersion})` : ''}, trying WIQL-by-id fallback: ${(inlineWiqlError === null || inlineWiqlError === void 0 ? void 0 : inlineWiqlError.message) || inlineWiqlError}`);
|
|
1491
|
+
const executeByIdUrl = this.appendApiVersion(`${this.orgUrl}${project}/_apis/wit/wiql/${encodeURIComponent(queryId)}?$top=2147483646&timePrecision=true&asOf=${encodeURIComponent(asOfIso)}`, resolvedApiVersion);
|
|
1492
|
+
queryResult = await tfs_1.TFSServices.getItemContent(executeByIdUrl, this.token);
|
|
1493
|
+
}
|
|
1494
|
+
return { queryDefinition, queryResult };
|
|
1495
|
+
});
|
|
1496
|
+
return Object.assign(Object.assign({}, result), { apiVersion });
|
|
1497
|
+
}
|
|
1498
|
+
toHistoricalWorkItemSnapshot(project, workItem) {
|
|
1499
|
+
var _a, _b, _c, _d;
|
|
1500
|
+
const fields = ((workItem === null || workItem === void 0 ? void 0 : workItem.fields) || {});
|
|
1501
|
+
const id = Number((workItem === null || workItem === void 0 ? void 0 : workItem.id) || fields['System.Id'] || 0);
|
|
1502
|
+
const workItemType = this.normalizeHistoricalCompareValue(fields['System.WorkItemType']);
|
|
1503
|
+
const testPhaseRaw = (_c = (_b = (_a = fields['Elisra.TestPhase']) !== null && _a !== void 0 ? _a : fields['Custom.TestPhase']) !== null && _b !== void 0 ? _b : fields['Elisra.Testphase']) !== null && _c !== void 0 ? _c : fields['Custom.Testphase'];
|
|
1504
|
+
const relatedLinkCount = Array.isArray(workItem === null || workItem === void 0 ? void 0 : workItem.relations) ? workItem.relations.length : 0;
|
|
1505
|
+
return {
|
|
1506
|
+
id,
|
|
1507
|
+
workItemType,
|
|
1508
|
+
title: this.normalizeHistoricalCompareValue(fields['System.Title']),
|
|
1509
|
+
state: this.normalizeHistoricalCompareValue(fields['System.State']),
|
|
1510
|
+
areaPath: this.normalizeHistoricalCompareValue(fields['System.AreaPath']),
|
|
1511
|
+
iterationPath: this.normalizeHistoricalCompareValue(fields['System.IterationPath']),
|
|
1512
|
+
versionId: this.toHistoricalRevision((_d = workItem === null || workItem === void 0 ? void 0 : workItem.rev) !== null && _d !== void 0 ? _d : fields['System.Rev']),
|
|
1513
|
+
versionTimestamp: this.normalizeHistoricalCompareValue(fields['System.ChangedDate']),
|
|
1514
|
+
description: this.normalizeHistoricalCompareValue(fields['System.Description']),
|
|
1515
|
+
steps: this.normalizeHistoricalCompareValue(fields['Microsoft.VSTS.TCM.Steps']),
|
|
1516
|
+
testPhase: this.normalizeTestPhaseValue(testPhaseRaw),
|
|
1517
|
+
relatedLinkCount,
|
|
1518
|
+
workItemUrl: `${this.orgUrl}${project}/_workitems/edit/${id}`,
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
async getHistoricalSnapshot(project, queryId, asOfInput) {
|
|
1522
|
+
const asOf = this.normalizeHistoricalAsOf(asOfInput);
|
|
1523
|
+
const { queryDefinition, queryResult, apiVersion } = await this.executeHistoricalQueryAtAsOf(project, queryId, asOf);
|
|
1524
|
+
const ids = this.extractHistoricalWorkItemIds(queryResult);
|
|
1525
|
+
if (ids.length === 0) {
|
|
1526
|
+
return {
|
|
1527
|
+
queryId,
|
|
1528
|
+
queryName: String((queryDefinition === null || queryDefinition === void 0 ? void 0 : queryDefinition.name) || queryId),
|
|
1529
|
+
asOf,
|
|
1530
|
+
total: 0,
|
|
1531
|
+
rows: [],
|
|
1532
|
+
snapshotMap: new Map(),
|
|
1533
|
+
skippedWorkItemIds: [],
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
const { items: values, skippedWorkItemIds } = await this.fetchHistoricalWorkItemsBatch(project, ids, asOf, apiVersion);
|
|
1537
|
+
const rows = values
|
|
1538
|
+
.map((workItem) => this.toHistoricalWorkItemSnapshot(project, workItem))
|
|
1539
|
+
.sort((a, b) => a.id - b.id);
|
|
1540
|
+
const snapshotMap = new Map();
|
|
1541
|
+
for (const row of rows) {
|
|
1542
|
+
snapshotMap.set(row.id, row);
|
|
1543
|
+
}
|
|
1544
|
+
return {
|
|
1545
|
+
queryId,
|
|
1546
|
+
queryName: String((queryDefinition === null || queryDefinition === void 0 ? void 0 : queryDefinition.name) || queryId),
|
|
1547
|
+
asOf,
|
|
1548
|
+
total: rows.length,
|
|
1549
|
+
rows,
|
|
1550
|
+
snapshotMap,
|
|
1551
|
+
skippedWorkItemIds,
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
collectHistoricalQueries(root, parentPath = '') {
|
|
1555
|
+
const items = [];
|
|
1556
|
+
if (!root) {
|
|
1557
|
+
return items;
|
|
1558
|
+
}
|
|
1559
|
+
const name = String((root === null || root === void 0 ? void 0 : root.name) || '').trim();
|
|
1560
|
+
const currentPath = parentPath && name ? `${parentPath}/${name}` : name || parentPath;
|
|
1561
|
+
if (!(root === null || root === void 0 ? void 0 : root.isFolder)) {
|
|
1562
|
+
const id = String((root === null || root === void 0 ? void 0 : root.id) || '').trim();
|
|
1563
|
+
if (id) {
|
|
1564
|
+
items.push({
|
|
1565
|
+
id,
|
|
1566
|
+
queryName: name || id,
|
|
1567
|
+
path: parentPath || 'Shared Queries',
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
return items;
|
|
1571
|
+
}
|
|
1572
|
+
const children = Array.isArray(root === null || root === void 0 ? void 0 : root.children) ? root.children : [];
|
|
1573
|
+
for (const child of children) {
|
|
1574
|
+
items.push(...this.collectHistoricalQueries(child, currentPath || 'Shared Queries'));
|
|
1575
|
+
}
|
|
1576
|
+
return items;
|
|
1577
|
+
}
|
|
1578
|
+
/**
|
|
1579
|
+
* Returns a flat list of shared queries for historical/as-of execution.
|
|
1580
|
+
*/
|
|
1581
|
+
async GetHistoricalQueries(project, path = 'shared') {
|
|
1582
|
+
const normalizedPath = this.normalizeHistoricalQueryPath(path);
|
|
1583
|
+
// Azure DevOps WIT query tree endpoint enforces $depth range 0..2.
|
|
1584
|
+
const depth = 2;
|
|
1585
|
+
const { result: root } = await this.withHistoricalApiVersionFallback('historical-queries-list', (apiVersion) => {
|
|
1586
|
+
const url = this.appendApiVersion(`${this.orgUrl}${project}/_apis/wit/queries/${normalizedPath}?$depth=${depth}&$expand=all`, apiVersion);
|
|
1587
|
+
return tfs_1.TFSServices.getItemContent(url, this.token);
|
|
1588
|
+
});
|
|
1589
|
+
const normalizedRoot = this.normalizeHistoricalQueryRoot(root);
|
|
1590
|
+
const items = this.collectHistoricalQueries(normalizedRoot).filter((query) => query.id !== '');
|
|
1591
|
+
items.sort((a, b) => {
|
|
1592
|
+
const byPath = a.path.localeCompare(b.path);
|
|
1593
|
+
if (byPath !== 0)
|
|
1594
|
+
return byPath;
|
|
1595
|
+
return a.queryName.localeCompare(b.queryName);
|
|
1596
|
+
});
|
|
1597
|
+
return items;
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Runs a shared query as-of a specific date-time and returns a flat work-item table snapshot.
|
|
1601
|
+
*/
|
|
1602
|
+
async GetHistoricalQueryResults(queryId, project, asOfInput) {
|
|
1603
|
+
const snapshot = await this.getHistoricalSnapshot(project, queryId, asOfInput);
|
|
1604
|
+
return {
|
|
1605
|
+
queryId: snapshot.queryId,
|
|
1606
|
+
queryName: snapshot.queryName,
|
|
1607
|
+
asOf: snapshot.asOf,
|
|
1608
|
+
total: snapshot.total,
|
|
1609
|
+
skippedWorkItemsCount: snapshot.skippedWorkItemIds.length,
|
|
1610
|
+
rows: snapshot.rows.map((row) => ({
|
|
1611
|
+
id: row.id,
|
|
1612
|
+
workItemType: row.workItemType,
|
|
1613
|
+
title: row.title,
|
|
1614
|
+
state: row.state,
|
|
1615
|
+
areaPath: row.areaPath,
|
|
1616
|
+
iterationPath: row.iterationPath,
|
|
1617
|
+
versionId: row.versionId,
|
|
1618
|
+
versionTimestamp: row.versionTimestamp,
|
|
1619
|
+
workItemUrl: row.workItemUrl,
|
|
1620
|
+
})),
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* Compares a shared query between two date-time baselines using the feature's noise-control fields.
|
|
1625
|
+
*/
|
|
1626
|
+
async CompareHistoricalQueryResults(queryId, project, baselineAsOfInput, compareToAsOfInput) {
|
|
1627
|
+
const [baseline, compareTo] = await Promise.all([
|
|
1628
|
+
this.getHistoricalSnapshot(project, queryId, baselineAsOfInput),
|
|
1629
|
+
this.getHistoricalSnapshot(project, queryId, compareToAsOfInput),
|
|
1630
|
+
]);
|
|
1631
|
+
const allIds = new Set([
|
|
1632
|
+
...Array.from(baseline.snapshotMap.keys()),
|
|
1633
|
+
...Array.from(compareTo.snapshotMap.keys()),
|
|
1634
|
+
]);
|
|
1635
|
+
const sortedIds = Array.from(allIds.values()).sort((a, b) => a - b);
|
|
1636
|
+
const rows = sortedIds.map((id) => {
|
|
1637
|
+
const baselineRow = baseline.snapshotMap.get(id) || null;
|
|
1638
|
+
const compareToRow = compareTo.snapshotMap.get(id) || null;
|
|
1639
|
+
const workItemType = (compareToRow === null || compareToRow === void 0 ? void 0 : compareToRow.workItemType) || (baselineRow === null || baselineRow === void 0 ? void 0 : baselineRow.workItemType) || '';
|
|
1640
|
+
const isTestCase = this.isTestCaseType(workItemType);
|
|
1641
|
+
if (baselineRow && !compareToRow) {
|
|
1642
|
+
return {
|
|
1643
|
+
id,
|
|
1644
|
+
workItemType,
|
|
1645
|
+
title: baselineRow.title,
|
|
1646
|
+
baselineRevisionId: baselineRow.versionId,
|
|
1647
|
+
compareToRevisionId: null,
|
|
1648
|
+
compareStatus: 'Deleted',
|
|
1649
|
+
changedFields: [],
|
|
1650
|
+
differences: [],
|
|
1651
|
+
workItemUrl: baselineRow.workItemUrl,
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
if (!baselineRow && compareToRow) {
|
|
1655
|
+
return {
|
|
1656
|
+
id,
|
|
1657
|
+
workItemType,
|
|
1658
|
+
title: compareToRow.title,
|
|
1659
|
+
baselineRevisionId: null,
|
|
1660
|
+
compareToRevisionId: compareToRow.versionId,
|
|
1661
|
+
compareStatus: 'Added',
|
|
1662
|
+
changedFields: [],
|
|
1663
|
+
differences: [],
|
|
1664
|
+
workItemUrl: compareToRow.workItemUrl,
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
const safeBaseline = baselineRow;
|
|
1668
|
+
const safeCompareTo = compareToRow;
|
|
1669
|
+
const changedFields = [];
|
|
1670
|
+
if (safeBaseline.description !== safeCompareTo.description) {
|
|
1671
|
+
changedFields.push('Description');
|
|
1672
|
+
}
|
|
1673
|
+
if (safeBaseline.title !== safeCompareTo.title) {
|
|
1674
|
+
changedFields.push('Title');
|
|
1675
|
+
}
|
|
1676
|
+
if (safeBaseline.state !== safeCompareTo.state) {
|
|
1677
|
+
changedFields.push('State');
|
|
1678
|
+
}
|
|
1679
|
+
if (isTestCase && safeBaseline.steps !== safeCompareTo.steps) {
|
|
1680
|
+
changedFields.push('Steps');
|
|
1681
|
+
}
|
|
1682
|
+
if (safeBaseline.testPhase !== safeCompareTo.testPhase) {
|
|
1683
|
+
changedFields.push('Test Phase');
|
|
1684
|
+
}
|
|
1685
|
+
if (isTestCase && safeBaseline.relatedLinkCount !== safeCompareTo.relatedLinkCount) {
|
|
1686
|
+
changedFields.push('Related Link Count');
|
|
1687
|
+
}
|
|
1688
|
+
const differences = changedFields.map((field) => {
|
|
1689
|
+
switch (field) {
|
|
1690
|
+
case 'Description':
|
|
1691
|
+
return { field, baseline: safeBaseline.description, compareTo: safeCompareTo.description };
|
|
1692
|
+
case 'Title':
|
|
1693
|
+
return { field, baseline: safeBaseline.title, compareTo: safeCompareTo.title };
|
|
1694
|
+
case 'State':
|
|
1695
|
+
return { field, baseline: safeBaseline.state, compareTo: safeCompareTo.state };
|
|
1696
|
+
case 'Steps':
|
|
1697
|
+
return { field, baseline: safeBaseline.steps, compareTo: safeCompareTo.steps };
|
|
1698
|
+
case 'Test Phase':
|
|
1699
|
+
return { field, baseline: safeBaseline.testPhase, compareTo: safeCompareTo.testPhase };
|
|
1700
|
+
case 'Related Link Count':
|
|
1701
|
+
return {
|
|
1702
|
+
field,
|
|
1703
|
+
baseline: String(safeBaseline.relatedLinkCount),
|
|
1704
|
+
compareTo: String(safeCompareTo.relatedLinkCount),
|
|
1705
|
+
};
|
|
1706
|
+
default:
|
|
1707
|
+
return { field, baseline: '', compareTo: '' };
|
|
1708
|
+
}
|
|
1709
|
+
});
|
|
1710
|
+
return {
|
|
1711
|
+
id,
|
|
1712
|
+
workItemType,
|
|
1713
|
+
title: safeCompareTo.title || safeBaseline.title,
|
|
1714
|
+
baselineRevisionId: safeBaseline.versionId,
|
|
1715
|
+
compareToRevisionId: safeCompareTo.versionId,
|
|
1716
|
+
compareStatus: changedFields.length > 0 ? 'Changed' : 'No changes',
|
|
1717
|
+
changedFields,
|
|
1718
|
+
differences,
|
|
1719
|
+
workItemUrl: safeCompareTo.workItemUrl || safeBaseline.workItemUrl,
|
|
1720
|
+
};
|
|
1721
|
+
});
|
|
1722
|
+
const summary = rows.reduce((acc, row) => {
|
|
1723
|
+
if (row.compareStatus === 'Added')
|
|
1724
|
+
acc.addedCount += 1;
|
|
1725
|
+
else if (row.compareStatus === 'Deleted')
|
|
1726
|
+
acc.deletedCount += 1;
|
|
1727
|
+
else if (row.compareStatus === 'Changed')
|
|
1728
|
+
acc.changedCount += 1;
|
|
1729
|
+
else
|
|
1730
|
+
acc.noChangeCount += 1;
|
|
1731
|
+
return acc;
|
|
1732
|
+
}, { addedCount: 0, deletedCount: 0, changedCount: 0, noChangeCount: 0 });
|
|
1733
|
+
return {
|
|
1734
|
+
queryId,
|
|
1735
|
+
queryName: compareTo.queryName || baseline.queryName || queryId,
|
|
1736
|
+
baseline: {
|
|
1737
|
+
asOf: baseline.asOf,
|
|
1738
|
+
total: baseline.total,
|
|
1739
|
+
},
|
|
1740
|
+
compareTo: {
|
|
1741
|
+
asOf: compareTo.asOf,
|
|
1742
|
+
total: compareTo.total,
|
|
1743
|
+
},
|
|
1744
|
+
summary: Object.assign(Object.assign({}, summary), { updatedCount: summary.changedCount }),
|
|
1745
|
+
skippedWorkItems: {
|
|
1746
|
+
baselineCount: baseline.skippedWorkItemIds.length,
|
|
1747
|
+
compareToCount: compareTo.skippedWorkItemIds.length,
|
|
1748
|
+
totalDistinct: new Set([...baseline.skippedWorkItemIds, ...compareTo.skippedWorkItemIds])
|
|
1749
|
+
.size,
|
|
1750
|
+
},
|
|
1751
|
+
rows,
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1192
1754
|
async PopulateWorkItemsByIds(workItemsArray = [], projectName = '') {
|
|
1193
1755
|
let url = `${this.orgUrl}${projectName}/_apis/wit/workitemsbatch`;
|
|
1194
1756
|
let res = [];
|