@abapify/adt-client 0.4.0 → 0.4.1
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/dist/index.d.mts +467 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +969 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +4 -4
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,969 @@
|
|
|
1
|
+
import { exception } from "@abapify/adt-schemas";
|
|
2
|
+
import { adtContract, createAdtClient as createAdtClient$1 } from "@abapify/adt-contracts";
|
|
3
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
4
|
+
import { dirname, join } from "path";
|
|
5
|
+
|
|
6
|
+
//#region src/utils/session.ts
|
|
7
|
+
/**
|
|
8
|
+
* Cookie Store - Manages HTTP cookies for stateful sessions
|
|
9
|
+
*/
|
|
10
|
+
var CookieStore = class {
|
|
11
|
+
cookies = /* @__PURE__ */ new Map();
|
|
12
|
+
/**
|
|
13
|
+
* Parse Set-Cookie header and store cookies
|
|
14
|
+
* Handles complex cookie strings with expires dates
|
|
15
|
+
*/
|
|
16
|
+
parseCookies(setCookieHeader) {
|
|
17
|
+
const cookieStrings = this.splitCookieHeader(setCookieHeader);
|
|
18
|
+
for (const cookieString of cookieStrings) {
|
|
19
|
+
const [name, value] = cookieString.split(";")[0].trim().split("=", 2);
|
|
20
|
+
if (name && value && !name.includes("expires") && !name.includes("path")) this.cookies.set(name.trim(), value.trim());
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Split Set-Cookie header by commas, avoiding splitting on expires dates
|
|
25
|
+
*/
|
|
26
|
+
splitCookieHeader(setCookieHeader) {
|
|
27
|
+
const cookies = [];
|
|
28
|
+
let current = "";
|
|
29
|
+
let inExpires = false;
|
|
30
|
+
for (let i = 0; i < setCookieHeader.length; i++) {
|
|
31
|
+
const char = setCookieHeader[i];
|
|
32
|
+
if (char === "," && !inExpires) {
|
|
33
|
+
if (current.trim()) cookies.push(current.trim());
|
|
34
|
+
current = "";
|
|
35
|
+
} else {
|
|
36
|
+
current += char;
|
|
37
|
+
if (current.toLowerCase().includes("expires=")) inExpires = true;
|
|
38
|
+
if (inExpires && (char === ";" || i === setCookieHeader.length - 1)) inExpires = false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (current.trim()) cookies.push(current.trim());
|
|
42
|
+
return cookies;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Get Cookie header value for requests
|
|
46
|
+
*/
|
|
47
|
+
getCookieHeader() {
|
|
48
|
+
if (this.cookies.size === 0) return;
|
|
49
|
+
return Array.from(this.cookies.entries()).map(([name, value]) => `${name}=${value}`).join("; ");
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Check if we have any cookies
|
|
53
|
+
*/
|
|
54
|
+
hasCookies() {
|
|
55
|
+
return this.cookies.size > 0;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Clear all cookies
|
|
59
|
+
*/
|
|
60
|
+
clear() {
|
|
61
|
+
this.cookies.clear();
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Get all cookies as a map (for testing/debugging)
|
|
65
|
+
*/
|
|
66
|
+
getAll() {
|
|
67
|
+
return new Map(this.cookies);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Inject pre-existing cookies (e.g., from SAML authentication)
|
|
71
|
+
* @param cookieString Cookie string in "name=value; name2=value2" format
|
|
72
|
+
*/
|
|
73
|
+
injectCookie(cookieString) {
|
|
74
|
+
const cookies = cookieString.split("; ");
|
|
75
|
+
for (const cookie of cookies) {
|
|
76
|
+
const [name, ...valueParts] = cookie.split("=");
|
|
77
|
+
const value = valueParts.join("=");
|
|
78
|
+
if (name && value) this.cookies.set(name.trim(), value.trim());
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* CSRF Token Manager - Handles CSRF token caching and initialization
|
|
84
|
+
*/
|
|
85
|
+
var CsrfTokenManager = class {
|
|
86
|
+
cachedToken;
|
|
87
|
+
/**
|
|
88
|
+
* Extract CSRF token from cookies (SAP stores it there)
|
|
89
|
+
*/
|
|
90
|
+
extractFromCookies(cookies) {
|
|
91
|
+
const xsrfEntry = Array.from(cookies.entries()).find(([key]) => key.toLowerCase().includes("xsrf") || key.toLowerCase().includes("csrf"));
|
|
92
|
+
if (!xsrfEntry) return;
|
|
93
|
+
const cookieValue = xsrfEntry[1];
|
|
94
|
+
const decodedToken = decodeURIComponent(cookieValue);
|
|
95
|
+
const tokenMatch = decodedToken.match(/^([A-Za-z0-9+/_-]+=*)/);
|
|
96
|
+
const actualToken = tokenMatch ? tokenMatch[1] : decodedToken;
|
|
97
|
+
if (actualToken && actualToken !== "Required" && actualToken !== "fetch" && actualToken !== "Fetch") return actualToken;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Extract CSRF token from response header
|
|
101
|
+
*/
|
|
102
|
+
extractFromHeader(headerValue) {
|
|
103
|
+
if (!headerValue || headerValue === "Required" || headerValue === "fetch" || headerValue === "Fetch") return;
|
|
104
|
+
return headerValue;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Cache a CSRF token
|
|
108
|
+
*/
|
|
109
|
+
cache(token) {
|
|
110
|
+
this.cachedToken = token;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Get cached CSRF token
|
|
114
|
+
*/
|
|
115
|
+
getCached() {
|
|
116
|
+
return this.cachedToken;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Clear cached CSRF token (e.g., on 403 errors)
|
|
120
|
+
*/
|
|
121
|
+
clear() {
|
|
122
|
+
this.cachedToken = void 0;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Check if we have a cached token
|
|
126
|
+
*/
|
|
127
|
+
hasCached() {
|
|
128
|
+
return !!this.cachedToken;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Alias for hasCached() - for consistency with SessionManager API
|
|
132
|
+
*/
|
|
133
|
+
hasToken() {
|
|
134
|
+
return this.hasCached();
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* ETag Manager - Tracks ETags for optimistic locking
|
|
139
|
+
*/
|
|
140
|
+
var ETagManager = class {
|
|
141
|
+
etags = /* @__PURE__ */ new Map();
|
|
142
|
+
/**
|
|
143
|
+
* Extract and cache ETag from response header
|
|
144
|
+
* @param url The request URL (used as key)
|
|
145
|
+
* @param headerValue The ETag header value
|
|
146
|
+
*/
|
|
147
|
+
cacheFromHeader(url, headerValue) {
|
|
148
|
+
if (headerValue) {
|
|
149
|
+
const key = this.normalizeUrl(url);
|
|
150
|
+
this.etags.set(key, headerValue);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Get cached ETag for a URL
|
|
155
|
+
*/
|
|
156
|
+
get(url) {
|
|
157
|
+
const key = this.normalizeUrl(url);
|
|
158
|
+
return this.etags.get(key);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Clear cached ETag for a URL
|
|
162
|
+
*/
|
|
163
|
+
clear(url) {
|
|
164
|
+
if (url) {
|
|
165
|
+
const key = this.normalizeUrl(url);
|
|
166
|
+
this.etags.delete(key);
|
|
167
|
+
} else this.etags.clear();
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Normalize URL to use as cache key.
|
|
171
|
+
* Removes query params, trailing slashes, and lowercases the path.
|
|
172
|
+
* SAP ADT URLs are case-insensitive, so /fmodules/ZAGE_FM and
|
|
173
|
+
* /fmodules/zage_fm must share the same ETag cache entry.
|
|
174
|
+
*/
|
|
175
|
+
normalizeUrl(url) {
|
|
176
|
+
try {
|
|
177
|
+
return new URL(url).pathname.replace(/\/$/, "").toLowerCase();
|
|
178
|
+
} catch {
|
|
179
|
+
return url.split("?")[0].replace(/\/$/, "").toLowerCase();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* Session Manager - Orchestrates cookies and CSRF tokens
|
|
185
|
+
*/
|
|
186
|
+
var SessionManager = class {
|
|
187
|
+
cookieStore = new CookieStore();
|
|
188
|
+
csrfManager = new CsrfTokenManager();
|
|
189
|
+
etagManager = new ETagManager();
|
|
190
|
+
securitySessionActive = false;
|
|
191
|
+
constructor(logger) {
|
|
192
|
+
this.logger = logger;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Process response to update session state
|
|
196
|
+
* Extracts cookies, CSRF tokens, and ETags
|
|
197
|
+
* @param response The HTTP response
|
|
198
|
+
* @param url Optional URL for ETag caching (required for ETag tracking)
|
|
199
|
+
*/
|
|
200
|
+
processResponse(response, url) {
|
|
201
|
+
if (url) {
|
|
202
|
+
const etag = response.headers.get("etag");
|
|
203
|
+
if (etag) {
|
|
204
|
+
this.etagManager.cacheFromHeader(url, etag);
|
|
205
|
+
this.logger?.debug(`Session: ETag cached for ${url}: ${etag}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const setCookieHeader = response.headers.get("set-cookie");
|
|
209
|
+
if (setCookieHeader) {
|
|
210
|
+
this.cookieStore.parseCookies(setCookieHeader);
|
|
211
|
+
this.logger?.debug("Session: Cookies updated from response");
|
|
212
|
+
}
|
|
213
|
+
const csrfHeader = response.headers.get("x-csrf-token");
|
|
214
|
+
const csrfToken = this.csrfManager.extractFromHeader(csrfHeader);
|
|
215
|
+
if (csrfToken) {
|
|
216
|
+
this.csrfManager.cache(csrfToken);
|
|
217
|
+
this.logger?.debug("Session: CSRF token cached from header");
|
|
218
|
+
}
|
|
219
|
+
if (!csrfToken && this.cookieStore.hasCookies()) {
|
|
220
|
+
const cookieCsrf = this.csrfManager.extractFromCookies(this.cookieStore.getAll());
|
|
221
|
+
if (cookieCsrf) {
|
|
222
|
+
this.csrfManager.cache(cookieCsrf);
|
|
223
|
+
this.logger?.debug("Session: CSRF token cached from cookies");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Get headers for next request
|
|
229
|
+
* @param method HTTP method
|
|
230
|
+
* @param url Optional URL for ETag lookup (for PUT/PATCH with optimistic locking)
|
|
231
|
+
*/
|
|
232
|
+
getRequestHeaders(method, url) {
|
|
233
|
+
const headers = {};
|
|
234
|
+
if (this.securitySessionActive) headers["x-sap-security-session"] = "use";
|
|
235
|
+
const cookieHeader = this.cookieStore.getCookieHeader();
|
|
236
|
+
if (cookieHeader) headers.Cookie = cookieHeader;
|
|
237
|
+
if ([
|
|
238
|
+
"POST",
|
|
239
|
+
"PUT",
|
|
240
|
+
"DELETE",
|
|
241
|
+
"PATCH"
|
|
242
|
+
].includes(method.toUpperCase())) {
|
|
243
|
+
const cachedToken = this.csrfManager.getCached();
|
|
244
|
+
if (cachedToken) headers["x-csrf-token"] = cachedToken;
|
|
245
|
+
else headers["x-csrf-token"] = "Fetch";
|
|
246
|
+
}
|
|
247
|
+
if (["PUT", "PATCH"].includes(method.toUpperCase()) && url) {
|
|
248
|
+
const etag = this.etagManager.get(url);
|
|
249
|
+
if (etag) {
|
|
250
|
+
headers["If-Match"] = etag;
|
|
251
|
+
this.logger?.debug(`Session: Using cached ETag for If-Match: ${etag}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return headers;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Get cached ETag for a URL
|
|
258
|
+
*/
|
|
259
|
+
getETag(url) {
|
|
260
|
+
return this.etagManager.get(url);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Clear cached ETag for a URL
|
|
264
|
+
*/
|
|
265
|
+
clearETag(url) {
|
|
266
|
+
this.etagManager.clear(url);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Initialize CSRF token using the Eclipse ADT security session flow:
|
|
270
|
+
*
|
|
271
|
+
* 1. GET /sessions + x-sap-security-session: create → create security session
|
|
272
|
+
* 2. GET /sessions + x-sap-security-session: use + x-csrf-token: Fetch → get CSRF token
|
|
273
|
+
* 3. DELETE /sessions/<id> + x-sap-security-session: use + x-csrf-token → destroy session
|
|
274
|
+
*
|
|
275
|
+
* The CSRF token survives the session deletion and remains valid for
|
|
276
|
+
* all subsequent lock/unlock operations. Deleting the session frees
|
|
277
|
+
* the slot — SAP allows only one security session per user.
|
|
278
|
+
*
|
|
279
|
+
* @param baseUrl - SAP system base URL
|
|
280
|
+
* @param authHeader - Authorization header (Basic/Bearer), or undefined for cookie auth
|
|
281
|
+
* @param client - SAP client number
|
|
282
|
+
* @param language - SAP language
|
|
283
|
+
*/
|
|
284
|
+
async initializeCsrf(baseUrl, authHeader, client, language) {
|
|
285
|
+
const sessionsUrl = new URL("/sap/bc/adt/core/http/sessions", baseUrl);
|
|
286
|
+
if (client) sessionsUrl.searchParams.append("sap-client", client);
|
|
287
|
+
if (language) sessionsUrl.searchParams.append("sap-language", language);
|
|
288
|
+
/** Build common headers, optionally including Authorization and cookies */
|
|
289
|
+
const baseHeaders = () => {
|
|
290
|
+
const h = {
|
|
291
|
+
Accept: "application/vnd.sap.adt.core.http.session.v3+xml",
|
|
292
|
+
"X-sap-adt-sessiontype": "stateful"
|
|
293
|
+
};
|
|
294
|
+
if (authHeader) h.Authorization = authHeader;
|
|
295
|
+
const cookie = this.cookieStore.getCookieHeader();
|
|
296
|
+
if (cookie) h.Cookie = cookie;
|
|
297
|
+
return h;
|
|
298
|
+
};
|
|
299
|
+
try {
|
|
300
|
+
this.logger?.debug("Session: Creating security session");
|
|
301
|
+
const createResponse = await fetch(sessionsUrl.toString(), {
|
|
302
|
+
method: "GET",
|
|
303
|
+
headers: {
|
|
304
|
+
...baseHeaders(),
|
|
305
|
+
"x-sap-security-session": "create"
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
if (!createResponse.ok) {
|
|
309
|
+
this.logger?.warn(`Session: Security session creation failed with status ${createResponse.status}`);
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
this.processResponse(createResponse);
|
|
313
|
+
const sessionPath = (await createResponse.text()).match(/href="([^"]*\/sessions\/[^"]*)"/)?.[1];
|
|
314
|
+
this.logger?.debug("Session: Fetching CSRF token");
|
|
315
|
+
const csrfResponse = await fetch(sessionsUrl.toString(), {
|
|
316
|
+
method: "GET",
|
|
317
|
+
headers: {
|
|
318
|
+
...baseHeaders(),
|
|
319
|
+
"x-sap-security-session": "use",
|
|
320
|
+
"x-csrf-token": "Fetch"
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
if (!csrfResponse.ok) {
|
|
324
|
+
this.logger?.warn(`Session: CSRF fetch failed with status ${csrfResponse.status}`);
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
this.processResponse(csrfResponse);
|
|
328
|
+
if (!this.csrfManager.hasCached()) {
|
|
329
|
+
this.logger?.warn("Session: CSRF fetch succeeded but no token found in response");
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
this.securitySessionActive = true;
|
|
333
|
+
this.logger?.debug("Session: CSRF token acquired");
|
|
334
|
+
if (sessionPath) {
|
|
335
|
+
const deleteUrl = new URL(sessionPath, baseUrl);
|
|
336
|
+
if (client) deleteUrl.searchParams.append("sap-client", client);
|
|
337
|
+
try {
|
|
338
|
+
this.logger?.debug(`Session: Deleting security session ${sessionPath}`);
|
|
339
|
+
await fetch(deleteUrl.toString(), {
|
|
340
|
+
method: "DELETE",
|
|
341
|
+
headers: {
|
|
342
|
+
...baseHeaders(),
|
|
343
|
+
"x-sap-security-session": "use",
|
|
344
|
+
"x-csrf-token": this.csrfManager.getCached()
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
this.logger?.debug("Session: Security session deleted");
|
|
348
|
+
} catch {
|
|
349
|
+
this.logger?.debug("Session: Failed to delete security session (will expire)");
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return true;
|
|
353
|
+
} catch (error) {
|
|
354
|
+
this.logger?.error(`Session: CSRF initialization error: ${error instanceof Error ? error.message : String(error)}`);
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Clear all session state (cookies and CSRF)
|
|
360
|
+
*/
|
|
361
|
+
clear() {
|
|
362
|
+
this.cookieStore.clear();
|
|
363
|
+
this.csrfManager.clear();
|
|
364
|
+
this.securitySessionActive = false;
|
|
365
|
+
this.logger?.debug("Session: Cleared all session state (cookies and CSRF)");
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Check if we have a cached CSRF token
|
|
369
|
+
*/
|
|
370
|
+
hasCsrfToken() {
|
|
371
|
+
return this.csrfManager.hasCached();
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Get Cookie header value for requests
|
|
375
|
+
*/
|
|
376
|
+
getCookieHeader() {
|
|
377
|
+
return this.cookieStore.getCookieHeader();
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Get session type header value
|
|
381
|
+
* SAP ADT requires 'stateful' for operations that need session persistence
|
|
382
|
+
*/
|
|
383
|
+
getSessionTypeHeader() {
|
|
384
|
+
return "stateful";
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Inject a pre-existing cookie (e.g., from SAML authentication)
|
|
388
|
+
* @param cookieString Cookie string in "name=value" format
|
|
389
|
+
*/
|
|
390
|
+
injectCookie(cookieString) {
|
|
391
|
+
this.cookieStore.injectCookie(cookieString);
|
|
392
|
+
this.logger?.debug(`Session: Injected cookie from external source`);
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
//#endregion
|
|
397
|
+
//#region src/errors.ts
|
|
398
|
+
/**
|
|
399
|
+
* ADT Error Handling
|
|
400
|
+
*
|
|
401
|
+
* SAP ADT returns errors in a standard XML format defined by exception.xsd:
|
|
402
|
+
* <exc:exception xmlns:exc="http://www.sap.com/abapxml/types/communicationframework">
|
|
403
|
+
* <namespace id="..."/>
|
|
404
|
+
* <type id="..."/>
|
|
405
|
+
* <message lang="EN">Human readable error message</message>
|
|
406
|
+
* <localizedMessage lang="EN">Localized message</localizedMessage>
|
|
407
|
+
* <properties>
|
|
408
|
+
* <entry key="key1">value1</entry>
|
|
409
|
+
* </properties>
|
|
410
|
+
* </exc:exception>
|
|
411
|
+
*
|
|
412
|
+
* Uses the generated exception schema from @abapify/adt-schemas for type-safe parsing.
|
|
413
|
+
*/
|
|
414
|
+
/**
|
|
415
|
+
* Custom error class for ADT exceptions
|
|
416
|
+
*
|
|
417
|
+
* Provides structured access to SAP ADT error details.
|
|
418
|
+
*/
|
|
419
|
+
var AdtError = class AdtError extends Error {
|
|
420
|
+
/** HTTP status code */
|
|
421
|
+
status;
|
|
422
|
+
/** HTTP status text */
|
|
423
|
+
statusText;
|
|
424
|
+
/** Parsed ADT exception data (if available) */
|
|
425
|
+
exception;
|
|
426
|
+
/** Raw response body */
|
|
427
|
+
rawBody;
|
|
428
|
+
/** Request URL */
|
|
429
|
+
url;
|
|
430
|
+
/** HTTP method */
|
|
431
|
+
method;
|
|
432
|
+
constructor(options) {
|
|
433
|
+
const baseMsg = `HTTP ${options.status}: ${options.statusText}`;
|
|
434
|
+
const detailMsg = options.exception?.message || options.exception?.localizedMessage;
|
|
435
|
+
const fullMsg = detailMsg ? `${baseMsg} - ${detailMsg}` : baseMsg;
|
|
436
|
+
super(fullMsg);
|
|
437
|
+
this.name = "AdtError";
|
|
438
|
+
this.status = options.status;
|
|
439
|
+
this.statusText = options.statusText;
|
|
440
|
+
this.url = options.url;
|
|
441
|
+
this.method = options.method;
|
|
442
|
+
this.rawBody = options.rawBody;
|
|
443
|
+
this.exception = options.exception;
|
|
444
|
+
if (Error.captureStackTrace) Error.captureStackTrace(this, AdtError);
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Get the most relevant error message
|
|
448
|
+
*/
|
|
449
|
+
get detailMessage() {
|
|
450
|
+
return this.exception?.localizedMessage || this.exception?.message || this.statusText;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Check if this is a specific error type
|
|
454
|
+
*/
|
|
455
|
+
isType(type) {
|
|
456
|
+
return this.exception?.type === type;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Check if this is from a specific namespace
|
|
460
|
+
*/
|
|
461
|
+
isNamespace(namespace) {
|
|
462
|
+
return this.exception?.namespace === namespace;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Get a property value from the exception
|
|
466
|
+
*/
|
|
467
|
+
getProperty(key) {
|
|
468
|
+
return this.exception?.properties?.[key];
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
/**
|
|
472
|
+
* Parse ADT exception XML into structured data
|
|
473
|
+
*
|
|
474
|
+
* Uses the generated exception schema from @abapify/adt-schemas for type-safe parsing.
|
|
475
|
+
*/
|
|
476
|
+
function parseAdtException(xml) {
|
|
477
|
+
if (!xml.includes("exception") || !/xmlns(?::[\w-]+)?="http:\/\/www\.sap\.com\/abapxml\/types\/communicationframework"/.test(xml)) return;
|
|
478
|
+
try {
|
|
479
|
+
const exc = exception.parse(xml).exception;
|
|
480
|
+
if (!exc) return;
|
|
481
|
+
const result = {};
|
|
482
|
+
if (exc.namespace?.id) result.namespace = exc.namespace.id;
|
|
483
|
+
if (exc.type?.id) result.type = exc.type.id;
|
|
484
|
+
if (exc.message?.$value) result.message = exc.message.$value;
|
|
485
|
+
if (exc.localizedMessage?.$value) result.localizedMessage = exc.localizedMessage.$value;
|
|
486
|
+
if (exc.properties?.entry) {
|
|
487
|
+
result.properties = {};
|
|
488
|
+
const entries = Array.isArray(exc.properties.entry) ? exc.properties.entry : [exc.properties.entry];
|
|
489
|
+
for (const entry of entries) if (entry.key && entry.$value) result.properties[entry.key] = entry.$value;
|
|
490
|
+
}
|
|
491
|
+
if (!result.namespace && !result.type && !result.message && !result.localizedMessage) return;
|
|
492
|
+
return result;
|
|
493
|
+
} catch {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Create an AdtError from HTTP response details
|
|
499
|
+
*/
|
|
500
|
+
function createAdtError(status, statusText, url, method, rawBody) {
|
|
501
|
+
return new AdtError({
|
|
502
|
+
status,
|
|
503
|
+
statusText,
|
|
504
|
+
url,
|
|
505
|
+
method,
|
|
506
|
+
rawBody,
|
|
507
|
+
exception: parseAdtException(rawBody)
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
//#endregion
|
|
512
|
+
//#region src/adapter.ts
|
|
513
|
+
function createAdtAdapter(config) {
|
|
514
|
+
const { baseUrl, username, password, cookieHeader, authorizationHeader, client, language, logger, plugins = [], onSessionExpired } = config;
|
|
515
|
+
const isSamlAuth = !!cookieHeader;
|
|
516
|
+
const basicCredentials = `${username}:${password}`;
|
|
517
|
+
const authHeader = authorizationHeader ?? (isSamlAuth ? void 0 : `Basic ${Buffer.from(basicCredentials).toString("base64")}`);
|
|
518
|
+
const sessionManager = new SessionManager(logger);
|
|
519
|
+
if (cookieHeader) sessionManager.injectCookie(cookieHeader);
|
|
520
|
+
return {
|
|
521
|
+
async request(options) {
|
|
522
|
+
logger?.debug("=== ADAPTER REQUEST START ===");
|
|
523
|
+
logger?.debug("options.url:", options.url);
|
|
524
|
+
logger?.debug("options.method:", options.method);
|
|
525
|
+
logger?.debug("options.body:", options.body ? "present" : "undefined");
|
|
526
|
+
logger?.debug("options.bodySchema:", options.bodySchema ? "present" : "undefined");
|
|
527
|
+
const url = new URL(options.url, baseUrl);
|
|
528
|
+
if (options.query) Object.entries(options.query).forEach(([key, value]) => {
|
|
529
|
+
url.searchParams.append(key, String(value));
|
|
530
|
+
});
|
|
531
|
+
if (client) url.searchParams.append("sap-client", client);
|
|
532
|
+
if (language) url.searchParams.append("sap-language", language);
|
|
533
|
+
if ([
|
|
534
|
+
"POST",
|
|
535
|
+
"PUT",
|
|
536
|
+
"DELETE",
|
|
537
|
+
"PATCH"
|
|
538
|
+
].includes(options.method.toUpperCase()) && !sessionManager.hasCsrfToken()) {
|
|
539
|
+
logger?.debug("Adapter: Initializing CSRF token before write operation");
|
|
540
|
+
await sessionManager.initializeCsrf(baseUrl, authHeader, client, language);
|
|
541
|
+
}
|
|
542
|
+
const headers = {
|
|
543
|
+
"X-sap-adt-sessiontype": sessionManager.getSessionTypeHeader(),
|
|
544
|
+
...sessionManager.getRequestHeaders(options.method, url.pathname),
|
|
545
|
+
...options.headers
|
|
546
|
+
};
|
|
547
|
+
if (authHeader) headers.Authorization = authHeader;
|
|
548
|
+
let bodySerializableSchema;
|
|
549
|
+
if (options.bodySchema && typeof options.bodySchema === "object") {
|
|
550
|
+
if ("build" in options.bodySchema && typeof options.bodySchema.build === "function") bodySerializableSchema = options.bodySchema;
|
|
551
|
+
}
|
|
552
|
+
let serializableSchema;
|
|
553
|
+
if (options.responses) {
|
|
554
|
+
const schema200 = options.responses[200];
|
|
555
|
+
logger?.debug("Schema detection:", {
|
|
556
|
+
hasSchema: !!schema200,
|
|
557
|
+
type: typeof schema200,
|
|
558
|
+
keys: schema200 && typeof schema200 === "object" ? Object.keys(schema200) : []
|
|
559
|
+
});
|
|
560
|
+
if (schema200 && typeof schema200 === "object" && "parse" in schema200 && typeof schema200.parse === "function") serializableSchema = schema200;
|
|
561
|
+
}
|
|
562
|
+
let requestBody;
|
|
563
|
+
const body = options.body;
|
|
564
|
+
logger?.debug("Request URL: " + options.url);
|
|
565
|
+
logger?.debug("Request method: " + options.method);
|
|
566
|
+
logger?.debug("options.body: " + (options.body ? JSON.stringify(options.body).substring(0, 500) : "UNDEFINED"));
|
|
567
|
+
logger?.debug("options.bodySchema: " + (options.bodySchema ? "present" : "undefined"));
|
|
568
|
+
logger?.debug("bodySerializableSchema: " + (bodySerializableSchema ? "present" : "undefined"));
|
|
569
|
+
logger?.debug("Body type: " + typeof body);
|
|
570
|
+
logger?.debug("Body value: " + (body ? JSON.stringify(body).substring(0, 500) : "UNDEFINED/NULL"));
|
|
571
|
+
if (body !== void 0 && body !== null) if (typeof body === "string") {
|
|
572
|
+
requestBody = body;
|
|
573
|
+
logger?.debug("Body: using raw string");
|
|
574
|
+
logger?.debug("Body content (first 200 chars):", requestBody.substring(0, 200));
|
|
575
|
+
} else if (bodySerializableSchema) try {
|
|
576
|
+
logger?.debug("Calling bodySerializableSchema.build with body: " + JSON.stringify(body).substring(0, 300));
|
|
577
|
+
requestBody = bodySerializableSchema.build(body);
|
|
578
|
+
logger?.debug("Body: serialized using Serializable.build()");
|
|
579
|
+
logger?.debug("Body output type: " + typeof requestBody);
|
|
580
|
+
logger?.debug("Body output length: " + requestBody?.length);
|
|
581
|
+
logger?.debug("Body content (first 500 chars): " + requestBody?.substring(0, 500));
|
|
582
|
+
} catch (buildError) {
|
|
583
|
+
logger?.error("Schema build() threw error: " + (buildError instanceof Error ? buildError.message : String(buildError)));
|
|
584
|
+
logger?.error("Stack: " + (buildError instanceof Error ? buildError.stack : "N/A"));
|
|
585
|
+
throw buildError;
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
requestBody = JSON.stringify(body);
|
|
589
|
+
logger?.debug("Body: serialized as JSON");
|
|
590
|
+
}
|
|
591
|
+
logger?.debug(`HTTP ${options.method} ${url.toString()}`);
|
|
592
|
+
{
|
|
593
|
+
const safeHeaders = {
|
|
594
|
+
...headers,
|
|
595
|
+
...headers.Authorization ? { Authorization: headers.Authorization.substring(0, 10) + "***" } : {},
|
|
596
|
+
...headers.Cookie ? { Cookie: "***" } : {},
|
|
597
|
+
...headers["x-csrf-token"] ? { "x-csrf-token": "***" } : {}
|
|
598
|
+
};
|
|
599
|
+
logger?.debug("Request headers: " + JSON.stringify(safeHeaders, null, 2));
|
|
600
|
+
}
|
|
601
|
+
const response = await fetch(url.toString(), {
|
|
602
|
+
method: options.method,
|
|
603
|
+
headers,
|
|
604
|
+
body: requestBody
|
|
605
|
+
});
|
|
606
|
+
sessionManager.processResponse(response, url.pathname);
|
|
607
|
+
logger?.debug(`Response: ${response.status} ${response.statusText}`);
|
|
608
|
+
logger?.debug("Response headers: " + JSON.stringify(Object.fromEntries(response.headers.entries()), null, 2));
|
|
609
|
+
if (!response.ok) {
|
|
610
|
+
const errorBody = await response.text();
|
|
611
|
+
const adtError = createAdtError(response.status, response.statusText, url.toString(), options.method, errorBody);
|
|
612
|
+
if (response.status === 412) {
|
|
613
|
+
const sentETag = headers["If-Match"] ?? "(none)";
|
|
614
|
+
const diag = `[If-Match: ${sentETag}, URL: ${url.pathname}]`;
|
|
615
|
+
logger?.error(`ETag mismatch: ${options.method} ${url.pathname} sent If-Match: ${sentETag}`);
|
|
616
|
+
Object.defineProperty(adtError, "message", {
|
|
617
|
+
value: `${adtError.message} ${diag}`,
|
|
618
|
+
writable: true,
|
|
619
|
+
configurable: true
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
logger?.error(`Request failed - ${adtError.message} (${options.method} ${url.toString()})`);
|
|
623
|
+
if (adtError.exception) logger?.error(`ADT Exception: namespace=${adtError.exception.namespace}, type=${adtError.exception.type}`);
|
|
624
|
+
if (errorBody && !adtError.exception) logger?.error(`Response body: ${errorBody}`);
|
|
625
|
+
if (response.status === 403) if (!(adtError.exception?.type === "ExceptionResourceNoAccess" && (adtError.message.includes("currently editing") || adtError.message.includes("is being edited")))) {
|
|
626
|
+
sessionManager.clear();
|
|
627
|
+
logger?.warn("Session cleared due to 403 Forbidden response");
|
|
628
|
+
} else logger?.warn("Lock conflict (403) — session preserved (object locked by another session)");
|
|
629
|
+
throw adtError;
|
|
630
|
+
}
|
|
631
|
+
const contentType = response.headers.get("content-type") || "";
|
|
632
|
+
const rawText = await response.text();
|
|
633
|
+
if (contentType.includes("text/html") && rawText.includes("SAMLRequest")) {
|
|
634
|
+
sessionManager.clear();
|
|
635
|
+
logger?.warn("SAML redirect detected - session expired");
|
|
636
|
+
if (onSessionExpired) {
|
|
637
|
+
logger?.info("Attempting automatic re-authentication...");
|
|
638
|
+
try {
|
|
639
|
+
const newCookie = await onSessionExpired();
|
|
640
|
+
sessionManager.injectCookie(newCookie);
|
|
641
|
+
logger?.info("Re-authentication successful, retrying request...");
|
|
642
|
+
return this.request(options);
|
|
643
|
+
} catch (refreshError) {
|
|
644
|
+
logger?.error("Re-authentication failed");
|
|
645
|
+
throw new Error("Session expired - re-authentication failed", { cause: refreshError });
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
throw new Error("Session expired - SAML re-authentication required");
|
|
649
|
+
}
|
|
650
|
+
let data;
|
|
651
|
+
if (contentType.includes("application/json") || contentType.includes("+json")) data = JSON.parse(rawText);
|
|
652
|
+
else if (contentType.includes("text/") || contentType.includes("xml")) if (serializableSchema && contentType.includes("xml")) data = serializableSchema.parse(rawText);
|
|
653
|
+
else data = rawText;
|
|
654
|
+
else data = rawText;
|
|
655
|
+
if (plugins.length > 0) {
|
|
656
|
+
const context = {
|
|
657
|
+
rawText,
|
|
658
|
+
parsedData: data,
|
|
659
|
+
url: url.toString(),
|
|
660
|
+
method: options.method,
|
|
661
|
+
contentType
|
|
662
|
+
};
|
|
663
|
+
for (const plugin of plugins) {
|
|
664
|
+
const result = await plugin.process(context);
|
|
665
|
+
if (result !== void 0) {
|
|
666
|
+
data = result;
|
|
667
|
+
context.parsedData = result;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return data;
|
|
672
|
+
},
|
|
673
|
+
clearETag(url) {
|
|
674
|
+
sessionManager.clearETag(url);
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
//#endregion
|
|
680
|
+
//#region src/services/transports.ts
|
|
681
|
+
/**
|
|
682
|
+
* Transport Service - orchestrates CTS operations
|
|
683
|
+
*/
|
|
684
|
+
var TransportService = class {
|
|
685
|
+
constructor(adt) {
|
|
686
|
+
this.adt = adt;
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* List transport requests
|
|
690
|
+
*/
|
|
691
|
+
async list(options) {
|
|
692
|
+
const response = await this.adt.cts.transportrequests.list(options);
|
|
693
|
+
return this.extractRequests(response).map((req) => this.mapToTransport(req));
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Get a single transport by number
|
|
697
|
+
*/
|
|
698
|
+
async get(trkorr) {
|
|
699
|
+
const data = await this.adt.cts.transportrequests.get(trkorr);
|
|
700
|
+
const unwrapped = data?.root && typeof data.root === "object" ? data.root : data;
|
|
701
|
+
const request = unwrapped?.request ?? unwrapped;
|
|
702
|
+
return this.mapToTransport(request);
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Create a new transport request
|
|
706
|
+
*
|
|
707
|
+
* Note: Full create implementation requires XML body building.
|
|
708
|
+
* For now, this is a placeholder that throws a helpful error.
|
|
709
|
+
* Use ADK's AdkTransportRequest.create() for full functionality.
|
|
710
|
+
*/
|
|
711
|
+
async create(_options) {
|
|
712
|
+
throw new Error("Transport creation via service layer not yet implemented. Use ADK's AdkTransportRequest.create() for now.");
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Delete a transport request
|
|
716
|
+
*/
|
|
717
|
+
async delete(trkorr) {
|
|
718
|
+
await this.adt.cts.transportrequests.delete(trkorr);
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Release a transport request
|
|
722
|
+
*
|
|
723
|
+
* Note: Release requires POST with specific action parameter.
|
|
724
|
+
* This is a placeholder for the full implementation.
|
|
725
|
+
*/
|
|
726
|
+
async release(_trkorr) {
|
|
727
|
+
throw new Error("Transport release via service layer not yet implemented.");
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Extract requests array from various response formats
|
|
731
|
+
*/
|
|
732
|
+
extractRequests(response) {
|
|
733
|
+
if (!response) return [];
|
|
734
|
+
const data = response;
|
|
735
|
+
if (Array.isArray(data.request)) return data.request;
|
|
736
|
+
if (data.request && typeof data.request === "object") return [data.request];
|
|
737
|
+
if (Array.isArray(response)) return response;
|
|
738
|
+
if (data.root && typeof data.root === "object") return this.extractRequests(data.root);
|
|
739
|
+
return [];
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Map raw request data to Transport interface
|
|
743
|
+
*/
|
|
744
|
+
mapToTransport(req) {
|
|
745
|
+
const data = req;
|
|
746
|
+
return {
|
|
747
|
+
number: String(data.trkorr ?? data.number ?? ""),
|
|
748
|
+
desc: data.as4text ?? data.desc,
|
|
749
|
+
owner: data.as4user ?? data.owner,
|
|
750
|
+
status: data.trstatus ?? data.status,
|
|
751
|
+
type: data.trfunction ?? data.type,
|
|
752
|
+
target: data.tarsystem ?? data.target,
|
|
753
|
+
tasks: this.extractTasks(data)
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Extract tasks from transport data
|
|
758
|
+
*/
|
|
759
|
+
extractTasks(data) {
|
|
760
|
+
const tasks = data.task ?? data.tasks;
|
|
761
|
+
if (!tasks) return [];
|
|
762
|
+
return (Array.isArray(tasks) ? tasks : [tasks]).map((task) => {
|
|
763
|
+
const t = task;
|
|
764
|
+
return {
|
|
765
|
+
number: String(t.trkorr ?? t.number ?? ""),
|
|
766
|
+
owner: t.as4user ?? t.owner,
|
|
767
|
+
status: t.trstatus ?? t.status
|
|
768
|
+
};
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
/**
|
|
773
|
+
* Create transport service instance
|
|
774
|
+
*/
|
|
775
|
+
function createTransportService(adt) {
|
|
776
|
+
return new TransportService(adt);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
//#endregion
|
|
780
|
+
//#region src/services/users.ts
|
|
781
|
+
/** Extract entry array from atomFeed parsed response */
|
|
782
|
+
function extractEntries(data) {
|
|
783
|
+
const feedData = data.feed;
|
|
784
|
+
if (!feedData) return [];
|
|
785
|
+
const rawEntries = feedData.entry;
|
|
786
|
+
if (!rawEntries) return [];
|
|
787
|
+
return Array.isArray(rawEntries) ? rawEntries : [rawEntries];
|
|
788
|
+
}
|
|
789
|
+
/** Map raw Atom entries to normalized UserInfo */
|
|
790
|
+
function toUserInfoList(entries) {
|
|
791
|
+
return entries.map((e) => ({
|
|
792
|
+
username: e.id ?? "",
|
|
793
|
+
fullName: e.title,
|
|
794
|
+
uri: e.link?.[0]?.href
|
|
795
|
+
}));
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* User Service - orchestrates user lookup operations
|
|
799
|
+
*/
|
|
800
|
+
var UserService = class {
|
|
801
|
+
constructor(adt) {
|
|
802
|
+
this.adt = adt;
|
|
803
|
+
}
|
|
804
|
+
/** Get the currently authenticated user via systeminformation */
|
|
805
|
+
async getCurrentUser() {
|
|
806
|
+
const sysInfo = await this.adt.core.http.systeminformation.getSystemInfo();
|
|
807
|
+
return {
|
|
808
|
+
userName: sysInfo.userName ?? "",
|
|
809
|
+
userFullName: sysInfo.userFullName,
|
|
810
|
+
systemID: sysInfo.systemID ?? "",
|
|
811
|
+
client: sysInfo.client ?? ""
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
/** Look up a specific user by exact username */
|
|
815
|
+
async getUserByName(username) {
|
|
816
|
+
const normalized = username.trim().toUpperCase();
|
|
817
|
+
if (!normalized) throw new Error("username is required");
|
|
818
|
+
return toUserInfoList(extractEntries(await this.adt.system.users.get(normalized)));
|
|
819
|
+
}
|
|
820
|
+
/** Search users by wildcard query */
|
|
821
|
+
async searchUsers(query, maxcount = 50) {
|
|
822
|
+
const normalizedQuery = query.trim();
|
|
823
|
+
if (!normalizedQuery) throw new Error("query is required");
|
|
824
|
+
if (!Number.isInteger(maxcount) || maxcount <= 0) throw new Error("maxcount must be a positive integer");
|
|
825
|
+
return toUserInfoList(extractEntries(await this.adt.system.users.search({
|
|
826
|
+
querystring: normalizedQuery,
|
|
827
|
+
maxcount
|
|
828
|
+
})));
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
function createUserService(adt) {
|
|
832
|
+
return new UserService(adt);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
//#endregion
|
|
836
|
+
//#region src/client.ts
|
|
837
|
+
/**
|
|
838
|
+
* ADT Client V2 - Contract-Based Architecture
|
|
839
|
+
*
|
|
840
|
+
* Provides typed access to SAP ADT REST APIs:
|
|
841
|
+
* - client.adt.* - Typed ADT REST contracts (speci-generated)
|
|
842
|
+
* - client.fetch() - Generic HTTP utility for raw requests
|
|
843
|
+
*
|
|
844
|
+
* Note: Business logic (transport management, etc.) has moved to @abapify/adk
|
|
845
|
+
* Use AdkTransportRequest for transport operations.
|
|
846
|
+
*/
|
|
847
|
+
function createAdtClient(config) {
|
|
848
|
+
const adapter = createAdtAdapter(config);
|
|
849
|
+
const adtClient = createAdtClient$1({
|
|
850
|
+
baseUrl: config.baseUrl,
|
|
851
|
+
adapter
|
|
852
|
+
});
|
|
853
|
+
const fetchFn = async (url, options) => {
|
|
854
|
+
const requestOptions = {
|
|
855
|
+
method: options?.method || "GET",
|
|
856
|
+
url,
|
|
857
|
+
headers: options?.headers || {},
|
|
858
|
+
body: options?.body
|
|
859
|
+
};
|
|
860
|
+
return adapter.request(requestOptions);
|
|
861
|
+
};
|
|
862
|
+
return {
|
|
863
|
+
adt: adtClient,
|
|
864
|
+
services: { transports: createTransportService(adtClient) },
|
|
865
|
+
fetch: fetchFn,
|
|
866
|
+
clearETag: (url) => adapter.clearETag(url)
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
//#endregion
|
|
871
|
+
//#region src/plugins/logging.ts
|
|
872
|
+
/**
|
|
873
|
+
* Logging plugin - logs requests and responses
|
|
874
|
+
*/
|
|
875
|
+
var LoggingPlugin = class {
|
|
876
|
+
name = "logging";
|
|
877
|
+
constructor(logger = console.log) {
|
|
878
|
+
this.logger = logger;
|
|
879
|
+
}
|
|
880
|
+
process(context) {
|
|
881
|
+
this.logger(`[${context.method}] ${context.url}`, {
|
|
882
|
+
contentType: context.contentType,
|
|
883
|
+
hasParsedData: !!context.parsedData,
|
|
884
|
+
rawSize: context.rawText.length
|
|
885
|
+
});
|
|
886
|
+
return context.parsedData;
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
//#endregion
|
|
891
|
+
//#region src/plugins/file-logging.ts
|
|
892
|
+
/**
|
|
893
|
+
* File Logging Plugin - Saves HTTP responses to files
|
|
894
|
+
*/
|
|
895
|
+
/**
|
|
896
|
+
* Plugin that logs HTTP responses to files
|
|
897
|
+
* Similar to v1's FileLogger functionality
|
|
898
|
+
*/
|
|
899
|
+
var FileLoggingPlugin = class {
|
|
900
|
+
name = "file-logging";
|
|
901
|
+
constructor(config) {
|
|
902
|
+
this.config = config;
|
|
903
|
+
}
|
|
904
|
+
process(context) {
|
|
905
|
+
try {
|
|
906
|
+
const filePath = this.generateFilePath(context.url, context.method);
|
|
907
|
+
const fullPath = join(this.config.outputDir, filePath);
|
|
908
|
+
mkdirSync(dirname(fullPath), { recursive: true });
|
|
909
|
+
writeFileSync(fullPath, context.rawText, "utf8");
|
|
910
|
+
this.config.logger?.trace(`Wrote response to: ${fullPath}`);
|
|
911
|
+
if (this.config.writeMetadata) {
|
|
912
|
+
const metadataPath = this.getMetadataPath(fullPath);
|
|
913
|
+
const metadata = {
|
|
914
|
+
url: context.url,
|
|
915
|
+
method: context.method,
|
|
916
|
+
contentType: context.contentType,
|
|
917
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
918
|
+
size: context.rawText.length
|
|
919
|
+
};
|
|
920
|
+
writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), "utf8");
|
|
921
|
+
this.config.logger?.trace(`Wrote metadata to: ${metadataPath}`);
|
|
922
|
+
}
|
|
923
|
+
} catch (error) {
|
|
924
|
+
this.config.logger?.error(`Failed to write response file: ${error instanceof Error ? error.message : String(error)}`);
|
|
925
|
+
}
|
|
926
|
+
return context.parsedData;
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Generate file path for HTTP response
|
|
930
|
+
* Converts URL to fixture-style path structure
|
|
931
|
+
*/
|
|
932
|
+
generateFilePath(url, method) {
|
|
933
|
+
const urlObj = new URL(url);
|
|
934
|
+
let path = urlObj.pathname;
|
|
935
|
+
path = path.replace(/^\/sap\/bc\/adt\/?/, "");
|
|
936
|
+
if (!path || path === "discovery") return "./adt/core/discovery.xml";
|
|
937
|
+
const [basePath] = path.split("?");
|
|
938
|
+
const segments = basePath.split("/").filter((s) => s);
|
|
939
|
+
const sourceTypes = [
|
|
940
|
+
"main",
|
|
941
|
+
"definitions",
|
|
942
|
+
"implementations",
|
|
943
|
+
"macros",
|
|
944
|
+
"testclasses"
|
|
945
|
+
];
|
|
946
|
+
const lastSegment = segments[segments.length - 1];
|
|
947
|
+
if (sourceTypes.includes(lastSegment)) return `./adt/${segments.join("/")}`;
|
|
948
|
+
const requestId = Date.now().toString() + Math.random().toString(36).slice(2, 7);
|
|
949
|
+
let dirPath = `./adt/${segments.join("/")}`;
|
|
950
|
+
if (urlObj.search) {
|
|
951
|
+
const sanitizedQuery = urlObj.search.slice(1).replaceAll(/[^a-zA-Z0-9_-]/g, "_");
|
|
952
|
+
dirPath += `/${sanitizedQuery}`;
|
|
953
|
+
}
|
|
954
|
+
const methodPrefix = method !== "GET" ? `${method.toLowerCase()}-` : "";
|
|
955
|
+
return `${dirPath}/${methodPrefix}${requestId}-response.xml`;
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Get metadata file path for a response file
|
|
959
|
+
*/
|
|
960
|
+
getMetadataPath(responsePath) {
|
|
961
|
+
if (/-response\.xml$/.test(responsePath)) return responsePath.replace(/-response\.xml$/, "-metadata.json");
|
|
962
|
+
if (/\.xml$/.test(responsePath)) return responsePath.replace(/\.xml$/, ".metadata.json");
|
|
963
|
+
return `${responsePath}.metadata.json`;
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
|
|
967
|
+
//#endregion
|
|
968
|
+
export { CookieStore, CsrfTokenManager, FileLoggingPlugin, LoggingPlugin, SessionManager, TransportService, UserService, adtContract, createAdtAdapter, createAdtClient, createTransportService, createUserService };
|
|
969
|
+
//# sourceMappingURL=index.mjs.map
|