@mcp-abap-adt/core 2.1.2 → 2.1.3
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/CHANGELOG.md +10 -0
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +0 -5
- package/dist/lib/utils.js.map +1 -1
- package/docs/README.md +1 -2
- package/docs/architecture/CONNECTION_ISOLATION.md +0 -1
- package/docs/architecture/README.md +1 -1
- package/docs/architecture/STATEFUL_SESSION_GUIDE.md +28 -717
- package/docs/deployment/DOCKER.md +1 -18
- package/docs/development/roadmaps/archive/test_locking_roadmap.md +0 -3
- package/docs/installation/CLINE_CONFIGURATION.md +1 -32
- package/docs/installation/platforms/INSTALL_LINUX.md +1 -1
- package/docs/installation/platforms/INSTALL_MACOS.md +1 -1
- package/docs/installation/platforms/INSTALL_WINDOWS.md +1 -1
- package/docs/user-guide/AVAILABLE_TOOLS.md +2 -2
- package/docs/user-guide/CLIENT_CONFIGURATION.md +1 -1
- package/docs/user-guide/HANDLERS_MANAGEMENT.md +2 -2
- package/docs/user-guide/README.md +0 -2
- package/package.json +1 -2
- package/bin/README.md +0 -125
- package/bin/lock-object.js +0 -240
- package/docs/user-guide/scenarios/COMMON_PATTERNS.md +0 -603
- package/docs/user-guide/scenarios/CREATING_CDS_VIEWS.md +0 -441
- package/docs/user-guide/scenarios/CREATING_CLASSES.md +0 -456
- package/docs/user-guide/scenarios/CREATING_FUNCTION_GROUPS.md +0 -582
- package/docs/user-guide/scenarios/README.md +0 -51
- package/docs/user-guide/scenarios/SESSION_MANAGEMENT.md +0 -335
- package/docs/user-guide/usage/README.md +0 -137
- package/docs/user-guide/usage/rap-business-objects/README.md +0 -28
- package/docs/user-guide/usage/rap-business-objects/creating-rap-bo.md +0 -189
- package/docs/user-guide/usage/rap-business-objects/deferred-activation.md +0 -82
- package/docs/user-guide/usage/simple-objects/README.md +0 -23
- package/docs/user-guide/usage/simple-objects/classes-high.md +0 -32
- package/docs/user-guide/usage/simple-objects/classes-low.md +0 -165
- package/docs/user-guide/usage/simple-objects/data-elements-high.md +0 -48
- package/docs/user-guide/usage/simple-objects/data-elements-low.md +0 -144
- package/docs/user-guide/usage/simple-objects/domains-high.md +0 -32
- package/docs/user-guide/usage/simple-objects/domains-low.md +0 -144
- package/docs/user-guide/usage/simple-objects/function-groups-high.md +0 -32
- package/docs/user-guide/usage/simple-objects/function-groups-low.md +0 -144
- package/docs/user-guide/usage/simple-objects/function-modules-high.md +0 -32
- package/docs/user-guide/usage/simple-objects/function-modules-low.md +0 -160
- package/docs/user-guide/usage/simple-objects/interfaces-high.md +0 -32
- package/docs/user-guide/usage/simple-objects/interfaces-low.md +0 -160
- package/docs/user-guide/usage/simple-objects/programs-high.md +0 -32
- package/docs/user-guide/usage/simple-objects/programs-low.md +0 -160
- package/docs/user-guide/usage/simple-objects/service-definitions-high.md +0 -32
- package/docs/user-guide/usage/simple-objects/service-definitions-low.md +0 -160
- package/docs/user-guide/usage/simple-objects/structures-high.md +0 -32
- package/docs/user-guide/usage/simple-objects/structures-low.md +0 -160
- package/docs/user-guide/usage/simple-objects/tables-high.md +0 -32
- package/docs/user-guide/usage/simple-objects/tables-low.md +0 -165
- package/docs/user-guide/usage/simple-objects/views-high.md +0 -32
- package/docs/user-guide/usage/simple-objects/views-low.md +0 -160
|
@@ -1,737 +1,48 @@
|
|
|
1
|
-
# Stateful Session
|
|
1
|
+
# Stateful ADT Session Flow
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This guide describes the **stateful ADT request flow** required for write operations
|
|
4
|
+
(lock/update/unlock) against ABAP via ADT. It is about **protocol behavior**, not
|
|
5
|
+
file-based session persistence.
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
Proper session management is **critical** for operations that modify ABAP objects (update source code, create/modify dictionary objects, etc.).
|
|
7
|
+
## Why Stateful Matters
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
Write operations must share:
|
|
10
|
+
- the same `sap-adt-connection-id` across LOCK/PUT/UNLOCK
|
|
11
|
+
- preserved cookies/CSRF tokens between requests
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|------|----------|
|
|
12
|
-
| Server / MCP handlers (this file) | `doc/architecture/STATEFUL_SESSION_GUIDE.md` |
|
|
13
|
-
| ADT clients / Builders | `@mcp-abap-adt/adt-clients` npm package documentation |
|
|
14
|
-
| Connection layer / HTTP session | `@mcp-abap-adt/connection` npm package documentation |
|
|
13
|
+
If these are not consistent, SAP rejects the lock handle or returns CSRF errors.
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
## Required Headers
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
Each request in the flow must include:
|
|
19
18
|
|
|
20
|
-
## Table of Contents
|
|
21
|
-
|
|
22
|
-
1. [Stateful Sessions](#stateful-sessions)
|
|
23
|
-
2. [Lock Mechanism](#lock-mechanism)
|
|
24
|
-
3. [Cookie Management](#cookie-management)
|
|
25
|
-
4. [Complete Workflow Examples](#complete-workflow-examples)
|
|
26
|
-
5. [Common Pitfalls](#common-pitfalls)
|
|
27
|
-
6. [Troubleshooting](#troubleshooting)
|
|
28
|
-
|
|
29
|
-
---
|
|
30
|
-
|
|
31
|
-
## Stateful Sessions
|
|
32
|
-
|
|
33
|
-
### What is a Stateful Session?
|
|
34
|
-
|
|
35
|
-
A stateful session in SAP ADT API ensures that multiple related requests (LOCK → PUT → UNLOCK) are executed within the same SAP session context. This is required for:
|
|
36
|
-
- Lock acquisition and release
|
|
37
|
-
- Modifying ABAP objects
|
|
38
|
-
- Transport request handling
|
|
39
|
-
- Consistent transaction context
|
|
40
|
-
|
|
41
|
-
### How to Create a Stateful Session
|
|
42
|
-
|
|
43
|
-
#### 1. Generate Session ID
|
|
44
|
-
|
|
45
|
-
Each stateful session requires a unique `sap-adt-connection-id`:
|
|
46
|
-
|
|
47
|
-
```typescript
|
|
48
|
-
import * as crypto from 'crypto';
|
|
49
|
-
|
|
50
|
-
function generateSessionId(): string {
|
|
51
|
-
return crypto.randomUUID().replace(/-/g, '');
|
|
52
|
-
}
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
Example: `a1b2c3d4e5f6789012345678901234567890abcd`
|
|
56
|
-
|
|
57
|
-
#### 2. Generate Request ID
|
|
58
|
-
|
|
59
|
-
Each individual request within a session needs a unique `sap-adt-request-id`:
|
|
60
|
-
|
|
61
|
-
```typescript
|
|
62
|
-
function generateRequestId(): string {
|
|
63
|
-
return crypto.randomUUID().replace(/-/g, '');
|
|
64
|
-
}
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
#### 3. Required Headers for Stateful Requests
|
|
68
|
-
|
|
69
|
-
Every request in a stateful session MUST include these headers:
|
|
70
|
-
|
|
71
|
-
```typescript
|
|
72
|
-
const headers = {
|
|
73
|
-
'sap-adt-connection-id': sessionId, // Same for all requests in session
|
|
74
|
-
'sap-adt-request-id': requestId, // Unique for each request
|
|
75
|
-
'x-sap-adt-sessiontype': 'stateful', // Declares stateful session
|
|
76
|
-
'X-sap-adt-profiling': 'server-time', // Optional: performance profiling
|
|
77
|
-
// + any operation-specific headers
|
|
78
|
-
};
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
**CRITICAL**:
|
|
82
|
-
- `sap-adt-connection-id` MUST be the **same** for all requests (LOCK, PUT, UNLOCK) in one operation
|
|
83
|
-
- `sap-adt-request-id` MUST be **different** for each request
|
|
84
|
-
- Cookies MUST be preserved between requests (handled automatically by BaseAbapConnection)
|
|
85
|
-
|
|
86
|
-
---
|
|
87
|
-
|
|
88
|
-
## Lock Mechanism
|
|
89
|
-
|
|
90
|
-
### Lock Workflow
|
|
91
|
-
|
|
92
|
-
All modify operations follow this pattern:
|
|
93
|
-
|
|
94
|
-
```
|
|
95
|
-
1. LOCK - Acquire exclusive lock on object
|
|
96
|
-
2. PUT - Modify object (upload source, change metadata, etc.)
|
|
97
|
-
3. UNLOCK - Release lock
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
**CRITICAL**: If lock is not released (due to error or crash), object remains locked until:
|
|
101
|
-
- Session timeout
|
|
102
|
-
- Manual unlock via transaction SM12
|
|
103
|
-
- System restart
|
|
104
|
-
|
|
105
|
-
### Step 1: Lock Object
|
|
106
|
-
|
|
107
|
-
> 🆕 Prefer using `new LockClient(connection).lock({ objectType: 'class', objectName: 'ZCL_TEST' })`.
|
|
108
|
-
> It wraps the workflow below, logs `[LOCK] ...` lines, and records lock handles in `.locks/active-locks.json` for recovery.
|
|
109
|
-
|
|
110
|
-
#### LOCK Request
|
|
111
|
-
|
|
112
|
-
**Method**: `POST`
|
|
113
|
-
|
|
114
|
-
**URL Pattern**:
|
|
115
|
-
```
|
|
116
|
-
/sap/bc/adt/<object-path>?_action=LOCK&accessMode=MODIFY
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
Examples:
|
|
120
|
-
- Class: `/sap/bc/adt/oo/classes/zcl_test?_action=LOCK&accessMode=MODIFY`
|
|
121
|
-
- Program: `/sap/bc/adt/programs/programs/z_test?_action=LOCK&accessMode=MODIFY`
|
|
122
|
-
- Table: `/sap/bc/adt/ddic/tables/ztable?_action=LOCK&accessMode=MODIFY`
|
|
123
|
-
|
|
124
|
-
**Headers**:
|
|
125
|
-
```http
|
|
126
|
-
sap-adt-connection-id: <sessionId>
|
|
127
|
-
sap-adt-request-id: <requestId>
|
|
128
|
-
x-sap-adt-sessiontype: stateful
|
|
129
|
-
Accept: application/vnd.sap.as+xml;charset=UTF-8;dataname=com.sap.adt.lock.result;q=0.8, application/vnd.sap.as+xml;charset=UTF-8;dataname=com.sap.adt.lock.result2;q=0.9
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
**Response**: XML with lock information
|
|
133
|
-
|
|
134
|
-
```xml
|
|
135
|
-
<?xml version="1.0" encoding="utf-8"?>
|
|
136
|
-
<asx:abap version="1.0" xmlns:asx="http://www.sap.com/abapxml">
|
|
137
|
-
<asx:values>
|
|
138
|
-
<DATA>
|
|
139
|
-
<LOCK_HANDLE>BD53F3688D0F164CA3ADF06FD43C39E1CC1C3B32</LOCK_HANDLE>
|
|
140
|
-
<CORRNR>E19K905635</CORRNR>
|
|
141
|
-
<CORRUSER>OKYSLYTSIA</CORRUSER>
|
|
142
|
-
<CORRTEXT>Transport description</CORRTEXT>
|
|
143
|
-
<IS_LOCAL/>
|
|
144
|
-
<IS_LINK_UP/>
|
|
145
|
-
<MODIFICATION_SUPPORT>NoModification</MODIFICATION_SUPPORT>
|
|
146
|
-
<LINK_UP_MODE/>
|
|
147
|
-
<CORR_LOCKS/>
|
|
148
|
-
<CORR_CONTENTS/>
|
|
149
|
-
<SCOPE_MESSAGES/>
|
|
150
|
-
</DATA>
|
|
151
|
-
</asx:values>
|
|
152
|
-
</asx:abap>
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
**Extract Lock Information**:
|
|
156
|
-
|
|
157
|
-
```typescript
|
|
158
|
-
import { XMLParser } from 'fast-xml-parser';
|
|
159
|
-
|
|
160
|
-
const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '@_' });
|
|
161
|
-
const result = parser.parse(response.data);
|
|
162
|
-
|
|
163
|
-
const lockHandle = result?.['asx:abap']?.['asx:values']?.['DATA']?.['LOCK_HANDLE'];
|
|
164
|
-
const corrNr = result?.['asx:abap']?.['asx:values']?.['DATA']?.['CORRNR'];
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
**CRITICAL**:
|
|
168
|
-
- `LOCK_HANDLE` is required for PUT and UNLOCK operations
|
|
169
|
-
- `CORRNR` (transport number) is required for PUT operation
|
|
170
|
-
- If `LOCK_HANDLE` is empty/null, throw error immediately - don't proceed!
|
|
171
|
-
|
|
172
|
-
### Step 2: Modify Object (PUT)
|
|
173
|
-
|
|
174
|
-
#### PUT Request
|
|
175
|
-
|
|
176
|
-
**Method**: `PUT`
|
|
177
|
-
|
|
178
|
-
**URL Pattern** (with lock parameters):
|
|
179
|
-
```
|
|
180
|
-
/sap/bc/adt/<object-path>?lockHandle=<LOCK_HANDLE>&corrNr=<CORRNR>
|
|
181
19
|
```
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
```
|
|
185
|
-
PUT /sap/bc/adt/oo/classes/zcl_test/source/main?lockHandle=BD53F3688D0F164CA3ADF06FD43C39E1CC1C3B32&corrNr=E19K905635
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
**Headers**:
|
|
189
|
-
```http
|
|
190
|
-
sap-adt-connection-id: <sessionId> # SAME as in LOCK request
|
|
191
|
-
sap-adt-request-id: <requestId> # NEW unique ID
|
|
20
|
+
sap-adt-connection-id: <session-id> # same for all requests
|
|
21
|
+
sap-adt-request-id: <request-id> # unique per request
|
|
192
22
|
x-sap-adt-sessiontype: stateful
|
|
193
|
-
Content-Type: text/plain; charset=utf-8
|
|
194
|
-
Accept: text/plain
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
**Body**: New source code (plain text)
|
|
198
|
-
|
|
199
|
-
```abap
|
|
200
|
-
CLASS zcl_test DEFINITION
|
|
201
|
-
PUBLIC
|
|
202
|
-
FINAL
|
|
203
|
-
CREATE PUBLIC.
|
|
204
|
-
|
|
205
|
-
PUBLIC SECTION.
|
|
206
|
-
METHODS: constructor.
|
|
207
|
-
ENDCLASS.
|
|
208
|
-
|
|
209
|
-
CLASS zcl_test IMPLEMENTATION.
|
|
210
|
-
METHOD constructor.
|
|
211
|
-
WRITE: / 'Hello from updated class'.
|
|
212
|
-
ENDMETHOD.
|
|
213
|
-
ENDCLASS.
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
**CRITICAL**:
|
|
217
|
-
- Lock handle and transport number are passed as **URL parameters**, NOT headers
|
|
218
|
-
- `sap-adt-connection-id` MUST be the same as in LOCK request
|
|
219
|
-
- Content-Type MUST be `text/plain; charset=utf-8`
|
|
220
|
-
|
|
221
|
-
### Step 3: Unlock Object
|
|
222
|
-
|
|
223
|
-
#### UNLOCK Request
|
|
224
|
-
|
|
225
|
-
**Method**: `POST`
|
|
226
|
-
|
|
227
|
-
**URL Pattern**:
|
|
228
|
-
```
|
|
229
|
-
/sap/bc/adt/<object-path>?_action=UNLOCK&lockHandle=<LOCK_HANDLE>
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
Example:
|
|
233
|
-
```
|
|
234
|
-
POST /sap/bc/adt/oo/classes/zcl_test?_action=UNLOCK&lockHandle=BD53F3688D0F164CA3ADF06FD43C39E1CC1C3B32
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
**Headers**:
|
|
238
|
-
```http
|
|
239
|
-
sap-adt-connection-id: <sessionId> # SAME as in LOCK and PUT
|
|
240
|
-
sap-adt-request-id: <requestId> # NEW unique ID
|
|
241
|
-
x-sap-adt-sessiontype: stateful
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
**CRITICAL**:
|
|
245
|
-
- Always unlock in `finally` block or error handler
|
|
246
|
-
- If unlock fails, object remains locked
|
|
247
|
-
- Lock handle is passed as URL parameter
|
|
248
|
-
|
|
249
|
-
---
|
|
250
|
-
|
|
251
|
-
## Cookie Management
|
|
252
|
-
|
|
253
|
-
### Why Cookies Matter
|
|
254
|
-
|
|
255
|
-
SAP ADT API uses cookies for:
|
|
256
|
-
- Session authentication (SAP_SESSIONID)
|
|
257
|
-
- Client context (sap-usercontext)
|
|
258
|
-
- Context ID for stateful operations (sap-contextid)
|
|
259
|
-
|
|
260
|
-
**CRITICAL**: Cookies MUST be preserved and sent with every request in a stateful session.
|
|
261
|
-
|
|
262
|
-
### How Cookies Work in Our Implementation
|
|
263
|
-
|
|
264
|
-
#### BaseAbapConnection handles cookies automatically:
|
|
265
|
-
|
|
266
|
-
1. **CSRF Token Request** - First request creates SAP session and receives cookies:
|
|
267
|
-
```typescript
|
|
268
|
-
// In BaseAbapConnection.ts
|
|
269
|
-
const response = await axios.head(url, { headers: { 'x-csrf-token': 'fetch' } });
|
|
270
|
-
|
|
271
|
-
// Extract cookies from response
|
|
272
|
-
const cookies = response.headers['set-cookie'];
|
|
273
|
-
// Store in this.csrfToken and this.cookies
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
2. **Subsequent Requests** - Cookies are automatically added:
|
|
277
|
-
```typescript
|
|
278
|
-
// In BaseAbapConnection.makeAdtRequest()
|
|
279
|
-
const headers = {
|
|
280
|
-
'x-csrf-token': this.csrfToken,
|
|
281
|
-
'Cookie': this.cookies, // Automatically added
|
|
282
|
-
...userHeaders
|
|
283
|
-
};
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
### Cookie Flow Example
|
|
287
|
-
|
|
288
|
-
```
|
|
289
|
-
Request 1 (CSRF fetch):
|
|
290
|
-
→ HEAD /sap/bc/adt/discovery
|
|
291
|
-
Headers: x-csrf-token: fetch
|
|
292
|
-
← Response:
|
|
293
|
-
x-csrf-token: AbCdEf123456
|
|
294
|
-
Set-Cookie: SAP_SESSIONID_E19_100=xyz...; sap-usercontext=...
|
|
295
|
-
|
|
296
|
-
Request 2 (LOCK):
|
|
297
|
-
→ POST /sap/bc/adt/oo/classes/zcl_test?_action=LOCK
|
|
298
|
-
Headers:
|
|
299
|
-
x-csrf-token: AbCdEf123456
|
|
300
|
-
Cookie: SAP_SESSIONID_E19_100=xyz...; sap-usercontext=...
|
|
301
|
-
sap-adt-connection-id: session123
|
|
302
|
-
← Response: Lock acquired
|
|
303
|
-
|
|
304
|
-
Request 3 (PUT):
|
|
305
|
-
→ PUT /sap/bc/adt/oo/classes/zcl_test/source/main?lockHandle=...
|
|
306
|
-
Headers:
|
|
307
|
-
x-csrf-token: AbCdEf123456
|
|
308
|
-
Cookie: SAP_SESSIONID_E19_100=xyz...; sap-usercontext=...
|
|
309
|
-
sap-adt-connection-id: session123 # SAME as Request 2
|
|
310
|
-
← Response: Source updated
|
|
311
|
-
|
|
312
|
-
Request 4 (UNLOCK):
|
|
313
|
-
→ POST /sap/bc/adt/oo/classes/zcl_test?_action=UNLOCK
|
|
314
|
-
Headers:
|
|
315
|
-
x-csrf-token: AbCdEf123456
|
|
316
|
-
Cookie: SAP_SESSIONID_E19_100=xyz...; sap-usercontext=...
|
|
317
|
-
sap-adt-connection-id: session123 # SAME as Request 2 and 3
|
|
318
|
-
← Response: Lock released
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
### Manual Cookie Management (if needed)
|
|
322
|
-
|
|
323
|
-
If you need to manage cookies manually:
|
|
324
|
-
|
|
325
|
-
```typescript
|
|
326
|
-
// Extract cookies from response
|
|
327
|
-
const setCookieHeader = response.headers['set-cookie'];
|
|
328
|
-
if (setCookieHeader) {
|
|
329
|
-
const cookies = Array.isArray(setCookieHeader)
|
|
330
|
-
? setCookieHeader.join('; ')
|
|
331
|
-
: setCookieHeader;
|
|
332
|
-
|
|
333
|
-
// Store and reuse in subsequent requests
|
|
334
|
-
this.cookies = cookies;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Add cookies to next request
|
|
338
|
-
const headers = {
|
|
339
|
-
'Cookie': this.cookies,
|
|
340
|
-
// ... other headers
|
|
341
|
-
};
|
|
342
23
|
```
|
|
343
24
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
---
|
|
25
|
+
## Standard Flow
|
|
347
26
|
|
|
348
|
-
|
|
27
|
+
1) **LOCK**
|
|
28
|
+
`POST /sap/bc/adt/<object-path>?_action=LOCK&accessMode=MODIFY`
|
|
349
29
|
|
|
350
|
-
|
|
30
|
+
Response includes `LOCK_HANDLE` and `CORRNR` (transport).
|
|
351
31
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
import { XMLParser } from 'fast-xml-parser';
|
|
32
|
+
2) **PUT / UPDATE**
|
|
33
|
+
`PUT /sap/bc/adt/<object-path>?lockHandle=<LOCK_HANDLE>&corrNr=<CORRNR>`
|
|
355
34
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const sessionId = crypto.randomUUID().replace(/-/g, '');
|
|
359
|
-
let lockHandle: string | null = null;
|
|
360
|
-
|
|
361
|
-
try {
|
|
362
|
-
// Step 1: LOCK
|
|
363
|
-
const lockUrl = `/sap/bc/adt/oo/classes/${className.toLowerCase()}?_action=LOCK&accessMode=MODIFY`;
|
|
364
|
-
const lockHeaders = {
|
|
365
|
-
'sap-adt-connection-id': sessionId,
|
|
366
|
-
'sap-adt-request-id': crypto.randomUUID().replace(/-/g, ''),
|
|
367
|
-
'x-sap-adt-sessiontype': 'stateful',
|
|
368
|
-
'Accept': 'application/vnd.sap.as+xml;charset=UTF-8;dataname=com.sap.adt.lock.result2;q=0.9'
|
|
369
|
-
};
|
|
370
|
-
|
|
371
|
-
const lockResponse = await makeAdtRequest(lockUrl, 'POST', lockHeaders);
|
|
372
|
-
|
|
373
|
-
// Parse lock handle and transport number
|
|
374
|
-
const parser = new XMLParser({ ignoreAttributes: false });
|
|
375
|
-
const result = parser.parse(lockResponse.data);
|
|
376
|
-
lockHandle = result?.['asx:abap']?.['asx:values']?.['DATA']?.['LOCK_HANDLE'];
|
|
377
|
-
const corrNr = result?.['asx:abap']?.['asx:values']?.['DATA']?.['CORRNR'];
|
|
378
|
-
|
|
379
|
-
if (!lockHandle) {
|
|
380
|
-
throw new Error('Failed to obtain lock handle');
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
console.log(`✓ Lock acquired: ${lockHandle}`);
|
|
384
|
-
|
|
385
|
-
// Step 2: PUT (upload source)
|
|
386
|
-
let putUrl = `/sap/bc/adt/oo/classes/${className.toLowerCase()}/source/main?lockHandle=${lockHandle}`;
|
|
387
|
-
if (corrNr) {
|
|
388
|
-
putUrl += `&corrNr=${corrNr}`;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const putHeaders = {
|
|
392
|
-
'sap-adt-connection-id': sessionId, // SAME session ID!
|
|
393
|
-
'sap-adt-request-id': crypto.randomUUID().replace(/-/g, ''),
|
|
394
|
-
'x-sap-adt-sessiontype': 'stateful',
|
|
395
|
-
'Content-Type': 'text/plain; charset=utf-8',
|
|
396
|
-
'Accept': 'text/plain'
|
|
397
|
-
};
|
|
398
|
-
|
|
399
|
-
await makeAdtRequest(putUrl, 'PUT', putHeaders, newSourceCode);
|
|
400
|
-
console.log(`✓ Source code updated`);
|
|
401
|
-
|
|
402
|
-
// Step 3: UNLOCK
|
|
403
|
-
const unlockUrl = `/sap/bc/adt/oo/classes/${className.toLowerCase()}?_action=UNLOCK&lockHandle=${lockHandle}`;
|
|
404
|
-
const unlockHeaders = {
|
|
405
|
-
'sap-adt-connection-id': sessionId, // SAME session ID!
|
|
406
|
-
'sap-adt-request-id': crypto.randomUUID().replace(/-/g, ''),
|
|
407
|
-
'x-sap-adt-sessiontype': 'stateful'
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
await makeAdtRequest(unlockUrl, 'POST', unlockHeaders);
|
|
411
|
-
lockHandle = null;
|
|
412
|
-
console.log(`✓ Lock released`);
|
|
413
|
-
|
|
414
|
-
} catch (error) {
|
|
415
|
-
// CRITICAL: Always unlock on error
|
|
416
|
-
if (lockHandle) {
|
|
417
|
-
try {
|
|
418
|
-
const unlockUrl = `/sap/bc/adt/oo/classes/${className.toLowerCase()}?_action=UNLOCK&lockHandle=${lockHandle}`;
|
|
419
|
-
await makeAdtRequest(unlockUrl, 'POST', {
|
|
420
|
-
'sap-adt-connection-id': sessionId,
|
|
421
|
-
'sap-adt-request-id': crypto.randomUUID().replace(/-/g, ''),
|
|
422
|
-
'x-sap-adt-sessiontype': 'stateful'
|
|
423
|
-
});
|
|
424
|
-
console.log('Lock released after error');
|
|
425
|
-
} catch (unlockError) {
|
|
426
|
-
console.error('Failed to unlock after error:', unlockError);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
throw error;
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
```
|
|
433
|
-
|
|
434
|
-
### Example 2: Update Program Source Code
|
|
435
|
-
|
|
436
|
-
```typescript
|
|
437
|
-
async function updateProgramSource(programName: string, newSourceCode: string) {
|
|
438
|
-
const sessionId = crypto.randomUUID().replace(/-/g, '');
|
|
439
|
-
let lockHandle: string | null = null;
|
|
440
|
-
|
|
441
|
-
try {
|
|
442
|
-
// Step 1: LOCK
|
|
443
|
-
const lockUrl = `/sap/bc/adt/programs/programs/${programName.toLowerCase()}?_action=LOCK&accessMode=MODIFY`;
|
|
444
|
-
const lockResponse = await makeAdtRequest(lockUrl, 'POST', {
|
|
445
|
-
'sap-adt-connection-id': sessionId,
|
|
446
|
-
'sap-adt-request-id': crypto.randomUUID().replace(/-/g, ''),
|
|
447
|
-
'x-sap-adt-sessiontype': 'stateful',
|
|
448
|
-
'Accept': 'application/vnd.sap.as+xml;charset=UTF-8;dataname=com.sap.adt.lock.result2;q=0.9'
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
const parser = new XMLParser({ ignoreAttributes: false });
|
|
452
|
-
const result = parser.parse(lockResponse.data);
|
|
453
|
-
lockHandle = result?.['asx:abap']?.['asx:values']?.['DATA']?.['LOCK_HANDLE'];
|
|
454
|
-
const corrNr = result?.['asx:abap']?.['asx:values']?.['DATA']?.['CORRNR'];
|
|
455
|
-
|
|
456
|
-
if (!lockHandle) {
|
|
457
|
-
throw new Error('Failed to obtain lock handle');
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// Step 2: PUT
|
|
461
|
-
let putUrl = `/sap/bc/adt/programs/programs/${programName.toLowerCase()}/source/main?lockHandle=${lockHandle}`;
|
|
462
|
-
if (corrNr) {
|
|
463
|
-
putUrl += `&corrNr=${corrNr}`;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
await makeAdtRequest(putUrl, 'PUT', {
|
|
467
|
-
'sap-adt-connection-id': sessionId,
|
|
468
|
-
'sap-adt-request-id': crypto.randomUUID().replace(/-/g, ''),
|
|
469
|
-
'x-sap-adt-sessiontype': 'stateful',
|
|
470
|
-
'Content-Type': 'text/plain; charset=utf-8',
|
|
471
|
-
'Accept': 'text/plain'
|
|
472
|
-
}, newSourceCode);
|
|
473
|
-
|
|
474
|
-
// Step 3: UNLOCK
|
|
475
|
-
const unlockUrl = `/sap/bc/adt/programs/programs/${programName.toLowerCase()}?_action=UNLOCK&lockHandle=${lockHandle}`;
|
|
476
|
-
await makeAdtRequest(unlockUrl, 'POST', {
|
|
477
|
-
'sap-adt-connection-id': sessionId,
|
|
478
|
-
'sap-adt-request-id': crypto.randomUUID().replace(/-/g, ''),
|
|
479
|
-
'x-sap-adt-sessiontype': 'stateful'
|
|
480
|
-
});
|
|
481
|
-
lockHandle = null;
|
|
482
|
-
|
|
483
|
-
} catch (error) {
|
|
484
|
-
if (lockHandle) {
|
|
485
|
-
try {
|
|
486
|
-
const unlockUrl = `/sap/bc/adt/programs/programs/${programName.toLowerCase()}?_action=UNLOCK&lockHandle=${lockHandle}`;
|
|
487
|
-
await makeAdtRequest(unlockUrl, 'POST', {
|
|
488
|
-
'sap-adt-connection-id': sessionId,
|
|
489
|
-
'sap-adt-request-id': crypto.randomUUID().replace(/-/g, ''),
|
|
490
|
-
'x-sap-adt-sessiontype': 'stateful'
|
|
491
|
-
});
|
|
492
|
-
} catch (unlockError) {
|
|
493
|
-
console.error('Failed to unlock:', unlockError);
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
throw error;
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
```
|
|
500
|
-
|
|
501
|
-
---
|
|
35
|
+
3) **UNLOCK**
|
|
36
|
+
`POST /sap/bc/adt/<object-path>?_action=UNLOCK&lockHandle=<LOCK_HANDLE>`
|
|
502
37
|
|
|
503
38
|
## Common Pitfalls
|
|
504
39
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
// WRONG - Different session IDs
|
|
509
|
-
const lockSessionId = generateSessionId();
|
|
510
|
-
await lock(lockSessionId);
|
|
511
|
-
|
|
512
|
-
const putSessionId = generateSessionId(); // ❌ NEW ID!
|
|
513
|
-
await put(putSessionId); // Will fail with "invalid lock handle"
|
|
514
|
-
```
|
|
515
|
-
|
|
516
|
-
```typescript
|
|
517
|
-
// CORRECT - Same session ID
|
|
518
|
-
const sessionId = generateSessionId();
|
|
519
|
-
await lock(sessionId);
|
|
520
|
-
await put(sessionId); // ✓ Same ID
|
|
521
|
-
await unlock(sessionId); // ✓ Same ID
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
### 2. ❌ Lock Handle in Headers Instead of URL
|
|
525
|
-
|
|
526
|
-
```typescript
|
|
527
|
-
// WRONG
|
|
528
|
-
const headers = {
|
|
529
|
-
'sap-adt-lockhandle': lockHandle // ❌ Wrong place!
|
|
530
|
-
};
|
|
531
|
-
await put('/sap/bc/adt/oo/classes/zcl_test/source/main', headers);
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
```typescript
|
|
535
|
-
// CORRECT
|
|
536
|
-
const url = `/sap/bc/adt/oo/classes/zcl_test/source/main?lockHandle=${lockHandle}&corrNr=${corrNr}`;
|
|
537
|
-
await put(url); // ✓ In URL
|
|
538
|
-
```
|
|
539
|
-
|
|
540
|
-
### 3. ❌ Not Unlocking on Error
|
|
541
|
-
|
|
542
|
-
```typescript
|
|
543
|
-
// WRONG
|
|
544
|
-
try {
|
|
545
|
-
await lock();
|
|
546
|
-
await put();
|
|
547
|
-
await unlock();
|
|
548
|
-
} catch (error) {
|
|
549
|
-
throw error; // ❌ Object stays locked!
|
|
550
|
-
}
|
|
551
|
-
```
|
|
552
|
-
|
|
553
|
-
```typescript
|
|
554
|
-
// CORRECT
|
|
555
|
-
let lockHandle = null;
|
|
556
|
-
try {
|
|
557
|
-
lockHandle = await lock();
|
|
558
|
-
await put();
|
|
559
|
-
await unlock();
|
|
560
|
-
lockHandle = null;
|
|
561
|
-
} catch (error) {
|
|
562
|
-
if (lockHandle) {
|
|
563
|
-
await unlock(); // ✓ Always unlock
|
|
564
|
-
}
|
|
565
|
-
throw error;
|
|
566
|
-
}
|
|
567
|
-
```
|
|
568
|
-
|
|
569
|
-
### 4. ❌ Missing Transport Number in PUT
|
|
570
|
-
|
|
571
|
-
```typescript
|
|
572
|
-
// WRONG
|
|
573
|
-
const url = `/sap/bc/adt/oo/classes/zcl_test/source/main?lockHandle=${lockHandle}`;
|
|
574
|
-
// ❌ Missing corrNr!
|
|
575
|
-
```
|
|
576
|
-
|
|
577
|
-
```typescript
|
|
578
|
-
// CORRECT
|
|
579
|
-
const url = `/sap/bc/adt/oo/classes/zcl_test/source/main?lockHandle=${lockHandle}&corrNr=${corrNr}`;
|
|
580
|
-
// ✓ Both parameters
|
|
581
|
-
```
|
|
582
|
-
|
|
583
|
-
### 5. ❌ Not Checking Lock Handle
|
|
584
|
-
|
|
585
|
-
```typescript
|
|
586
|
-
// WRONG
|
|
587
|
-
const lockHandle = result?.['asx:abap']?.['asx:values']?.['DATA']?.['LOCK_HANDLE'];
|
|
588
|
-
await put(lockHandle); // ❌ May be null/undefined!
|
|
589
|
-
```
|
|
590
|
-
|
|
591
|
-
```typescript
|
|
592
|
-
// CORRECT
|
|
593
|
-
const lockHandle = result?.['asx:abap']?.['asx:values']?.['DATA']?.['LOCK_HANDLE'];
|
|
594
|
-
if (!lockHandle) {
|
|
595
|
-
throw new Error('Failed to obtain lock handle');
|
|
596
|
-
}
|
|
597
|
-
await put(lockHandle); // ✓ Validated
|
|
598
|
-
```
|
|
599
|
-
|
|
600
|
-
### 6. ❌ Mixing Cookies from Different Sessions
|
|
601
|
-
|
|
602
|
-
```typescript
|
|
603
|
-
// WRONG - Using global connection for stateful operations
|
|
604
|
-
const connection1 = new Connection();
|
|
605
|
-
await connection1.lock();
|
|
606
|
-
|
|
607
|
-
const connection2 = new Connection(); // ❌ Different cookies!
|
|
608
|
-
await connection2.put(); // Will fail
|
|
609
|
-
```
|
|
610
|
-
|
|
611
|
-
```typescript
|
|
612
|
-
// CORRECT - Use same connection instance
|
|
613
|
-
const connection = new Connection();
|
|
614
|
-
await connection.lock();
|
|
615
|
-
await connection.put(); // ✓ Same cookies
|
|
616
|
-
await connection.unlock();
|
|
617
|
-
```
|
|
618
|
-
|
|
619
|
-
---
|
|
620
|
-
|
|
621
|
-
## Troubleshooting
|
|
622
|
-
|
|
623
|
-
### Error: "Resource is not locked (invalid lock handle)"
|
|
624
|
-
|
|
625
|
-
**Symptoms**: HTTP 423, `ExceptionResourceInvalidLockHandle`
|
|
626
|
-
|
|
627
|
-
**Causes**:
|
|
628
|
-
1. Lock handle not passed correctly in PUT URL
|
|
629
|
-
2. Different `sap-adt-connection-id` between LOCK and PUT
|
|
630
|
-
3. Cookies not preserved between requests
|
|
631
|
-
4. Lock handle is null/empty
|
|
632
|
-
|
|
633
|
-
**Solutions**:
|
|
634
|
-
- ✓ Verify lock handle is in URL: `?lockHandle=...&corrNr=...`
|
|
635
|
-
- ✓ Check same `sap-adt-connection-id` in all requests
|
|
636
|
-
- ✓ Enable DEBUG logging to see actual requests
|
|
637
|
-
- ✓ Verify lock handle extracted from XML response
|
|
638
|
-
|
|
639
|
-
### Error: "User X is currently editing object Y"
|
|
640
|
-
|
|
641
|
-
**Symptoms**: HTTP error, `ExceptionResourceNoAccess`
|
|
642
|
-
|
|
643
|
-
**Causes**:
|
|
644
|
-
1. Object is locked by another user or session
|
|
645
|
-
2. Previous lock was not released (crash, error)
|
|
646
|
-
|
|
647
|
-
**Solutions**:
|
|
648
|
-
- ✓ Check SM12 transaction in SAP for active locks
|
|
649
|
-
- ✓ Release stale locks manually via SM12
|
|
650
|
-
- ✓ Wait for session timeout (usually 15-30 minutes)
|
|
651
|
-
- ✓ Always use try/finally for unlock
|
|
652
|
-
|
|
653
|
-
### Error: "Session terminated" or "CSRF token invalid"
|
|
654
|
-
|
|
655
|
-
**Symptoms**: HTTP 403, CSRF token errors
|
|
656
|
-
|
|
657
|
-
**Causes**:
|
|
658
|
-
1. SAP session expired
|
|
659
|
-
2. CSRF token not refreshed
|
|
660
|
-
3. Cookies lost between requests
|
|
661
|
-
|
|
662
|
-
**Solutions**:
|
|
663
|
-
- ✓ Fetch new CSRF token before operations
|
|
664
|
-
- ✓ Verify cookies are preserved in connection
|
|
665
|
-
- ✓ Check SAP session timeout settings
|
|
666
|
-
|
|
667
|
-
### Debugging Tips
|
|
668
|
-
|
|
669
|
-
#### Enable Debug Logging
|
|
670
|
-
|
|
671
|
-
```bash
|
|
672
|
-
export DEBUG=true # Linux/Mac
|
|
673
|
-
$env:DEBUG = "true" # PowerShell
|
|
674
|
-
```
|
|
675
|
-
|
|
676
|
-
#### Inspect Requests
|
|
677
|
-
|
|
678
|
-
```typescript
|
|
679
|
-
logger.info(`LOCK URL: ${lockUrl}`);
|
|
680
|
-
logger.info(`Session ID: ${sessionId}`);
|
|
681
|
-
logger.info(`Lock response: ${JSON.stringify(lockResponse.data)}`);
|
|
682
|
-
logger.info(`Lock handle: ${lockHandle}`);
|
|
683
|
-
logger.info(`PUT URL: ${putUrl}`);
|
|
684
|
-
```
|
|
685
|
-
|
|
686
|
-
#### Check Parsed XML
|
|
687
|
-
|
|
688
|
-
```typescript
|
|
689
|
-
const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '@_' });
|
|
690
|
-
const result = parser.parse(response.data);
|
|
691
|
-
console.log('Parsed XML:', JSON.stringify(result, null, 2));
|
|
692
|
-
```
|
|
693
|
-
|
|
694
|
-
#### Verify Headers
|
|
695
|
-
|
|
696
|
-
```typescript
|
|
697
|
-
console.log('Request headers:', JSON.stringify(headers, null, 2));
|
|
698
|
-
console.log('Response headers:', JSON.stringify(response.headers, null, 2));
|
|
699
|
-
```
|
|
700
|
-
|
|
701
|
-
---
|
|
702
|
-
|
|
703
|
-
## Summary Checklist
|
|
704
|
-
|
|
705
|
-
When implementing stateful operations:
|
|
706
|
-
|
|
707
|
-
- [ ] Generate **one** session ID for entire operation
|
|
708
|
-
- [ ] Generate **unique** request ID for each request
|
|
709
|
-
- [ ] Add stateful headers to all requests:
|
|
710
|
-
- [ ] `sap-adt-connection-id`
|
|
711
|
-
- [ ] `sap-adt-request-id`
|
|
712
|
-
- [ ] `x-sap-adt-sessiontype: stateful`
|
|
713
|
-
- [ ] LOCK: Extract `LOCK_HANDLE` and `CORRNR` from XML response
|
|
714
|
-
- [ ] LOCK: Verify lock handle is not null/empty
|
|
715
|
-
- [ ] PUT: Pass `lockHandle` and `corrNr` as **URL parameters**
|
|
716
|
-
- [ ] PUT: Use correct Content-Type (`text/plain; charset=utf-8`)
|
|
717
|
-
- [ ] UNLOCK: Always execute in finally/error handler
|
|
718
|
-
- [ ] UNLOCK: Pass `lockHandle` as **URL parameter**
|
|
719
|
-
- [ ] Cookies: Preserved automatically by BaseAbapConnection
|
|
720
|
-
- [ ] Error handling: Always unlock on errors
|
|
721
|
-
- [ ] Logging: Enable DEBUG for troubleshooting
|
|
722
|
-
|
|
723
|
-
---
|
|
724
|
-
|
|
725
|
-
## References
|
|
726
|
-
|
|
727
|
-
- Eclipse ADT API Documentation
|
|
728
|
-
- SAP Note: ABAP Development Tools (ADT)
|
|
729
|
-
- Transaction SM12: Lock Management
|
|
730
|
-
- BaseAbapConnection.ts - Cookie and CSRF management
|
|
731
|
-
- `src/handlers/class/high/handleUpdateClass.ts` - Complete implementation example
|
|
40
|
+
- **Different session IDs** between LOCK/PUT/UNLOCK
|
|
41
|
+
- **New cookies/CSRF** (missing carry-over between requests)
|
|
42
|
+
- **Missing lockHandle or corrNr** in update call
|
|
732
43
|
|
|
733
|
-
|
|
44
|
+
## Notes
|
|
734
45
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
46
|
+
- Session state may be managed in-memory or by external stores (auth-broker/stores),
|
|
47
|
+
but the ADT flow always requires the same `sap-adt-connection-id` and cookies
|
|
48
|
+
across the stateful sequence.
|