@elisra-devops/docgen-data-provider 1.18.0 → 1.19.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/helpers/tfs.d.ts +15 -0
- package/bin/helpers/tfs.js +148 -130
- package/bin/helpers/tfs.js.map +1 -1
- package/bin/modules/TestDataProvider.d.ts +8 -1
- package/bin/modules/TestDataProvider.js +165 -83
- package/bin/modules/TestDataProvider.js.map +1 -1
- package/package.json +1 -1
- package/src/helpers/tfs.ts +166 -131
- package/src/modules/TestDataProvider.ts +228 -126
package/src/helpers/tfs.ts
CHANGED
|
@@ -1,16 +1,34 @@
|
|
|
1
|
-
import axios from 'axios';
|
|
1
|
+
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
|
|
2
2
|
import logger from '../utils/logger';
|
|
3
3
|
|
|
4
4
|
export class TFSServices {
|
|
5
|
+
// Connection pooling
|
|
6
|
+
private static connectionPool = {
|
|
7
|
+
httpAgent: new (require('http').Agent)({
|
|
8
|
+
keepAlive: true,
|
|
9
|
+
maxSockets: 50, // Allow more connections
|
|
10
|
+
keepAliveMsecs: 30000, // Keep connections alive longer
|
|
11
|
+
}),
|
|
12
|
+
httpsAgent: new (require('https').Agent)({
|
|
13
|
+
keepAlive: true,
|
|
14
|
+
maxSockets: 50,
|
|
15
|
+
keepAliveMsecs: 30000,
|
|
16
|
+
}),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Axios instance with connection reuse
|
|
20
|
+
private static axiosInstance: AxiosInstance = axios.create({
|
|
21
|
+
httpAgent: this.connectionPool.httpAgent,
|
|
22
|
+
httpsAgent: this.connectionPool.httpsAgent,
|
|
23
|
+
});
|
|
24
|
+
|
|
5
25
|
public static async downloadZipFile(url: string, pat: string): Promise<any> {
|
|
6
26
|
try {
|
|
7
|
-
|
|
27
|
+
const res = await this.axiosInstance.request({
|
|
8
28
|
url: url,
|
|
9
29
|
headers: { 'Content-Type': 'application/zip' },
|
|
10
|
-
auth: {
|
|
11
|
-
|
|
12
|
-
password: pat,
|
|
13
|
-
},
|
|
30
|
+
auth: { username: '', password: pat },
|
|
31
|
+
timeout: 15000, // Increased timeout for large files
|
|
14
32
|
});
|
|
15
33
|
return res;
|
|
16
34
|
} catch (e) {
|
|
@@ -27,52 +45,22 @@ export class TFSServices {
|
|
|
27
45
|
customHeaders: any = {},
|
|
28
46
|
printError: boolean = true
|
|
29
47
|
): Promise<any> {
|
|
30
|
-
|
|
48
|
+
const config: AxiosRequestConfig = {
|
|
31
49
|
headers: customHeaders,
|
|
32
50
|
method: requestMethod,
|
|
33
|
-
auth: {
|
|
34
|
-
username: '',
|
|
35
|
-
password: pat,
|
|
36
|
-
},
|
|
51
|
+
auth: { username: '', password: pat },
|
|
37
52
|
data: data,
|
|
38
53
|
responseType: 'arraybuffer', // Important for binary data
|
|
39
|
-
timeout:
|
|
54
|
+
timeout: 8000, // Increased timeout for images
|
|
40
55
|
};
|
|
41
|
-
let json;
|
|
42
|
-
let attempts = 0;
|
|
43
|
-
const maxAttempts = 3;
|
|
44
|
-
|
|
45
|
-
logger.silly(`making request:
|
|
46
|
-
url: ${url}
|
|
47
|
-
config: ${JSON.stringify(config)}`);
|
|
48
56
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const mimeType = contentType.split(';')[0].trim(); // Extracts "image/png"
|
|
57
|
-
return `data:${mimeType};base64,${base64String}`;
|
|
58
|
-
} catch (e: any) {
|
|
59
|
-
attempts++;
|
|
60
|
-
if (e.message.includes('ETIMEDOUT') && attempts < maxAttempts) {
|
|
61
|
-
logger.warn(`Request timed out. Retrying attempt ${attempts} of ${maxAttempts}...`);
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
if (printError) {
|
|
65
|
-
if (e.response) {
|
|
66
|
-
logger.error(`Error fetching image from Azure DevOps at ${url}: ${e.message}`);
|
|
67
|
-
logger.error(`Status: ${e.response.status}`);
|
|
68
|
-
logger.error(`Response Data: ${JSON.stringify(e.response.data)}`);
|
|
69
|
-
} else {
|
|
70
|
-
logger.error(`Error fetching image from Azure DevOps at ${url}: ${e.message}`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
throw e;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
57
|
+
return this.executeWithRetry(url, config, printError, (response) => {
|
|
58
|
+
// Convert binary data to Base64
|
|
59
|
+
const base64String = Buffer.from(response.data, 'binary').toString('base64');
|
|
60
|
+
const contentType = response.headers['content-type'] || 'application/octet-stream';
|
|
61
|
+
const mimeType = contentType.split(';')[0].trim();
|
|
62
|
+
return `data:${mimeType};base64,${base64String}`;
|
|
63
|
+
});
|
|
76
64
|
}
|
|
77
65
|
|
|
78
66
|
public static async getItemContent(
|
|
@@ -83,87 +71,37 @@ export class TFSServices {
|
|
|
83
71
|
customHeaders: any = {},
|
|
84
72
|
printError: boolean = true
|
|
85
73
|
): Promise<any> {
|
|
86
|
-
|
|
74
|
+
// Clean URL
|
|
75
|
+
const cleanUrl = url.replace(/ /g, '%20');
|
|
76
|
+
|
|
77
|
+
const config: AxiosRequestConfig = {
|
|
87
78
|
headers: customHeaders,
|
|
88
79
|
method: requestMethod,
|
|
89
|
-
auth: {
|
|
90
|
-
username: '',
|
|
91
|
-
password: pat,
|
|
92
|
-
},
|
|
80
|
+
auth: { username: '', password: pat },
|
|
93
81
|
data: data,
|
|
94
|
-
timeout:
|
|
82
|
+
timeout: 10000, // More reasonable timeout
|
|
95
83
|
};
|
|
96
|
-
let json;
|
|
97
|
-
let attempts = 0;
|
|
98
|
-
const maxAttempts = 3;
|
|
99
|
-
url = url.replace(/ /g, '%20');
|
|
100
|
-
logger.silly(`making request:
|
|
101
|
-
url: ${url}
|
|
102
|
-
config: ${JSON.stringify(config)}`);
|
|
103
|
-
|
|
104
|
-
while (attempts < maxAttempts) {
|
|
105
|
-
try {
|
|
106
|
-
let result = await axios(url, config);
|
|
107
|
-
json = JSON.parse(JSON.stringify(result.data));
|
|
108
|
-
return json;
|
|
109
|
-
} catch (e: any) {
|
|
110
|
-
logger.warn(`error fetching item content from azure devops at ${url}`);
|
|
111
|
-
logger.warn(`error: ${JSON.stringify(e.response?.data?.message)}`);
|
|
112
|
-
if (e.response?.data?.message.includes('could not be found')) {
|
|
113
|
-
logger.info(`File does not exist, or you do not have permissions to read it.`);
|
|
114
|
-
return undefined;
|
|
115
|
-
}
|
|
116
84
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
(e.response?.data?.message?.includes('timeout') && attempts < maxAttempts)
|
|
122
|
-
) {
|
|
123
|
-
logger.warn(`Request timed out. Retrying attempt ${attempts} of ${maxAttempts}...`);
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
if (printError) {
|
|
127
|
-
if (e.response) {
|
|
128
|
-
// Log detailed error information including the URL
|
|
129
|
-
logger.error(`Error making request to Azure DevOps at ${url}: ${e.message}`);
|
|
130
|
-
logger.error(`Status: ${e.response.status}`);
|
|
131
|
-
logger.error(`Response Data: ${JSON.stringify(e.response.data?.message)}`);
|
|
132
|
-
} else {
|
|
133
|
-
// Handle other errors (network, etc.)
|
|
134
|
-
logger.error(`Error making request to Azure DevOps at ${url}: ${e.message}`);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
throw e;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
85
|
+
return this.executeWithRetry(cleanUrl, config, printError, (response) => {
|
|
86
|
+
// Direct return of data without extra JSON parsing
|
|
87
|
+
return response.data;
|
|
88
|
+
});
|
|
140
89
|
}
|
|
141
90
|
|
|
142
91
|
public static async getJfrogRequest(url: string, header?: any) {
|
|
143
|
-
|
|
92
|
+
const config: AxiosRequestConfig = {
|
|
144
93
|
method: 'GET',
|
|
94
|
+
headers: header,
|
|
95
|
+
timeout: 8000, // Reasonable timeout
|
|
145
96
|
};
|
|
146
|
-
if (header) {
|
|
147
|
-
config['headers'] = header;
|
|
148
|
-
}
|
|
149
97
|
|
|
150
|
-
let json;
|
|
151
98
|
try {
|
|
152
|
-
|
|
153
|
-
|
|
99
|
+
const result = await this.axiosInstance.request(config);
|
|
100
|
+
return result.data;
|
|
154
101
|
} catch (e: any) {
|
|
155
|
-
|
|
156
|
-
// Log detailed error information including the URL
|
|
157
|
-
logger.error(`Error making request Jfrog at ${url}: ${e.message}`);
|
|
158
|
-
logger.error(`Status: ${e.response.status}`);
|
|
159
|
-
logger.error(`Response Data: ${JSON.stringify(e.response.data)}`);
|
|
160
|
-
} else {
|
|
161
|
-
// Handle other errors (network, etc.)
|
|
162
|
-
logger.error(`Error making request to Jfrog at ${url}: ${e.message}`);
|
|
163
|
-
}
|
|
102
|
+
this.logDetailedError(e, url);
|
|
164
103
|
throw e;
|
|
165
104
|
}
|
|
166
|
-
return json;
|
|
167
105
|
}
|
|
168
106
|
|
|
169
107
|
public static async postRequest(
|
|
@@ -173,32 +111,129 @@ export class TFSServices {
|
|
|
173
111
|
data: any,
|
|
174
112
|
customHeaders: any = { headers: { 'Content-Type': 'application/json' } }
|
|
175
113
|
): Promise<any> {
|
|
176
|
-
|
|
114
|
+
const config: AxiosRequestConfig = {
|
|
177
115
|
headers: customHeaders,
|
|
178
116
|
method: requestMethod,
|
|
179
|
-
auth: {
|
|
180
|
-
username: '',
|
|
181
|
-
password: pat,
|
|
182
|
-
},
|
|
117
|
+
auth: { username: '', password: pat },
|
|
183
118
|
data: data,
|
|
119
|
+
timeout: 10000, // More reasonable timeout
|
|
184
120
|
};
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
121
|
+
|
|
122
|
+
// Use shorter log format for better performance
|
|
123
|
+
logger.silly(`Request: ${url} [${requestMethod}]`);
|
|
124
|
+
|
|
189
125
|
try {
|
|
190
|
-
result = await
|
|
126
|
+
const result = await this.axiosInstance.request(config);
|
|
127
|
+
return result;
|
|
191
128
|
} catch (e: any) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
129
|
+
this.logDetailedError(e, url);
|
|
130
|
+
throw e;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Execute a request with intelligent retry logic
|
|
136
|
+
*/
|
|
137
|
+
private static async executeWithRetry(
|
|
138
|
+
url: string,
|
|
139
|
+
config: AxiosRequestConfig,
|
|
140
|
+
printError: boolean,
|
|
141
|
+
responseProcessor: (response: any) => any
|
|
142
|
+
): Promise<any> {
|
|
143
|
+
let attempts = 0;
|
|
144
|
+
const maxAttempts = 3;
|
|
145
|
+
const baseDelay = 500; // Start with 500ms delay
|
|
146
|
+
|
|
147
|
+
while (true) {
|
|
148
|
+
try {
|
|
149
|
+
const result = await this.axiosInstance.request({ ...config, url });
|
|
150
|
+
return responseProcessor(result);
|
|
151
|
+
} catch (e: any) {
|
|
152
|
+
attempts++;
|
|
153
|
+
const errorMessage = this.getErrorMessage(e);
|
|
154
|
+
|
|
155
|
+
// Handle not found errors
|
|
156
|
+
if (errorMessage.includes('could not be found')) {
|
|
157
|
+
logger.info(`File does not exist, or you do not have permissions to read it.`);
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Check if we should retry
|
|
162
|
+
if (attempts < maxAttempts && this.isRetryableError(e)) {
|
|
163
|
+
// Calculate exponential backoff with jitter
|
|
164
|
+
const jitter = Math.random() * 0.3 + 0.85; // Between 0.85 and 1.15
|
|
165
|
+
const delay = Math.min(baseDelay * Math.pow(2, attempts - 1) * jitter, 5000);
|
|
166
|
+
|
|
167
|
+
logger.warn(`Request failed. Retrying in ${Math.round(delay)}ms (${attempts}/${maxAttempts})`);
|
|
168
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Log error if needed
|
|
173
|
+
if (printError) {
|
|
174
|
+
this.logDetailedError(e, url);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
throw e;
|
|
200
178
|
}
|
|
201
179
|
}
|
|
202
|
-
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Check if an error is retryable
|
|
184
|
+
*/
|
|
185
|
+
private static isRetryableError(error: any): boolean {
|
|
186
|
+
// Network errors
|
|
187
|
+
if (error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT' || error.message.includes('timeout')) {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Server errors (5xx)
|
|
192
|
+
if (error.response?.status >= 500) {
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Rate limiting (429)
|
|
197
|
+
if (error.response?.status === 429) {
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Log detailed error information
|
|
206
|
+
*/
|
|
207
|
+
private static logDetailedError(error: any, url: string): void {
|
|
208
|
+
if (error.response) {
|
|
209
|
+
logger.error(`Error for ${url}: ${error.message}`);
|
|
210
|
+
logger.error(`Status: ${error.response.status}`);
|
|
211
|
+
|
|
212
|
+
if (error.response.data) {
|
|
213
|
+
if (typeof error.response.data === 'string') {
|
|
214
|
+
logger.error(`Response: ${error.response.data.substring(0, 200)}`);
|
|
215
|
+
} else {
|
|
216
|
+
const dataMessage =
|
|
217
|
+
error.response.data.message || JSON.stringify(error.response.data).substring(0, 200);
|
|
218
|
+
logger.error(`Response: ${dataMessage}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
logger.error(`Error for ${url}: ${error.message}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private static getErrorMessage(error: any): string {
|
|
227
|
+
if (error.response?.data?.message) {
|
|
228
|
+
return JSON.stringify(error.response.data.message);
|
|
229
|
+
} else if (error.response?.data) {
|
|
230
|
+
return JSON.stringify(error.response.data);
|
|
231
|
+
} else if (error.response) {
|
|
232
|
+
return `HTTP ${error.response.status}`;
|
|
233
|
+
} else if (error.message) {
|
|
234
|
+
return error.message;
|
|
235
|
+
} else {
|
|
236
|
+
return 'Unknown error occurred';
|
|
237
|
+
}
|
|
203
238
|
}
|
|
204
239
|
}
|