@mcp-abap-adt/adt-clients 3.11.2 → 3.12.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/dist/batch/buildBatchPayload.js +2 -2
- package/dist/core/class/AdtClassLegacy.d.ts +17 -1
- package/dist/core/class/AdtClassLegacy.d.ts.map +1 -1
- package/dist/core/class/AdtClassLegacy.js +100 -1
- package/dist/utils/acceptNegotiation.d.ts +1 -0
- package/dist/utils/acceptNegotiation.d.ts.map +1 -1
- package/dist/utils/acceptNegotiation.js +60 -1
- package/package.json +4 -2
|
@@ -51,9 +51,9 @@ function buildBatchPayload(parts, boundary = createBatchBoundary()) {
|
|
|
51
51
|
innerRequest,
|
|
52
52
|
].join('\r\n');
|
|
53
53
|
})
|
|
54
|
-
.join('');
|
|
54
|
+
.join('\r\n');
|
|
55
55
|
return {
|
|
56
56
|
boundary,
|
|
57
|
-
body: `${multipartParts}--${boundary}--\r\n`,
|
|
57
|
+
body: `${multipartParts}\r\n--${boundary}--\r\n`,
|
|
58
58
|
};
|
|
59
59
|
}
|
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AdtClassLegacy - Class handler for legacy SAP systems (BASIS < 7.50)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* On legacy systems, the x-sap-adt-sessiontype: stateful header causes locks
|
|
5
|
+
* to be stored in ABAP session memory instead of the global enqueue server.
|
|
6
|
+
* This means lock + update + unlock MUST happen within the same stateful
|
|
7
|
+
* HTTP session — switching to stateless between lock and update invalidates
|
|
8
|
+
* the lock handle (GitHub #11).
|
|
9
|
+
*
|
|
10
|
+
* Overrides:
|
|
11
|
+
* - update() — keeps lock→check→update→unlock in one stateful session
|
|
12
|
+
* - delete() — uses direct DELETE instead of /sap/bc/adt/deletion/ API
|
|
5
13
|
*/
|
|
14
|
+
import type { IAdtOperationOptions } from '@mcp-abap-adt/interfaces';
|
|
6
15
|
import { AdtClass } from './AdtClass';
|
|
7
16
|
import type { IClassConfig, IClassState } from './types';
|
|
8
17
|
export declare class AdtClassLegacy extends AdtClass {
|
|
18
|
+
/**
|
|
19
|
+
* Update class — legacy override.
|
|
20
|
+
*
|
|
21
|
+
* Keeps lock→check→update→unlock in a single stateful session so the
|
|
22
|
+
* lock handle remains valid (legacy stores locks in ABAP session memory).
|
|
23
|
+
*/
|
|
24
|
+
update(config: Partial<IClassConfig>, options?: IAdtOperationOptions): Promise<IClassState>;
|
|
9
25
|
delete(config: Partial<IClassConfig>): Promise<IClassState>;
|
|
10
26
|
}
|
|
11
27
|
//# sourceMappingURL=AdtClassLegacy.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdtClassLegacy.d.ts","sourceRoot":"","sources":["../../../src/core/class/AdtClassLegacy.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"AdtClassLegacy.d.ts","sourceRoot":"","sources":["../../../src/core/class/AdtClassLegacy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAa,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAMhF,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAItC,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAIzD,qBAAa,cAAe,SAAQ,QAAQ;IAC1C;;;;;OAKG;IACY,MAAM,CACnB,MAAM,EAAE,OAAO,CAAC,YAAY,CAAC,EAC7B,OAAO,CAAC,EAAE,oBAAoB,GAC7B,OAAO,CAAC,WAAW,CAAC;IA6HR,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC;CA+C3E"}
|
|
@@ -2,16 +2,115 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* AdtClassLegacy - Class handler for legacy SAP systems (BASIS < 7.50)
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* On legacy systems, the x-sap-adt-sessiontype: stateful header causes locks
|
|
6
|
+
* to be stored in ABAP session memory instead of the global enqueue server.
|
|
7
|
+
* This means lock + update + unlock MUST happen within the same stateful
|
|
8
|
+
* HTTP session — switching to stateless between lock and update invalidates
|
|
9
|
+
* the lock handle (GitHub #11).
|
|
10
|
+
*
|
|
11
|
+
* Overrides:
|
|
12
|
+
* - update() — keeps lock→check→update→unlock in one stateful session
|
|
13
|
+
* - delete() — uses direct DELETE instead of /sap/bc/adt/deletion/ API
|
|
6
14
|
*/
|
|
7
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
16
|
exports.AdtClassLegacy = void 0;
|
|
9
17
|
const internalUtils_1 = require("../../utils/internalUtils");
|
|
10
18
|
const deleteLegacy_1 = require("../shared/deleteLegacy");
|
|
11
19
|
const AdtClass_1 = require("./AdtClass");
|
|
20
|
+
const activation_1 = require("./activation");
|
|
21
|
+
const check_1 = require("./check");
|
|
12
22
|
const lock_1 = require("./lock");
|
|
13
23
|
const unlock_1 = require("./unlock");
|
|
24
|
+
const update_1 = require("./update");
|
|
14
25
|
class AdtClassLegacy extends AdtClass_1.AdtClass {
|
|
26
|
+
/**
|
|
27
|
+
* Update class — legacy override.
|
|
28
|
+
*
|
|
29
|
+
* Keeps lock→check→update→unlock in a single stateful session so the
|
|
30
|
+
* lock handle remains valid (legacy stores locks in ABAP session memory).
|
|
31
|
+
*/
|
|
32
|
+
async update(config, options) {
|
|
33
|
+
if (!config.className) {
|
|
34
|
+
throw new Error('Class name is required');
|
|
35
|
+
}
|
|
36
|
+
// Low-level mode: caller owns the session
|
|
37
|
+
if (options?.lockHandle) {
|
|
38
|
+
return super.update(config, options);
|
|
39
|
+
}
|
|
40
|
+
let lockHandle;
|
|
41
|
+
const state = { errors: [] };
|
|
42
|
+
try {
|
|
43
|
+
// Enter stateful session for the entire lock→update→unlock chain
|
|
44
|
+
this.connection.setSessionType('stateful');
|
|
45
|
+
// 1. Lock
|
|
46
|
+
this.logger?.info?.('Legacy update step 1: Locking class');
|
|
47
|
+
lockHandle = await (0, lock_1.lockClass)(this.connection, config.className);
|
|
48
|
+
state.lockHandle = lockHandle;
|
|
49
|
+
this.logger?.info?.('Class locked, handle:', lockHandle);
|
|
50
|
+
// 2. Check inactive with source code
|
|
51
|
+
const codeToUpdate = options?.sourceCode || config.sourceCode;
|
|
52
|
+
if (codeToUpdate) {
|
|
53
|
+
this.logger?.info?.('Legacy update step 2: Checking inactive version');
|
|
54
|
+
state.checkResult = await (0, check_1.checkClass)(this.connection, config.className, 'inactive', codeToUpdate, this.contentTypes?.sourceArtifactContentType());
|
|
55
|
+
this.logger?.info?.('Check passed');
|
|
56
|
+
}
|
|
57
|
+
// 3. Update
|
|
58
|
+
if (codeToUpdate && lockHandle) {
|
|
59
|
+
this.logger?.info?.('Legacy update step 3: Updating class');
|
|
60
|
+
state.updateResult = await (0, update_1.updateClass)(this.connection, config.className, codeToUpdate, lockHandle, config.transportRequest, this.contentTypes?.sourceArtifactContentType());
|
|
61
|
+
this.logger?.info?.('Class updated');
|
|
62
|
+
}
|
|
63
|
+
// 4. Unlock (still within the same stateful session)
|
|
64
|
+
if (lockHandle) {
|
|
65
|
+
this.logger?.info?.('Legacy update step 4: Unlocking class');
|
|
66
|
+
state.unlockResult = await (0, unlock_1.unlockClass)(this.connection, config.className, lockHandle);
|
|
67
|
+
lockHandle = undefined;
|
|
68
|
+
this.logger?.info?.('Class unlocked');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
// Cleanup: try to unlock if still locked (within same session)
|
|
73
|
+
if (lockHandle) {
|
|
74
|
+
try {
|
|
75
|
+
this.logger?.warn?.('Unlocking class during error cleanup');
|
|
76
|
+
await (0, unlock_1.unlockClass)(this.connection, config.className, lockHandle);
|
|
77
|
+
}
|
|
78
|
+
catch (unlockError) {
|
|
79
|
+
this.logger?.warn?.('Failed to unlock during cleanup:', (0, internalUtils_1.safeErrorMessage)(unlockError));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
// Always return to stateless after the chain
|
|
86
|
+
this.connection.setSessionType('stateless');
|
|
87
|
+
}
|
|
88
|
+
// Post-lock operations (stateless is fine)
|
|
89
|
+
// 5. Final check
|
|
90
|
+
this.logger?.info?.('Legacy update step 5: Final check');
|
|
91
|
+
state.checkResult = await (0, check_1.checkClass)(this.connection, config.className, 'inactive');
|
|
92
|
+
this.logger?.info?.('Final check passed');
|
|
93
|
+
// 6. Activate (if requested)
|
|
94
|
+
if (options?.activateOnUpdate) {
|
|
95
|
+
this.logger?.info?.('Legacy update step 6: Activating class');
|
|
96
|
+
const activateResult = await (0, activation_1.activateClass)(this.connection, config.className);
|
|
97
|
+
state.activateResult = activateResult;
|
|
98
|
+
this.logger?.info?.('Class activated, status:', activateResult.status);
|
|
99
|
+
// Read with long polling to ensure object is ready after activation
|
|
100
|
+
this.logger?.info?.('Read (wait for object ready after activation)');
|
|
101
|
+
try {
|
|
102
|
+
const readState = await this.read({ className: config.className }, 'active', { withLongPolling: true });
|
|
103
|
+
if (readState) {
|
|
104
|
+
state.readResult = readState.readResult;
|
|
105
|
+
}
|
|
106
|
+
this.logger?.info?.('Object is ready after activation');
|
|
107
|
+
}
|
|
108
|
+
catch (readError) {
|
|
109
|
+
this.logger?.warn?.('Read with long polling failed after activation:', (0, internalUtils_1.safeErrorMessage)(readError));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return state;
|
|
113
|
+
}
|
|
15
114
|
async delete(config) {
|
|
16
115
|
if (!config.className) {
|
|
17
116
|
throw new Error('Class name is required');
|
|
@@ -7,6 +7,7 @@ export declare function clearAcceptCache(): void;
|
|
|
7
7
|
export declare function setAcceptCorrectionEnabled(enabled?: boolean): void;
|
|
8
8
|
export declare function getAcceptCorrectionEnabled(): boolean;
|
|
9
9
|
export declare function extractSupportedAccept(error: unknown): string[];
|
|
10
|
+
export declare function extractSupportedContentType(error: unknown): string[];
|
|
10
11
|
export declare function wrapConnectionAcceptNegotiation(connection: IAbapConnection, logger?: ILogger): void;
|
|
11
12
|
export declare function makeAdtRequestWithAcceptNegotiation<T = unknown, D = unknown>(connection: IAbapConnection, request: IAbapRequestOptions, options?: IAcceptNegotiationOptions): Promise<IAdtResponse<T, D>>;
|
|
12
13
|
//# sourceMappingURL=acceptNegotiation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"acceptNegotiation.d.ts","sourceRoot":"","sources":["../../src/utils/acceptNegotiation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,eAAe,EACf,mBAAmB,EACnB,YAAY,EACZ,OAAO,EACR,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"acceptNegotiation.d.ts","sourceRoot":"","sources":["../../src/utils/acceptNegotiation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,eAAe,EACf,mBAAmB,EACnB,YAAY,EACZ,OAAO,EACR,MAAM,0BAA0B,CAAC;AAUlC,MAAM,WAAW,yBAAyB;IACxC,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,gBAAgB,IAAI,IAAI,CAGvC;AAED,wBAAgB,0BAA0B,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAElE;AAED,wBAAgB,0BAA0B,IAAI,OAAO,CAKpD;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CAoC/D;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CAsCpE;AAcD,wBAAgB,+BAA+B,CAC7C,UAAU,EAAE,eAAe,EAC3B,MAAM,CAAC,EAAE,OAAO,GACf,IAAI,CAkBN;AAED,wBAAsB,mCAAmC,CACvD,CAAC,GAAG,OAAO,EACX,CAAC,GAAG,OAAO,EAEX,UAAU,EAAE,eAAe,EAC3B,OAAO,EAAE,mBAAmB,EAC5B,OAAO,CAAC,EAAE,yBAAyB,GAClC,OAAO,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CA6E7B"}
|
|
@@ -4,13 +4,16 @@ exports.clearAcceptCache = clearAcceptCache;
|
|
|
4
4
|
exports.setAcceptCorrectionEnabled = setAcceptCorrectionEnabled;
|
|
5
5
|
exports.getAcceptCorrectionEnabled = getAcceptCorrectionEnabled;
|
|
6
6
|
exports.extractSupportedAccept = extractSupportedAccept;
|
|
7
|
+
exports.extractSupportedContentType = extractSupportedContentType;
|
|
7
8
|
exports.wrapConnectionAcceptNegotiation = wrapConnectionAcceptNegotiation;
|
|
8
9
|
exports.makeAdtRequestWithAcceptNegotiation = makeAdtRequestWithAcceptNegotiation;
|
|
9
10
|
const acceptCache = new Map();
|
|
11
|
+
const contentTypeCache = new Map();
|
|
10
12
|
const baseRequestMap = new WeakMap();
|
|
11
13
|
let acceptCorrectionOverride;
|
|
12
14
|
function clearAcceptCache() {
|
|
13
15
|
acceptCache.clear();
|
|
16
|
+
contentTypeCache.clear();
|
|
14
17
|
}
|
|
15
18
|
function setAcceptCorrectionEnabled(enabled) {
|
|
16
19
|
acceptCorrectionOverride = enabled;
|
|
@@ -19,7 +22,7 @@ function getAcceptCorrectionEnabled() {
|
|
|
19
22
|
if (acceptCorrectionOverride !== undefined) {
|
|
20
23
|
return acceptCorrectionOverride;
|
|
21
24
|
}
|
|
22
|
-
return process.env.ADT_ACCEPT_CORRECTION
|
|
25
|
+
return process.env.ADT_ACCEPT_CORRECTION !== 'false';
|
|
23
26
|
}
|
|
24
27
|
function extractSupportedAccept(error) {
|
|
25
28
|
const types = new Set();
|
|
@@ -53,6 +56,39 @@ function extractSupportedAccept(error) {
|
|
|
53
56
|
}
|
|
54
57
|
return Array.from(types).filter(Boolean);
|
|
55
58
|
}
|
|
59
|
+
function extractSupportedContentType(error) {
|
|
60
|
+
const e = error;
|
|
61
|
+
if (e?.response?.status !== 415) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
const types = new Set();
|
|
65
|
+
const headers = (e?.response?.headers || {});
|
|
66
|
+
const headerCandidates = [
|
|
67
|
+
headers['content-type'],
|
|
68
|
+
headers['x-sap-adt-supported-content-type'],
|
|
69
|
+
headers['supported-content-type'],
|
|
70
|
+
];
|
|
71
|
+
for (const value of headerCandidates) {
|
|
72
|
+
if (typeof value === 'string') {
|
|
73
|
+
value
|
|
74
|
+
.split(',')
|
|
75
|
+
.map((entry) => entry.trim())
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
.forEach((entry) => {
|
|
78
|
+
types.add(entry);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const data = e?.response?.data;
|
|
83
|
+
const text = typeof data === 'string' ? data : data ? JSON.stringify(data) : '';
|
|
84
|
+
if (text) {
|
|
85
|
+
const matches = text.match(/[a-zA-Z0-9.+-]+\/[a-zA-Z0-9.+-]+(?:;[^,\s]+)?/g) || [];
|
|
86
|
+
for (const match of matches) {
|
|
87
|
+
types.add(match);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return Array.from(types).filter(Boolean);
|
|
91
|
+
}
|
|
56
92
|
function buildCacheKey(request) {
|
|
57
93
|
return `${request.method.toUpperCase()} ${request.url}`;
|
|
58
94
|
}
|
|
@@ -78,6 +114,12 @@ async function makeAdtRequestWithAcceptNegotiation(connection, request, options)
|
|
|
78
114
|
if (cachedAccept) {
|
|
79
115
|
headers.Accept = cachedAccept;
|
|
80
116
|
}
|
|
117
|
+
const cachedContentType = enableCorrection
|
|
118
|
+
? contentTypeCache.get(cacheKey)
|
|
119
|
+
: undefined;
|
|
120
|
+
if (cachedContentType) {
|
|
121
|
+
headers['Content-Type'] = cachedContentType;
|
|
122
|
+
}
|
|
81
123
|
const baseRequest = getBaseRequest(connection);
|
|
82
124
|
try {
|
|
83
125
|
return await baseRequest({
|
|
@@ -104,6 +146,23 @@ async function makeAdtRequestWithAcceptNegotiation(connection, request, options)
|
|
|
104
146
|
}
|
|
105
147
|
}
|
|
106
148
|
}
|
|
149
|
+
if (e.response?.status === 415) {
|
|
150
|
+
const supported = extractSupportedContentType(error);
|
|
151
|
+
if (supported.length > 0) {
|
|
152
|
+
logger?.warn?.(`Content-Type not supported for ${request.url}. Supported Content-Type: ${supported.join(', ')}`);
|
|
153
|
+
}
|
|
154
|
+
if (enableCorrection && supported.length > 0) {
|
|
155
|
+
const nextContentType = supported[0];
|
|
156
|
+
if (headers['Content-Type'] !== nextContentType) {
|
|
157
|
+
contentTypeCache.set(cacheKey, nextContentType);
|
|
158
|
+
logger?.warn?.(`Retrying ${request.url} with corrected Content-Type: ${nextContentType}`);
|
|
159
|
+
return await baseRequest({
|
|
160
|
+
...request,
|
|
161
|
+
headers: { ...headers, 'Content-Type': nextContentType },
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
107
166
|
throw error;
|
|
108
167
|
}
|
|
109
168
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcp-abap-adt/adt-clients",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.12.0",
|
|
4
4
|
"description": "ADT clients for SAP ABAP systems - AdtClient and AdtRuntimeClient",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -60,7 +60,8 @@
|
|
|
60
60
|
"shared:teardown": "npx jest --testPathIgnorePatterns node_modules --testPathPatterns admin/shared-deps/teardown",
|
|
61
61
|
"shared:check": "npx jest --testPathIgnorePatterns node_modules --testPathPatterns admin/shared-deps/check",
|
|
62
62
|
"pretest": "npm run test:check:integration",
|
|
63
|
-
"prepublishOnly": "npm run build"
|
|
63
|
+
"prepublishOnly": "npm run build",
|
|
64
|
+
"prepare": "husky"
|
|
64
65
|
},
|
|
65
66
|
"engines": {
|
|
66
67
|
"node": ">=18.0.0"
|
|
@@ -81,6 +82,7 @@
|
|
|
81
82
|
"@types/jest": "^30.0.0",
|
|
82
83
|
"@types/node": "^25.3.3",
|
|
83
84
|
"dotenv": "^17.3.1",
|
|
85
|
+
"husky": "^9.1.7",
|
|
84
86
|
"jest": "^30.0.5",
|
|
85
87
|
"jest-util": "^30.2.0",
|
|
86
88
|
"ts-jest": "^29.4.1",
|