abapgit-agent 1.17.8 → 1.18.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/README.md +1 -0
- package/abap/CLAUDE.md +150 -25
- package/abap/CLAUDE.slim.md +5 -4
- package/abap/guidelines/abaplint.md +2 -0
- package/abap/guidelines/cds-testing.md +12 -0
- package/abap/guidelines/cds.md +7 -0
- package/abap/guidelines/debug-dump.md +4 -0
- package/abap/guidelines/debug-session.md +27 -2
- package/abap/guidelines/run-probe-classes.md +43 -0
- package/abap/guidelines/string-template.md +66 -1
- package/bin/abapgit-agent +3 -2
- package/package.json +10 -6
- package/src/commands/debug.js +156 -119
- package/src/commands/guide.js +17 -0
- package/src/commands/inspect.js +7 -4
- package/src/commands/pull.js +32 -14
- package/src/commands/unit.js +2 -1
- package/src/commands/view.js +1 -1
- package/src/config.js +13 -1
- package/src/utils/abap-http.js +136 -252
- package/src/utils/adt-http.js +134 -216
- package/src/utils/debug-daemon.js +57 -48
- package/src/utils/debug-session.js +126 -25
package/src/utils/abap-http.js
CHANGED
|
@@ -1,23 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
* ABAP HTTP
|
|
4
|
+
* ABAP HTTP client for the custom abapgit-agent REST handler.
|
|
5
|
+
*
|
|
6
|
+
* Uses axios with connection pooling (keepAlive). Handles CSRF tokens,
|
|
7
|
+
* cookie management, session caching, and automatic auth retry.
|
|
8
|
+
*
|
|
9
|
+
* Responses are parsed as JSON (the /sap/bc/z_abapgit_agent/ handler
|
|
10
|
+
* always returns JSON). ABAP-specific newline escaping is applied
|
|
11
|
+
* before parsing.
|
|
3
12
|
*/
|
|
13
|
+
const axios = require('axios');
|
|
4
14
|
const https = require('https');
|
|
5
|
-
const http = require('http');
|
|
6
|
-
const { extractBodyDetail } = require('./format-error');
|
|
7
15
|
const fs = require('fs');
|
|
8
16
|
const path = require('path');
|
|
17
|
+
const { extractBodyDetail } = require('./format-error');
|
|
9
18
|
const os = require('os');
|
|
10
19
|
const crypto = require('crypto');
|
|
11
20
|
|
|
12
|
-
/**
|
|
13
|
-
* ABAP HTTP client with CSRF token, cookie, and session caching
|
|
14
|
-
*/
|
|
15
21
|
class AbapHttp {
|
|
16
22
|
constructor(config) {
|
|
17
23
|
this.config = config;
|
|
18
24
|
this.csrfToken = null;
|
|
19
25
|
this.cookies = null;
|
|
20
26
|
|
|
27
|
+
const isHttp = config.protocol === 'http';
|
|
28
|
+
const baseURL = `${config.protocol || 'https'}://${config.host}:${config.sapport}`;
|
|
29
|
+
this._axios = axios.create({
|
|
30
|
+
baseURL,
|
|
31
|
+
...(isHttp
|
|
32
|
+
? { httpAgent: new (require('http').Agent)({ keepAlive: true }) }
|
|
33
|
+
: { httpsAgent: new https.Agent({ rejectUnauthorized: false, keepAlive: true }) }),
|
|
34
|
+
maxRedirects: 0,
|
|
35
|
+
validateStatus: () => true,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Request interceptor: inject auth, CSRF, cookies, sap-client on every request
|
|
39
|
+
this._axios.interceptors.request.use((reqConfig) => {
|
|
40
|
+
reqConfig.headers['Authorization'] =
|
|
41
|
+
`Basic ${Buffer.from(`${config.user}:${config.password}`).toString('base64')}`;
|
|
42
|
+
reqConfig.headers['sap-client'] = config.client;
|
|
43
|
+
reqConfig.headers['sap-language'] = config.language || 'EN';
|
|
44
|
+
if (this.cookies) reqConfig.headers['Cookie'] = this.cookies;
|
|
45
|
+
if (['post', 'delete'].includes(reqConfig.method) && this.csrfToken) {
|
|
46
|
+
reqConfig.headers['X-CSRF-Token'] = this.csrfToken;
|
|
47
|
+
}
|
|
48
|
+
return reqConfig;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Response interceptor: merge Set-Cookie headers into cookie jar.
|
|
52
|
+
// Skip on error responses (>= 400) to avoid stale cookie overwrites.
|
|
53
|
+
this._axios.interceptors.response.use((resp) => {
|
|
54
|
+
if (resp.status >= 400) return resp;
|
|
55
|
+
const setCookie = resp.headers['set-cookie'];
|
|
56
|
+
if (setCookie) {
|
|
57
|
+
const incoming = Array.isArray(setCookie)
|
|
58
|
+
? setCookie.map(c => c.split(';')[0])
|
|
59
|
+
: [setCookie.split(';')[0]];
|
|
60
|
+
const jar = new Map();
|
|
61
|
+
if (this.cookies) {
|
|
62
|
+
this.cookies.split(';').forEach(pair => {
|
|
63
|
+
const trimmed = pair.trim();
|
|
64
|
+
if (trimmed) {
|
|
65
|
+
const eq = trimmed.indexOf('=');
|
|
66
|
+
jar.set(eq === -1 ? trimmed : trimmed.slice(0, eq).trim(), trimmed);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
incoming.forEach(pair => {
|
|
71
|
+
const trimmed = pair.trim();
|
|
72
|
+
if (trimmed) {
|
|
73
|
+
const eq = trimmed.indexOf('=');
|
|
74
|
+
jar.set(eq === -1 ? trimmed : trimmed.slice(0, eq).trim(), trimmed);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
this.cookies = [...jar.values()].join('; ');
|
|
78
|
+
}
|
|
79
|
+
return resp;
|
|
80
|
+
});
|
|
81
|
+
|
|
21
82
|
// Session cache file path
|
|
22
83
|
const configHash = crypto.createHash('md5')
|
|
23
84
|
.update(`${config.host}:${config.user}:${config.client}`)
|
|
@@ -25,48 +86,27 @@ class AbapHttp {
|
|
|
25
86
|
.substring(0, 8);
|
|
26
87
|
|
|
27
88
|
this.sessionFile = path.join(os.tmpdir(), `abapgit-session-${configHash}.json`);
|
|
28
|
-
|
|
29
|
-
// Try to load cached session
|
|
30
89
|
this.loadSession();
|
|
31
90
|
}
|
|
32
91
|
|
|
33
|
-
/**
|
|
34
|
-
* Load session from cache file if valid
|
|
35
|
-
*/
|
|
36
92
|
loadSession() {
|
|
37
|
-
if (!fs.existsSync(this.sessionFile))
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
|
|
93
|
+
if (!fs.existsSync(this.sessionFile)) return;
|
|
41
94
|
try {
|
|
42
95
|
const session = JSON.parse(fs.readFileSync(this.sessionFile, 'utf8'));
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const now = Date.now();
|
|
46
|
-
const safetyMargin = 2 * 60 * 1000; // 2 minutes
|
|
47
|
-
|
|
48
|
-
if (session.expiresAt > now + safetyMargin) {
|
|
96
|
+
const safetyMargin = 2 * 60 * 1000;
|
|
97
|
+
if (session.expiresAt > Date.now() + safetyMargin) {
|
|
49
98
|
this.csrfToken = session.csrfToken;
|
|
50
99
|
this.cookies = session.cookies;
|
|
51
|
-
// Silent - no console output for cached session
|
|
52
100
|
} else {
|
|
53
|
-
// Session expired
|
|
54
101
|
this.clearSession();
|
|
55
102
|
}
|
|
56
103
|
} catch (e) {
|
|
57
|
-
// Corrupted cache file - clear it
|
|
58
104
|
this.clearSession();
|
|
59
105
|
}
|
|
60
106
|
}
|
|
61
107
|
|
|
62
|
-
/**
|
|
63
|
-
* Save session to cache file
|
|
64
|
-
*/
|
|
65
108
|
saveSession() {
|
|
66
|
-
// Conservative expiration: 15 minutes
|
|
67
|
-
// (ABAP default session timeout is typically 20 minutes)
|
|
68
109
|
const expiresAt = Date.now() + (15 * 60 * 1000);
|
|
69
|
-
|
|
70
110
|
try {
|
|
71
111
|
fs.writeFileSync(this.sessionFile, JSON.stringify({
|
|
72
112
|
csrfToken: this.csrfToken,
|
|
@@ -75,37 +115,27 @@ class AbapHttp {
|
|
|
75
115
|
savedAt: Date.now()
|
|
76
116
|
}));
|
|
77
117
|
} catch (e) {
|
|
78
|
-
// Ignore write errors
|
|
118
|
+
// Ignore write errors
|
|
79
119
|
}
|
|
80
120
|
}
|
|
81
121
|
|
|
82
|
-
/**
|
|
83
|
-
* Clear cached session
|
|
84
|
-
*/
|
|
85
122
|
clearSession() {
|
|
86
123
|
this.csrfToken = null;
|
|
87
124
|
this.cookies = null;
|
|
88
|
-
|
|
89
125
|
try {
|
|
90
|
-
if (fs.existsSync(this.sessionFile))
|
|
91
|
-
fs.unlinkSync(this.sessionFile);
|
|
92
|
-
}
|
|
126
|
+
if (fs.existsSync(this.sessionFile)) fs.unlinkSync(this.sessionFile);
|
|
93
127
|
} catch (e) {
|
|
94
|
-
// Ignore
|
|
128
|
+
// Ignore deletion errors
|
|
95
129
|
}
|
|
96
130
|
}
|
|
97
131
|
|
|
98
132
|
/**
|
|
99
|
-
* Detect if error is due to expired/invalid session
|
|
100
|
-
* @param {Error|object} error - Error object or response
|
|
101
|
-
* @returns {boolean} True if auth error
|
|
133
|
+
* Detect if error is due to expired/invalid session.
|
|
102
134
|
*/
|
|
103
135
|
isAuthError(error) {
|
|
104
|
-
|
|
105
|
-
if (error.statusCode ===
|
|
106
|
-
if (error.statusCode === 403) return true; // Forbidden
|
|
136
|
+
if (error.statusCode === 401) return true;
|
|
137
|
+
if (error.statusCode === 403) return true;
|
|
107
138
|
|
|
108
|
-
// Error message patterns
|
|
109
139
|
const message = (error.message || error.error || '').toLowerCase();
|
|
110
140
|
if (message.includes('csrf')) return true;
|
|
111
141
|
if (message.includes('token')) return true;
|
|
@@ -114,7 +144,6 @@ class AbapHttp {
|
|
|
114
144
|
if (message.includes('unauthorized')) return true;
|
|
115
145
|
if (message.includes('forbidden')) return true;
|
|
116
146
|
|
|
117
|
-
// Response body patterns
|
|
118
147
|
if (error.body) {
|
|
119
148
|
const bodyStr = JSON.stringify(error.body).toLowerCase();
|
|
120
149
|
if (bodyStr.includes('csrf')) return true;
|
|
@@ -126,253 +155,108 @@ class AbapHttp {
|
|
|
126
155
|
}
|
|
127
156
|
|
|
128
157
|
/**
|
|
129
|
-
* Fetch CSRF token
|
|
130
|
-
* @returns {Promise<string>} CSRF token
|
|
158
|
+
* Fetch CSRF token via GET /health with X-CSRF-Token: fetch
|
|
131
159
|
*/
|
|
132
160
|
async fetchCsrfToken() {
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
port: url.port,
|
|
139
|
-
path: url.pathname,
|
|
140
|
-
method: 'GET',
|
|
141
|
-
headers: {
|
|
142
|
-
'Authorization': `Basic ${Buffer.from(`${this.config.user}:${this.config.password}`).toString('base64')}`,
|
|
143
|
-
'sap-client': this.config.client,
|
|
144
|
-
'sap-language': this.config.language || 'EN',
|
|
145
|
-
'X-CSRF-Token': 'fetch',
|
|
146
|
-
'Content-Type': 'application/json'
|
|
147
|
-
},
|
|
148
|
-
agent: this.config.protocol === 'http' ? undefined : new https.Agent({ rejectUnauthorized: false })
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
const req = (this.config.protocol === 'http' ? http : https).request(options, (res) => {
|
|
152
|
-
const csrfToken = res.headers['x-csrf-token'];
|
|
153
|
-
|
|
154
|
-
// Save cookies from response
|
|
155
|
-
const setCookie = res.headers['set-cookie'];
|
|
156
|
-
if (setCookie) {
|
|
157
|
-
this.cookies = Array.isArray(setCookie)
|
|
158
|
-
? setCookie.map(c => c.split(';')[0]).join('; ')
|
|
159
|
-
: setCookie.split(';')[0];
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
let body = '';
|
|
163
|
-
res.on('data', chunk => body += chunk);
|
|
164
|
-
res.on('end', () => {
|
|
165
|
-
this.csrfToken = csrfToken;
|
|
166
|
-
|
|
167
|
-
// Save session to cache
|
|
168
|
-
this.saveSession();
|
|
169
|
-
|
|
170
|
-
resolve(csrfToken);
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
req.on('error', reject);
|
|
175
|
-
req.end();
|
|
161
|
+
const resp = await this._axios.get('/sap/bc/z_abapgit_agent/health', {
|
|
162
|
+
headers: {
|
|
163
|
+
'X-CSRF-Token': 'fetch',
|
|
164
|
+
'Content-Type': 'application/json'
|
|
165
|
+
}
|
|
176
166
|
});
|
|
167
|
+
this.csrfToken = resp.headers['x-csrf-token'] || this.csrfToken;
|
|
168
|
+
this.saveSession();
|
|
169
|
+
return this.csrfToken;
|
|
177
170
|
}
|
|
178
171
|
|
|
179
172
|
/**
|
|
180
|
-
* Make HTTP request
|
|
181
|
-
*
|
|
182
|
-
* @param {string} urlPath - URL path
|
|
183
|
-
* @param {object} data - Request body (for POST)
|
|
184
|
-
* @param {object} options - Additional options (headers, csrfToken, isRetry)
|
|
185
|
-
* @returns {Promise<object>} Response JSON
|
|
173
|
+
* Make HTTP request with automatic retry on auth failure.
|
|
174
|
+
* Returns parsed JSON.
|
|
186
175
|
*/
|
|
187
176
|
async request(method, urlPath, data = null, options = {}) {
|
|
188
177
|
try {
|
|
189
|
-
// Try with current session (cached or fresh)
|
|
190
178
|
return await this._makeRequest(method, urlPath, data, options);
|
|
191
|
-
|
|
192
179
|
} catch (error) {
|
|
193
|
-
// Check if it's an authentication/authorization failure
|
|
194
180
|
if (this.isAuthError(error) && !options.isRetry) {
|
|
195
|
-
// Session expired - refresh and retry once
|
|
196
|
-
console.error('⚠️ Session expired, refreshing...');
|
|
197
|
-
|
|
198
|
-
// Clear stale session
|
|
199
181
|
this.clearSession();
|
|
200
|
-
|
|
201
|
-
// Fetch fresh token/cookies
|
|
202
182
|
await this.fetchCsrfToken();
|
|
203
|
-
|
|
204
|
-
// Retry ONCE with fresh session
|
|
205
|
-
return await this._makeRequest(method, urlPath, data, {
|
|
206
|
-
...options,
|
|
207
|
-
isRetry: true // Prevent infinite loop
|
|
208
|
-
});
|
|
183
|
+
return await this._makeRequest(method, urlPath, data, { ...options, isRetry: true });
|
|
209
184
|
}
|
|
210
|
-
|
|
211
|
-
// Not an auth error or already retried - propagate
|
|
212
185
|
throw error;
|
|
213
186
|
}
|
|
214
187
|
}
|
|
215
188
|
|
|
216
189
|
/**
|
|
217
|
-
* Internal request
|
|
218
|
-
* @param {string} method - HTTP method
|
|
219
|
-
* @param {string} urlPath - URL path
|
|
220
|
-
* @param {object} data - Request body
|
|
221
|
-
* @param {object} options - Additional options
|
|
222
|
-
* @returns {Promise<object>} Response JSON
|
|
190
|
+
* Internal request — returns parsed JSON.
|
|
223
191
|
*/
|
|
224
192
|
async _makeRequest(method, urlPath, data = null, options = {}) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
193
|
+
const headers = {
|
|
194
|
+
'Content-Type': 'application/json',
|
|
195
|
+
...options.headers
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const resp = await this._axios.request({
|
|
199
|
+
method,
|
|
200
|
+
url: urlPath,
|
|
201
|
+
data: data ? JSON.stringify(data) : undefined,
|
|
202
|
+
headers,
|
|
203
|
+
responseType: 'text',
|
|
204
|
+
timeout: 120000,
|
|
205
|
+
});
|
|
234
206
|
|
|
235
|
-
|
|
236
|
-
headers['Authorization'] = `Basic ${Buffer.from(`${this.config.user}:${this.config.password}`).toString('base64')}`;
|
|
207
|
+
const statusCode = resp.status;
|
|
237
208
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
209
|
+
if (statusCode === 401 || statusCode === 403) {
|
|
210
|
+
throw { statusCode, message: `Authentication failed: ${statusCode}`, isAuthError: true };
|
|
211
|
+
}
|
|
242
212
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
213
|
+
if (statusCode >= 400) {
|
|
214
|
+
const detail = extractBodyDetail(resp.data || '');
|
|
215
|
+
const message = detail
|
|
216
|
+
? `(HTTP ${statusCode}) ${detail}`
|
|
217
|
+
: `(HTTP ${statusCode}) ${resp.statusText || 'Internal Server Error'}`;
|
|
218
|
+
throw { statusCode, message, body: resp.data || '' };
|
|
219
|
+
}
|
|
247
220
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
headers,
|
|
254
|
-
agent: this.config.protocol === 'http' ? undefined : new https.Agent({ rejectUnauthorized: false })
|
|
255
|
-
};
|
|
221
|
+
// Parse JSON response — handle ABAP unescaped newlines
|
|
222
|
+
const body = resp.data || '';
|
|
223
|
+
try {
|
|
224
|
+
const cleanedBody = body.replace(/\n/g, '\\n');
|
|
225
|
+
const parsed = JSON.parse(cleanedBody);
|
|
256
226
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
statusCode: res.statusCode,
|
|
262
|
-
message: `Authentication failed: ${res.statusCode}`,
|
|
263
|
-
isAuthError: true
|
|
264
|
-
});
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
227
|
+
// Check for CSRF/session errors in response body
|
|
228
|
+
if (this.isAuthError(parsed)) {
|
|
229
|
+
throw { statusCode, message: 'CSRF token or session error', body: parsed, isAuthError: true };
|
|
230
|
+
}
|
|
267
231
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
message,
|
|
280
|
-
body: body
|
|
281
|
-
});
|
|
282
|
-
});
|
|
283
|
-
return;
|
|
232
|
+
return parsed;
|
|
233
|
+
} catch (e) {
|
|
234
|
+
if (e.isAuthError) throw e;
|
|
235
|
+
// Fallback: try to extract JSON from response
|
|
236
|
+
const jsonMatch = body.match(/\{[\s\S]*\}/);
|
|
237
|
+
if (jsonMatch) {
|
|
238
|
+
try {
|
|
239
|
+
const parsed = JSON.parse(jsonMatch[0].replace(/\n/g, '\\n'));
|
|
240
|
+
return parsed;
|
|
241
|
+
} catch (e2) {
|
|
242
|
+
throw { statusCode, message: 'Failed to parse JSON response', body, error: e2.message };
|
|
284
243
|
}
|
|
285
|
-
|
|
286
|
-
let body = '';
|
|
287
|
-
res.on('data', chunk => body += chunk);
|
|
288
|
-
res.on('end', () => {
|
|
289
|
-
try {
|
|
290
|
-
// Handle unescaped newlines from ABAP - replace actual newlines with \n
|
|
291
|
-
const cleanedBody = body.replace(/\n/g, '\\n');
|
|
292
|
-
const parsed = JSON.parse(cleanedBody);
|
|
293
|
-
|
|
294
|
-
// Check for CSRF/session errors in response body
|
|
295
|
-
if (this.isAuthError(parsed)) {
|
|
296
|
-
reject({
|
|
297
|
-
statusCode: res.statusCode,
|
|
298
|
-
message: 'CSRF token or session error',
|
|
299
|
-
body: parsed,
|
|
300
|
-
isAuthError: true
|
|
301
|
-
});
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
resolve(parsed);
|
|
306
|
-
} catch (e) {
|
|
307
|
-
// Fallback: try to extract JSON from response
|
|
308
|
-
const jsonMatch = body.match(/\{[\s\S]*\}/);
|
|
309
|
-
if (jsonMatch) {
|
|
310
|
-
try {
|
|
311
|
-
const parsed = JSON.parse(jsonMatch[0].replace(/\n/g, '\\n'));
|
|
312
|
-
resolve(parsed);
|
|
313
|
-
} catch (e2) {
|
|
314
|
-
// JSON parse failed - reject instead of resolve
|
|
315
|
-
reject({
|
|
316
|
-
statusCode: res.statusCode,
|
|
317
|
-
message: 'Failed to parse JSON response',
|
|
318
|
-
body: body,
|
|
319
|
-
error: e2.message
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
} else {
|
|
323
|
-
// No JSON found in body - reject instead of resolve
|
|
324
|
-
reject({
|
|
325
|
-
statusCode: res.statusCode,
|
|
326
|
-
message: 'Invalid response format (not JSON)',
|
|
327
|
-
body: body,
|
|
328
|
-
error: e.message
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
});
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
req.on('error', reject);
|
|
336
|
-
|
|
337
|
-
if (data) {
|
|
338
|
-
req.write(JSON.stringify(data));
|
|
339
244
|
}
|
|
340
|
-
|
|
341
|
-
}
|
|
245
|
+
throw { statusCode, message: 'Invalid response format (not JSON)', body, error: e.message };
|
|
246
|
+
}
|
|
342
247
|
}
|
|
343
248
|
|
|
344
|
-
/**
|
|
345
|
-
* Convenience method for GET requests
|
|
346
|
-
* @param {string} urlPath - URL path
|
|
347
|
-
* @param {object} options - Additional options
|
|
348
|
-
* @returns {Promise<object>} Response JSON
|
|
349
|
-
*/
|
|
350
249
|
async get(urlPath, options = {}) {
|
|
351
250
|
return this.request('GET', urlPath, null, options);
|
|
352
251
|
}
|
|
353
252
|
|
|
354
|
-
/**
|
|
355
|
-
* Convenience method for POST requests
|
|
356
|
-
* @param {string} urlPath - URL path
|
|
357
|
-
* @param {object} data - Request body
|
|
358
|
-
* @param {object} options - Additional options (must include csrfToken)
|
|
359
|
-
* @returns {Promise<object>} Response JSON
|
|
360
|
-
*/
|
|
361
253
|
async post(urlPath, data, options = {}) {
|
|
362
254
|
return this.request('POST', urlPath, data, options);
|
|
363
255
|
}
|
|
364
256
|
|
|
365
|
-
/**
|
|
366
|
-
* Convenience method for DELETE requests
|
|
367
|
-
* @param {string} urlPath - URL path
|
|
368
|
-
* @param {object} options - Additional options
|
|
369
|
-
* @returns {Promise<object>} Response JSON
|
|
370
|
-
*/
|
|
371
257
|
async delete(urlPath, options = {}) {
|
|
372
258
|
return this.request('DELETE', urlPath, null, options);
|
|
373
259
|
}
|
|
374
260
|
}
|
|
375
261
|
|
|
376
|
-
module.exports = {
|
|
377
|
-
AbapHttp
|
|
378
|
-
};
|
|
262
|
+
module.exports = { AbapHttp };
|