@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.
@@ -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
- * Overrides delete() to use direct DELETE instead of /sap/bc/adt/deletion/ API.
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;;;;GAIG;AAQH,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAGzD,qBAAa,cAAe,SAAQ,QAAQ;IAC3B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC;CA+C3E"}
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
- * Overrides delete() to use direct DELETE instead of /sap/bc/adt/deletion/ API.
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;AASlC,MAAM,WAAW,yBAAyB;IACxC,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC;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;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,CA8C7B"}
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 === 'true';
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.11.2",
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",