@mcp-abap-adt/adt-clients 32.0.0 → 32.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -1
- package/dist/batch/AdtBatchConnection.d.ts +29 -0
- package/dist/batch/AdtBatchConnection.d.ts.map +1 -0
- package/dist/batch/AdtBatchConnection.js +155 -0
- package/dist/batch/index.d.ts +2 -0
- package/dist/batch/index.d.ts.map +1 -0
- package/dist/batch/index.js +5 -0
- package/dist/batch/wire.d.ts +10 -0
- package/dist/batch/wire.d.ts.map +1 -0
- package/dist/batch/wire.js +88 -0
- package/dist/core/accessControl/versions.d.ts +1 -1
- package/dist/core/accessControl/versions.js +1 -1
- package/dist/core/appendStructure/versions.d.ts +1 -1
- package/dist/core/appendStructure/versions.js +1 -1
- package/dist/core/behaviorDefinition/versions.d.ts +1 -1
- package/dist/core/behaviorDefinition/versions.js +1 -1
- package/dist/core/behaviorImplementation/versions.d.ts +1 -1
- package/dist/core/behaviorImplementation/versions.js +1 -1
- package/dist/core/enhancement/versions.js +1 -1
- package/dist/core/functionGroup/create.js +1 -1
- package/dist/core/functionInclude/versions.js +1 -1
- package/dist/core/functionModule/versions.js +1 -1
- package/dist/core/scalarFunction/versions.d.ts +1 -1
- package/dist/core/scalarFunction/versions.js +1 -1
- package/dist/core/scalarFunctionImplementation/versions.d.ts +1 -1
- package/dist/core/scalarFunctionImplementation/versions.js +1 -1
- package/dist/core/service/AdtService.js +2 -2
- package/dist/core/service/lock.d.ts +1 -1
- package/dist/core/service/lock.js +1 -1
- package/dist/core/service/types.d.ts +2 -2
- package/dist/core/serviceDefinition/versions.d.ts +1 -1
- package/dist/core/serviceDefinition/versions.js +1 -1
- package/dist/core/structure/versions.d.ts +1 -1
- package/dist/core/structure/versions.js +1 -1
- package/dist/core/tabletype/versions.d.ts +1 -1
- package/dist/core/tabletype/versions.js +1 -1
- package/dist/core/transformation/versions.d.ts +1 -1
- package/dist/core/transformation/versions.js +1 -1
- package/dist/core/transport/AdtRequest.d.ts +2 -2
- package/dist/core/transport/AdtRequest.js +2 -2
- package/dist/core/transport/objects.d.ts +1 -1
- package/dist/core/transport/objects.js +1 -1
- package/dist/endpoints/objects.d.ts +5 -0
- package/dist/endpoints/objects.d.ts.map +1 -1
- package/dist/endpoints/objects.js +6 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/runtime/debugger/abap.d.ts.map +1 -1
- package/dist/runtime/debugger/abap.js +28 -40
- package/dist/runtime/debugger/batchPayload.d.ts +2 -16
- package/dist/runtime/debugger/batchPayload.d.ts.map +1 -1
- package/dist/runtime/debugger/batchPayload.js +5 -28
- package/dist/runtime/dumps/read.d.ts +1 -1
- package/dist/runtime/dumps/read.js +1 -1
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +3 -0
- package/docs/usage/BATCH.md +160 -0
- package/docs/usage/CLIENT_API_REFERENCE.md +6 -6
- package/docs/usage/ERRATA.md +1 -1
- package/docs/usage/STATEFUL_SESSION_GUIDE.md +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -450,7 +450,7 @@ await utils.readObjectSource('view' satisfies AdtSourceObjectType, 'ZOK_I_CDS_TE
|
|
|
450
450
|
The `withLongPolling` parameter asks ADT to hold a read until the object is
|
|
451
451
|
available, instead of answering from whatever is there right now:
|
|
452
452
|
|
|
453
|
-
> **Measured caveat — do not rely on it for readiness.** On
|
|
453
|
+
> **Measured caveat — do not rely on it for readiness.** On the ABAP Cloud system the
|
|
454
454
|
> parameter changes nothing on an object read. Reading a message class straight
|
|
455
455
|
> after creating it returned `404` in ~650ms **with** the flag — the same
|
|
456
456
|
> latency as without it — and the server did not hold the request. The system's
|
|
@@ -1115,3 +1115,31 @@ users can replace it with their own build.
|
|
|
1115
1115
|
## Author
|
|
1116
1116
|
|
|
1117
1117
|
Oleksii Kyslytsia <oleksij.kyslytsja@gmail.com>
|
|
1118
|
+
|
|
1119
|
+
### Recording batches
|
|
1120
|
+
|
|
1121
|
+
`AdtBatchConnection` records calls from the existing clients. Start the member
|
|
1122
|
+
calls, execute the queue, then await their results. Group creates before locks,
|
|
1123
|
+
then queue writes with the returned lock handles before unlocks;
|
|
1124
|
+
activate all objects with one plain `activateObjectsGroup` call. At a debugger
|
|
1125
|
+
stop, record step + stack + variables on the attached stateful connection.
|
|
1126
|
+
See [domain and debugger examples](docs/usage/BATCH.md).
|
|
1127
|
+
|
|
1128
|
+
`execute()` also sends follow-up envelopes for requests recorded during settlement,
|
|
1129
|
+
including automatic 406/415 negotiation retries (up to 100 envelopes, then all
|
|
1130
|
+
remaining requests reject). Explicit session switches apply to the next recording;
|
|
1131
|
+
LOCK/UNLOCK run in stateful envelopes; other ordinary members run stateless.
|
|
1132
|
+
Consecutive LOCKs, consecutive UNLOCKs, and consecutive stateless requests each
|
|
1133
|
+
form separate envelopes sent in queue order.
|
|
1134
|
+
Execution returns the last outer response. An envelope failure stops sending and
|
|
1135
|
+
rejects unsent requests with a clear library error; inner HTTP failures remain
|
|
1136
|
+
per-part failures. The wrapped session mode is restored after execution.
|
|
1137
|
+
|
|
1138
|
+
```typescript
|
|
1139
|
+
import { AdtBatchConnection, AdtClient } from '@mcp-abap-adt/adt-clients';
|
|
1140
|
+
|
|
1141
|
+
const batch = new AdtBatchConnection(connection, { endpoint: 'communication' });
|
|
1142
|
+
const pending = new AdtClient(batch).getDomain().readMetadata({ domainName: 'Z_DOMAIN' });
|
|
1143
|
+
await batch.execute();
|
|
1144
|
+
const answer = await pending;
|
|
1145
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { IAbapConnection, IAbapRequestOptions, IAdtWireResponse } from '@mcp-abap-adt/interfaces-adt-connection';
|
|
2
|
+
import type { ILogger } from '@mcp-abap-adt/interfaces-utils';
|
|
3
|
+
type Session = 'stateful' | 'stateless';
|
|
4
|
+
/** Records member requests; execute also drains requests queued during settlement. */
|
|
5
|
+
export declare class AdtBatchConnection implements IAbapConnection {
|
|
6
|
+
private readonly connection;
|
|
7
|
+
private readonly options;
|
|
8
|
+
private queue;
|
|
9
|
+
private session;
|
|
10
|
+
private readonly defaultSession;
|
|
11
|
+
private settlementFailure?;
|
|
12
|
+
private executing;
|
|
13
|
+
constructor(connection: IAbapConnection, options: {
|
|
14
|
+
endpoint: 'communication' | 'debugger';
|
|
15
|
+
logger?: ILogger;
|
|
16
|
+
});
|
|
17
|
+
private wrappedSession;
|
|
18
|
+
connect(): Promise<void>;
|
|
19
|
+
getBaseUrl(): Promise<string>;
|
|
20
|
+
getSessionId(): string | null;
|
|
21
|
+
setSessionType(type: Session): void;
|
|
22
|
+
getSessionType(): Session;
|
|
23
|
+
makeAdtRequest<T = unknown, D = unknown>(request: IAbapRequestOptions): Promise<IAdtWireResponse<T, D>>;
|
|
24
|
+
private envelopeKey;
|
|
25
|
+
/** Empty queues return a synthetic 204, data '', without network traffic. */
|
|
26
|
+
execute(): Promise<IAdtWireResponse>;
|
|
27
|
+
}
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=AdtBatchConnection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AdtBatchConnection.d.ts","sourceRoot":"","sources":["../../src/batch/AdtBatchConnection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,mBAAmB,EACnB,gBAAgB,EACjB,MAAM,yCAAyC,CAAC;AACjD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAI9D,KAAK,OAAO,GAAG,UAAU,GAAG,WAAW,CAAC;AAQxC,sFAAsF;AACtF,qBAAa,kBAAmB,YAAW,eAAe;IAOtD,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,OAAO;IAP1B,OAAO,CAAC,KAAK,CAAiB;IAC9B,OAAO,CAAC,OAAO,CAAU;IACzB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAU;IACzC,OAAO,CAAC,iBAAiB,CAAC,CAAqB;IAC/C,OAAO,CAAC,SAAS,CAAS;gBAEP,UAAU,EAAE,eAAe,EAC3B,OAAO,EAAE;QACxB,QAAQ,EAAE,eAAe,GAAG,UAAU,CAAC;QACvC,MAAM,CAAC,EAAE,OAAO,CAAC;KAClB;IAQH,OAAO,CAAC,cAAc;IAWtB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAGxB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC;IAG7B,YAAY,IAAI,MAAM,GAAG,IAAI;IAG7B,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI;IAGnC,cAAc,IAAI,OAAO;IAGzB,cAAc,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO,EACrC,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAuBlC,OAAO,CAAC,WAAW;IAkBnB,6EAA6E;IACvE,OAAO,IAAI,OAAO,CAAC,gBAAgB,CAAC;CAuF3C"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AdtBatchConnection = void 0;
|
|
4
|
+
const objects_1 = require("../endpoints/objects");
|
|
5
|
+
const wire_1 = require("./wire");
|
|
6
|
+
/** Records member requests; execute also drains requests queued during settlement. */
|
|
7
|
+
class AdtBatchConnection {
|
|
8
|
+
connection;
|
|
9
|
+
options;
|
|
10
|
+
queue = [];
|
|
11
|
+
session;
|
|
12
|
+
defaultSession;
|
|
13
|
+
settlementFailure;
|
|
14
|
+
executing = false;
|
|
15
|
+
constructor(connection, options) {
|
|
16
|
+
this.connection = connection;
|
|
17
|
+
this.options = options;
|
|
18
|
+
// Debugger requests use the attached stateful session; communication
|
|
19
|
+
// members opt into stateful mode only for their own LOCK or UNLOCK.
|
|
20
|
+
this.defaultSession =
|
|
21
|
+
options.endpoint === 'debugger' ? 'stateful' : 'stateless';
|
|
22
|
+
this.session = this.defaultSession;
|
|
23
|
+
}
|
|
24
|
+
wrappedSession() {
|
|
25
|
+
const connection = this.connection;
|
|
26
|
+
return (connection.getSessionType?.() ??
|
|
27
|
+
connection.getSessionMode?.() ??
|
|
28
|
+
'stateless');
|
|
29
|
+
}
|
|
30
|
+
connect() {
|
|
31
|
+
return this.connection.connect();
|
|
32
|
+
}
|
|
33
|
+
getBaseUrl() {
|
|
34
|
+
return this.connection.getBaseUrl();
|
|
35
|
+
}
|
|
36
|
+
getSessionId() {
|
|
37
|
+
return this.connection.getSessionId();
|
|
38
|
+
}
|
|
39
|
+
setSessionType(type) {
|
|
40
|
+
this.session = type;
|
|
41
|
+
}
|
|
42
|
+
getSessionType() {
|
|
43
|
+
return this.session;
|
|
44
|
+
}
|
|
45
|
+
makeAdtRequest(request) {
|
|
46
|
+
if (this.settlementFailure)
|
|
47
|
+
return Promise.reject(this.settlementFailure.error);
|
|
48
|
+
const headers = { ...request.headers };
|
|
49
|
+
const stateful = this.session === 'stateful' ||
|
|
50
|
+
Object.entries(headers).some(([key, value]) => key.toLowerCase() === 'x-sap-adt-sessiontype' && value === 'stateful');
|
|
51
|
+
// Members restore their mode only after the deferred answer. Scope an
|
|
52
|
+
// explicit switch to this recording so neighbouring members cannot inherit it.
|
|
53
|
+
this.session = this.defaultSession;
|
|
54
|
+
if (stateful)
|
|
55
|
+
headers['X-sap-adt-sessiontype'] = 'stateful';
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
this.queue.push({
|
|
58
|
+
request: { ...request, headers },
|
|
59
|
+
stateful,
|
|
60
|
+
resolve,
|
|
61
|
+
reject,
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
envelopeKey(part) {
|
|
66
|
+
if (!part.stateful)
|
|
67
|
+
return 'stateless';
|
|
68
|
+
if (this.options.endpoint === 'debugger')
|
|
69
|
+
return 'stateful';
|
|
70
|
+
const action = new URL(part.request.url, 'https://batch.invalid').searchParams.get('_action') ??
|
|
71
|
+
new URLSearchParams(part.request.params).get('_action');
|
|
72
|
+
if (action === 'LOCK')
|
|
73
|
+
return 'lock';
|
|
74
|
+
if (action === 'UNLOCK')
|
|
75
|
+
return 'unlock';
|
|
76
|
+
// Communication members currently use stateful mode only for LOCK/UNLOCK.
|
|
77
|
+
// Keep any future other stateful requests in their own consecutive group.
|
|
78
|
+
return 'stateful';
|
|
79
|
+
}
|
|
80
|
+
/** Empty queues return a synthetic 204, data '', without network traffic. */
|
|
81
|
+
async execute() {
|
|
82
|
+
if (this.executing)
|
|
83
|
+
throw new Error('An ADT batch execute is already in progress');
|
|
84
|
+
const pending = this.queue;
|
|
85
|
+
this.queue = [];
|
|
86
|
+
if (!pending.length)
|
|
87
|
+
return { status: 204, statusText: 'Empty batch', headers: {}, data: '' };
|
|
88
|
+
this.executing = true;
|
|
89
|
+
const previous = this.wrappedSession();
|
|
90
|
+
let active = [];
|
|
91
|
+
try {
|
|
92
|
+
let response;
|
|
93
|
+
for (let envelope = 0;; envelope++) {
|
|
94
|
+
if (envelope === 100)
|
|
95
|
+
throw new Error('ADT batch exceeded 100 settlement envelopes');
|
|
96
|
+
const key = this.envelopeKey(pending[0]);
|
|
97
|
+
const nextMode = pending.findIndex((part) => this.envelopeKey(part) !== key);
|
|
98
|
+
active = pending.splice(0, nextMode < 0 ? pending.length : nextMode);
|
|
99
|
+
const payload = (0, wire_1.buildBatchPayload)(active.map((part) => (0, wire_1.innerRequest)(part.request)));
|
|
100
|
+
this.connection.setSessionType(active[0].stateful ? 'stateful' : 'stateless');
|
|
101
|
+
this.options.logger?.debug('Executing ADT batch', {
|
|
102
|
+
endpoint: this.options.endpoint,
|
|
103
|
+
parts: active.length,
|
|
104
|
+
});
|
|
105
|
+
response = await this.connection.makeAdtRequest({
|
|
106
|
+
url: objects_1.BATCH[this.options.endpoint],
|
|
107
|
+
method: 'POST',
|
|
108
|
+
timeout: 0,
|
|
109
|
+
data: payload.body,
|
|
110
|
+
headers: {
|
|
111
|
+
'Content-Type': `multipart/mixed; boundary=${payload.boundary}`,
|
|
112
|
+
Accept: 'multipart/mixed',
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
const parts = (0, wire_1.parseBatchResponse)(response);
|
|
116
|
+
for (const [index, part] of active.entries()) {
|
|
117
|
+
const answer = parts[index];
|
|
118
|
+
if (!answer)
|
|
119
|
+
part.reject(new Error(`ADT batch response part count mismatch: expected ${active.length}, received ${parts.length}; missing part ${index + 1}`));
|
|
120
|
+
else if (answer.status >= 200 && answer.status < 300)
|
|
121
|
+
part.resolve(answer);
|
|
122
|
+
else
|
|
123
|
+
part.reject(Object.assign(new Error(`Request failed with status code ${answer.status}`), { response: answer }));
|
|
124
|
+
}
|
|
125
|
+
if (parts.length !== active.length)
|
|
126
|
+
throw new Error(`ADT batch response part count mismatch: expected ${active.length}, received ${parts.length}`);
|
|
127
|
+
active = [];
|
|
128
|
+
// Let negotiation and member continuations record their follow-up calls.
|
|
129
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
130
|
+
pending.push(...this.queue.splice(0));
|
|
131
|
+
if (!pending.length)
|
|
132
|
+
return response;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
const unsentError = new Error('ADT batch request not sent because an earlier batch envelope failed', { cause: error });
|
|
137
|
+
this.settlementFailure = { error: unsentError };
|
|
138
|
+
for (const part of active)
|
|
139
|
+
part.reject(error);
|
|
140
|
+
for (const part of pending)
|
|
141
|
+
part.reject(unsentError);
|
|
142
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
143
|
+
for (const part of this.queue.splice(0))
|
|
144
|
+
part.reject(unsentError);
|
|
145
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
this.connection.setSessionType(previous);
|
|
150
|
+
this.executing = false;
|
|
151
|
+
this.settlementFailure = undefined;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
exports.AdtBatchConnection = AdtBatchConnection;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/batch/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AdtBatchConnection = void 0;
|
|
4
|
+
var AdtBatchConnection_1 = require("./AdtBatchConnection");
|
|
5
|
+
Object.defineProperty(exports, "AdtBatchConnection", { enumerable: true, get: function () { return AdtBatchConnection_1.AdtBatchConnection; } });
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { IAbapRequestOptions, IAdtWireResponse } from '@mcp-abap-adt/interfaces-adt-connection';
|
|
2
|
+
export declare const createBatchBoundary: () => string;
|
|
3
|
+
export declare const createRequestId: () => string;
|
|
4
|
+
export declare function buildBatchPayload(requests: readonly string[], boundary?: string): {
|
|
5
|
+
boundary: string;
|
|
6
|
+
body: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function innerRequest(request: IAbapRequestOptions): string;
|
|
9
|
+
export declare function parseBatchResponse(response: IAdtWireResponse): IAdtWireResponse[];
|
|
10
|
+
//# sourceMappingURL=wire.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wire.d.ts","sourceRoot":"","sources":["../../src/batch/wire.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,mBAAmB,EACnB,gBAAgB,EACjB,MAAM,yCAAyC,CAAC;AAEjD,eAAO,MAAM,mBAAmB,QAAO,MAAiC,CAAC;AACzE,eAAO,MAAM,eAAe,QAAO,MAAwC,CAAC;AAE5E,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,SAAS,MAAM,EAAE,EAC3B,QAAQ,SAAwB,GAC/B;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAWpC;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,CAqBjE;AAED,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,gBAAgB,GACzB,gBAAgB,EAAE,CAyDpB"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createRequestId = exports.createBatchBoundary = void 0;
|
|
4
|
+
exports.buildBatchPayload = buildBatchPayload;
|
|
5
|
+
exports.innerRequest = innerRequest;
|
|
6
|
+
exports.parseBatchResponse = parseBatchResponse;
|
|
7
|
+
const node_crypto_1 = require("node:crypto");
|
|
8
|
+
const createBatchBoundary = () => `batch_${(0, node_crypto_1.randomUUID)()}`;
|
|
9
|
+
exports.createBatchBoundary = createBatchBoundary;
|
|
10
|
+
const createRequestId = () => (0, node_crypto_1.randomUUID)().replace(/-/g, '');
|
|
11
|
+
exports.createRequestId = createRequestId;
|
|
12
|
+
function buildBatchPayload(requests, boundary = (0, exports.createBatchBoundary)()) {
|
|
13
|
+
return {
|
|
14
|
+
boundary,
|
|
15
|
+
body: requests
|
|
16
|
+
.map((request) => `--${boundary}\r\nContent-Type: application/http\r\ncontent-transfer-encoding: binary\r\n\r\n${request}`)
|
|
17
|
+
.join('') + `\r\n--${boundary}--\r\n`,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function innerRequest(request) {
|
|
21
|
+
const headers = { ...request.headers };
|
|
22
|
+
// `data: null` is how a member says "no body" (lock, unlock): it must not
|
|
23
|
+
// become the four characters "null". Sent that way, every PUT under those
|
|
24
|
+
// locks was refused 423 "invalid lock handle" (on an on-premise system, 2026-10-16).
|
|
25
|
+
const hasBody = request.data !== undefined && request.data !== null;
|
|
26
|
+
const body = hasBody ? String(request.data) : '';
|
|
27
|
+
if (hasBody) {
|
|
28
|
+
for (const key of Object.keys(headers))
|
|
29
|
+
if (key.toLowerCase() === 'content-length')
|
|
30
|
+
delete headers[key];
|
|
31
|
+
headers['Content-Length'] = String(Buffer.byteLength(body));
|
|
32
|
+
}
|
|
33
|
+
const params = request.params
|
|
34
|
+
? new URLSearchParams(request.params).toString()
|
|
35
|
+
: '';
|
|
36
|
+
const url = request.url +
|
|
37
|
+
(params ? `${request.url.includes('?') ? '&' : '?'}${params}` : '');
|
|
38
|
+
return `${request.method} ${url} HTTP/1.1\r\n${Object.entries(headers)
|
|
39
|
+
.map(([key, value]) => `${key}: ${value}\r\n`)
|
|
40
|
+
.join('')}\r\n${body}\r\n`;
|
|
41
|
+
}
|
|
42
|
+
function parseBatchResponse(response) {
|
|
43
|
+
const contentType = Object.entries(response.headers).find(([key]) => key.toLowerCase() === 'content-type')?.[1];
|
|
44
|
+
const boundary = /boundary=(?:"([^"]+)"|([^;\s]+))/i.exec(String(contentType));
|
|
45
|
+
if (!boundary)
|
|
46
|
+
throw new Error('Invalid ADT batch response: missing multipart boundary');
|
|
47
|
+
const escaped = (boundary[1] ?? boundary[2]).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
48
|
+
// Only complete delimiter lines count; boundary-like body text is payload.
|
|
49
|
+
const delimiter = new RegExp(`(?:^|\\r?\\n)--${escaped}(--)?[ \t]*(?:\\r?\\n|$)`, 'g');
|
|
50
|
+
const body = String(response.data);
|
|
51
|
+
const chunks = [];
|
|
52
|
+
let start;
|
|
53
|
+
for (const match of body.matchAll(delimiter)) {
|
|
54
|
+
if (start !== undefined)
|
|
55
|
+
chunks.push(body.slice(start, match.index));
|
|
56
|
+
if (match[1])
|
|
57
|
+
break;
|
|
58
|
+
start = match.index + match[0].length;
|
|
59
|
+
}
|
|
60
|
+
return chunks.map((part) => {
|
|
61
|
+
const match = /(?:^|\r?\n)HTTP\/1\.\d\s+(\d{3})\s*([^\r\n]*)\r?\n/.exec(part);
|
|
62
|
+
if (!match || match.index === undefined)
|
|
63
|
+
throw new Error('Invalid ADT batch response: missing inner HTTP status');
|
|
64
|
+
const rest = part.slice(match.index + match[0].length);
|
|
65
|
+
// A bodyless answer (an UNLOCK's 200) ends right after its headers: the
|
|
66
|
+
// empty line's last CRLF belongs to the next delimiter (RFC 2046), so the
|
|
67
|
+
// part holds one line break, not two. Headers then run to the end.
|
|
68
|
+
// Immediately after the status line, one blank line means no headers.
|
|
69
|
+
const found = /^(?:\r?\n)|\r?\n\r?\n/.exec(rest);
|
|
70
|
+
const split = found && found.index !== undefined
|
|
71
|
+
? { index: found.index, length: found[0].length }
|
|
72
|
+
: { index: rest.length, length: 0 };
|
|
73
|
+
const headers = {};
|
|
74
|
+
for (const line of rest.slice(0, split.index).split(/\r?\n/)) {
|
|
75
|
+
const colon = line.indexOf(':');
|
|
76
|
+
if (colon > 0)
|
|
77
|
+
headers[line.slice(0, colon).toLowerCase()] = line
|
|
78
|
+
.slice(colon + 1)
|
|
79
|
+
.trim();
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
status: Number(match[1]),
|
|
83
|
+
statusText: match[2].trim(),
|
|
84
|
+
headers,
|
|
85
|
+
data: rest.slice(split.index + split.length),
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -7,7 +7,7 @@ import type { IAccessControlConfig } from './types';
|
|
|
7
7
|
* system without the resource answers 404 or 406, which comes back as the
|
|
8
8
|
* transport's failure; until 23.0.0 it was thrown as a library error.
|
|
9
9
|
*
|
|
10
|
-
* Candidate URI — probe-verify on
|
|
10
|
+
* Candidate URI — probe-verify on the ABAP Cloud system.
|
|
11
11
|
*/
|
|
12
12
|
export declare function getAccessControlVersions(connection: IAbapConnection, config: Partial<IAccessControlConfig>): Promise<IAdtWireResponse>;
|
|
13
13
|
/** The source of one version, by the `contentUri` its entry carried. */
|
|
@@ -11,7 +11,7 @@ const ACCEPT_VERSION_FEED = 'application/atom+xml;type=feed';
|
|
|
11
11
|
* system without the resource answers 404 or 406, which comes back as the
|
|
12
12
|
* transport's failure; until 23.0.0 it was thrown as a library error.
|
|
13
13
|
*
|
|
14
|
-
* Candidate URI — probe-verify on
|
|
14
|
+
* Candidate URI — probe-verify on the ABAP Cloud system.
|
|
15
15
|
*/
|
|
16
16
|
async function getAccessControlVersions(connection, config) {
|
|
17
17
|
return connection.makeAdtRequest({
|
|
@@ -7,7 +7,7 @@ import type { IAppendStructureConfig } from './types';
|
|
|
7
7
|
* system without the resource answers 404 or 406, which comes back as the
|
|
8
8
|
* transport's failure; until 23.0.0 it was thrown as a library error.
|
|
9
9
|
*
|
|
10
|
-
* Candidate URI — probe-verify on
|
|
10
|
+
* Candidate URI — probe-verify on the ABAP Cloud system.
|
|
11
11
|
*/
|
|
12
12
|
export declare function getAppendStructureVersions(connection: IAbapConnection, config: Partial<IAppendStructureConfig>): Promise<IAdtWireResponse>;
|
|
13
13
|
/** The source of one version, by the `contentUri` its entry carried. */
|
|
@@ -11,7 +11,7 @@ const ACCEPT_VERSION_FEED = 'application/atom+xml;type=feed';
|
|
|
11
11
|
* system without the resource answers 404 or 406, which comes back as the
|
|
12
12
|
* transport's failure; until 23.0.0 it was thrown as a library error.
|
|
13
13
|
*
|
|
14
|
-
* Candidate URI — probe-verify on
|
|
14
|
+
* Candidate URI — probe-verify on the ABAP Cloud system.
|
|
15
15
|
*/
|
|
16
16
|
async function getAppendStructureVersions(connection, config) {
|
|
17
17
|
return connection.makeAdtRequest({
|
|
@@ -7,7 +7,7 @@ import type { IBehaviorDefinitionConfig } from './types';
|
|
|
7
7
|
* system without the resource answers 404 or 406, which comes back as the
|
|
8
8
|
* transport's failure; until 23.0.0 it was thrown as a library error.
|
|
9
9
|
*
|
|
10
|
-
* Candidate URI — probe-verify on
|
|
10
|
+
* Candidate URI — probe-verify on the ABAP Cloud system.
|
|
11
11
|
*/
|
|
12
12
|
export declare function getBehaviorDefinitionVersions(connection: IAbapConnection, config: Partial<IBehaviorDefinitionConfig>): Promise<IAdtWireResponse>;
|
|
13
13
|
/** The source of one version, by the `contentUri` its entry carried. */
|
|
@@ -11,7 +11,7 @@ const ACCEPT_VERSION_FEED = 'application/atom+xml;type=feed';
|
|
|
11
11
|
* system without the resource answers 404 or 406, which comes back as the
|
|
12
12
|
* transport's failure; until 23.0.0 it was thrown as a library error.
|
|
13
13
|
*
|
|
14
|
-
* Candidate URI — probe-verify on
|
|
14
|
+
* Candidate URI — probe-verify on the ABAP Cloud system.
|
|
15
15
|
*/
|
|
16
16
|
async function getBehaviorDefinitionVersions(connection, config) {
|
|
17
17
|
return connection.makeAdtRequest({
|
|
@@ -7,7 +7,7 @@ import type { IBehaviorImplementationConfig } from './types';
|
|
|
7
7
|
* system without the resource answers 404 or 406, which comes back as the
|
|
8
8
|
* transport's failure; until 23.0.0 it was thrown as a library error.
|
|
9
9
|
*
|
|
10
|
-
* Candidate URI — probe-verify on
|
|
10
|
+
* Candidate URI — probe-verify on the ABAP Cloud system.
|
|
11
11
|
*/
|
|
12
12
|
export declare function getBehaviorImplementationVersions(connection: IAbapConnection, config: Partial<IBehaviorImplementationConfig>): Promise<IAdtWireResponse>;
|
|
13
13
|
/** The source of one version, by the `contentUri` its entry carried. */
|
|
@@ -11,7 +11,7 @@ const ACCEPT_VERSION_FEED = 'application/atom+xml;type=feed';
|
|
|
11
11
|
* system without the resource answers 404 or 406, which comes back as the
|
|
12
12
|
* transport's failure; until 23.0.0 it was thrown as a library error.
|
|
13
13
|
*
|
|
14
|
-
* Candidate URI — probe-verify on
|
|
14
|
+
* Candidate URI — probe-verify on the ABAP Cloud system.
|
|
15
15
|
*/
|
|
16
16
|
async function getBehaviorImplementationVersions(connection, config) {
|
|
17
17
|
return connection.makeAdtRequest({
|
|
@@ -12,7 +12,7 @@ const ACCEPT_VERSION_FEED = 'application/atom+xml;type=feed';
|
|
|
12
12
|
* system without the resource answers 404 or 406, which comes back as the
|
|
13
13
|
* transport's failure; until 23.0.0 it was thrown as a library error.
|
|
14
14
|
*/
|
|
15
|
-
// candidate URI — probe-verify on
|
|
15
|
+
// candidate URI — probe-verify on the ABAP Cloud system
|
|
16
16
|
async function getEnhancementVersions(connection, config) {
|
|
17
17
|
return connection.makeAdtRequest({
|
|
18
18
|
url: `${(0, types_1.getEnhancementUri)(config.enhancementType, config.enhancementName)}/source/main/versions`,
|
|
@@ -20,7 +20,7 @@ async function create(connection, params, logger, contentTypes) {
|
|
|
20
20
|
if (finalResponsible && finalResponsible.trim() === '') {
|
|
21
21
|
finalResponsible = undefined;
|
|
22
22
|
}
|
|
23
|
-
// Don't escape masterSystem and responsible - they are identifiers (like "
|
|
23
|
+
// Don't escape masterSystem and responsible - they are identifiers (like "SID", "DEVELOPER")
|
|
24
24
|
// Escaping them may cause SAP to fail parsing and trigger Kerberos errors
|
|
25
25
|
const masterSystemAttr = finalMasterSystem
|
|
26
26
|
? ` adtcore:masterSystem="${finalMasterSystem}"`
|
|
@@ -11,7 +11,7 @@ const ACCEPT_VERSION_FEED = 'application/atom+xml;type=feed';
|
|
|
11
11
|
* system without the resource answers 404 or 406, which comes back as the
|
|
12
12
|
* transport's failure; until 23.0.0 it was thrown as a library error.
|
|
13
13
|
*/
|
|
14
|
-
// candidate URI — probe-verify on
|
|
14
|
+
// candidate URI — probe-verify on the ABAP Cloud system
|
|
15
15
|
async function getFunctionIncludeVersions(connection, config) {
|
|
16
16
|
return connection.makeAdtRequest({
|
|
17
17
|
url: `${(0, objects_1.versionsUri)((0, objects_1.sourceUri)(objects_1.FUNCTION_INCLUDE.uri(config.functionGroupName, config.includeName.toUpperCase())))}`,
|
|
@@ -11,7 +11,7 @@ const ACCEPT_VERSION_FEED = 'application/atom+xml;type=feed';
|
|
|
11
11
|
* system without the resource answers 404 or 406, which comes back as the
|
|
12
12
|
* transport's failure; until 23.0.0 it was thrown as a library error.
|
|
13
13
|
*/
|
|
14
|
-
// candidate URI — probe-verify on
|
|
14
|
+
// candidate URI — probe-verify on the ABAP Cloud system
|
|
15
15
|
async function getFunctionModuleVersions(connection, config) {
|
|
16
16
|
return connection.makeAdtRequest({
|
|
17
17
|
url: `${(0, objects_1.versionsUri)((0, objects_1.sourceUri)(objects_1.FUNCTION_MODULE.uri(config.functionGroupName, config.functionModuleName)))}`,
|
|
@@ -2,7 +2,7 @@ import type { IAbapConnection, IAdtWireResponse } from '@mcp-abap-adt/interfaces
|
|
|
2
2
|
import type { IScalarFunctionConfig } from './types';
|
|
3
3
|
/**
|
|
4
4
|
* The version history — the Atom feed, as it arrived (candidate URI,
|
|
5
|
-
* probe-verify on
|
|
5
|
+
* probe-verify on the ABAP Cloud system).
|
|
6
6
|
*
|
|
7
7
|
* `objectVersions` in @mcp-abap-adt/adt-strategies reads it into entries. A
|
|
8
8
|
* system without the resource answers 404 or 406, which comes back as the
|
|
@@ -6,7 +6,7 @@ const objects_1 = require("../../endpoints/objects");
|
|
|
6
6
|
const ACCEPT_VERSION_FEED = 'application/atom+xml;type=feed';
|
|
7
7
|
/**
|
|
8
8
|
* The version history — the Atom feed, as it arrived (candidate URI,
|
|
9
|
-
* probe-verify on
|
|
9
|
+
* probe-verify on the ABAP Cloud system).
|
|
10
10
|
*
|
|
11
11
|
* `objectVersions` in @mcp-abap-adt/adt-strategies reads it into entries. A
|
|
12
12
|
* system without the resource answers 404 or 406, which comes back as the
|
|
@@ -2,7 +2,7 @@ import type { IAbapConnection, IAdtWireResponse } from '@mcp-abap-adt/interfaces
|
|
|
2
2
|
import type { IScalarFunctionImplementationConfig } from './types';
|
|
3
3
|
/**
|
|
4
4
|
* The version history — the Atom feed, as it arrived (candidate URI,
|
|
5
|
-
* probe-verify on
|
|
5
|
+
* probe-verify on the ABAP Cloud system).
|
|
6
6
|
*
|
|
7
7
|
* `objectVersions` in @mcp-abap-adt/adt-strategies reads it into entries. A
|
|
8
8
|
* system without the resource answers 404 or 406, which comes back as the
|
|
@@ -6,7 +6,7 @@ const objects_1 = require("../../endpoints/objects");
|
|
|
6
6
|
const ACCEPT_VERSION_FEED = 'application/atom+xml;type=feed';
|
|
7
7
|
/**
|
|
8
8
|
* The version history — the Atom feed, as it arrived (candidate URI,
|
|
9
|
-
* probe-verify on
|
|
9
|
+
* probe-verify on the ABAP Cloud system).
|
|
10
10
|
*
|
|
11
11
|
* `objectVersions` in @mcp-abap-adt/adt-strategies reads it into entries. A
|
|
12
12
|
* system without the resource answers 404 or 406, which comes back as the
|
|
@@ -74,7 +74,7 @@ class AdtServiceBinding {
|
|
|
74
74
|
}
|
|
75
75
|
async publishByServiceType(serviceType, bindingName,
|
|
76
76
|
// **The caller's, when they give one — else none.** A publication job is
|
|
77
|
-
// the slowest thing this library asks for — measured at ~135s on
|
|
77
|
+
// the slowest thing this library asks for — measured at ~135s on the ABAP Cloud system,
|
|
78
78
|
// and an unpublish once not settled after eleven minutes. The library sets
|
|
79
79
|
// no deadline of its own; `IAdtOperationOptions.timeout` is how a caller
|
|
80
80
|
// asks for one.
|
|
@@ -125,7 +125,7 @@ class AdtServiceBinding {
|
|
|
125
125
|
}
|
|
126
126
|
async unpublishByServiceType(serviceType, bindingName,
|
|
127
127
|
// **The caller's, when they give one — else none.** A publication job is
|
|
128
|
-
// the slowest thing this library asks for — measured at ~135s on
|
|
128
|
+
// the slowest thing this library asks for — measured at ~135s on the ABAP Cloud system,
|
|
129
129
|
// and an unpublish once not settled after eleven minutes. The library sets
|
|
130
130
|
// no deadline of its own; `IAdtOperationOptions.timeout` is how a caller
|
|
131
131
|
// asks for one.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Locking a service binding — which is what publishing one is.
|
|
3
3
|
*
|
|
4
4
|
* A binding is not edited the way a class is: it is created, and then published
|
|
5
|
-
* and unpublished. Measured from Eclipse (ADT 3.60.3) on the
|
|
5
|
+
* and unpublished. Measured from Eclipse (ADT 3.60.3) on the ABAP Cloud system,
|
|
6
6
|
* 2026-09-05, the lock is what those two take:
|
|
7
7
|
*
|
|
8
8
|
* ```
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Locking a service binding — which is what publishing one is.
|
|
4
4
|
*
|
|
5
5
|
* A binding is not edited the way a class is: it is created, and then published
|
|
6
|
-
* and unpublished. Measured from Eclipse (ADT 3.60.3) on the
|
|
6
|
+
* and unpublished. Measured from Eclipse (ADT 3.60.3) on the ABAP Cloud system,
|
|
7
7
|
* 2026-09-05, the lock is what those two take:
|
|
8
8
|
*
|
|
9
9
|
* ```
|
|
@@ -105,7 +105,7 @@ export type IServiceBindingPublicationConfig = Partial<ISBC> & {
|
|
|
105
105
|
/**
|
|
106
106
|
* What a publication job needs, which is not the same for the two protocols.
|
|
107
107
|
*
|
|
108
|
-
* **V2 requires the service name and version; V4 does not.** Measured on
|
|
108
|
+
* **V2 requires the service name and version; V4 does not.** Measured on an ABAP Cloud system,
|
|
109
109
|
* 2026-09-29, on one binding per protocol with a known publication state and a
|
|
110
110
|
* single job each — the only difference being the query string:
|
|
111
111
|
*
|
|
@@ -141,7 +141,7 @@ interface IServiceBindingPublicationCommon {
|
|
|
141
141
|
* How long to wait for the publication job, in milliseconds.
|
|
142
142
|
*
|
|
143
143
|
* A publication is the slowest request this library makes — ~135s measured on
|
|
144
|
-
*
|
|
144
|
+
* the ABAP Cloud system, and one unpublish still unsettled after eleven minutes. Absent, the
|
|
145
145
|
* request carries no deadline: it waits for SAP's answer.
|
|
146
146
|
*/
|
|
147
147
|
timeout?: number;
|
|
@@ -2,7 +2,7 @@ import type { IAbapConnection, IAdtWireResponse } from '@mcp-abap-adt/interfaces
|
|
|
2
2
|
import type { IServiceDefinitionConfig } from './types';
|
|
3
3
|
/**
|
|
4
4
|
* The version history — the Atom feed, as it arrived (candidate URI,
|
|
5
|
-
* probe-verify on
|
|
5
|
+
* probe-verify on the ABAP Cloud system).
|
|
6
6
|
*
|
|
7
7
|
* `objectVersions` in @mcp-abap-adt/adt-strategies reads it into entries. A
|
|
8
8
|
* system without the resource answers 404 or 406, which comes back as the
|
|
@@ -6,7 +6,7 @@ const objects_1 = require("../../endpoints/objects");
|
|
|
6
6
|
const ACCEPT_VERSION_FEED = 'application/atom+xml;type=feed';
|
|
7
7
|
/**
|
|
8
8
|
* The version history — the Atom feed, as it arrived (candidate URI,
|
|
9
|
-
* probe-verify on
|
|
9
|
+
* probe-verify on the ABAP Cloud system).
|
|
10
10
|
*
|
|
11
11
|
* `objectVersions` in @mcp-abap-adt/adt-strategies reads it into entries. A
|
|
12
12
|
* system without the resource answers 404 or 406, which comes back as the
|
|
@@ -2,7 +2,7 @@ import type { IAbapConnection, IAdtWireResponse } from '@mcp-abap-adt/interfaces
|
|
|
2
2
|
import type { IStructureConfig } from './types';
|
|
3
3
|
/**
|
|
4
4
|
* The version history — the Atom feed, as it arrived (candidate URI,
|
|
5
|
-
* probe-verify on
|
|
5
|
+
* probe-verify on the ABAP Cloud system).
|
|
6
6
|
*
|
|
7
7
|
* `objectVersions` in @mcp-abap-adt/adt-strategies reads it into entries. A
|
|
8
8
|
* system without the resource answers 404 or 406, which comes back as the
|
|
@@ -6,7 +6,7 @@ const objects_1 = require("../../endpoints/objects");
|
|
|
6
6
|
const ACCEPT_VERSION_FEED = 'application/atom+xml;type=feed';
|
|
7
7
|
/**
|
|
8
8
|
* The version history — the Atom feed, as it arrived (candidate URI,
|
|
9
|
-
* probe-verify on
|
|
9
|
+
* probe-verify on the ABAP Cloud system).
|
|
10
10
|
*
|
|
11
11
|
* `objectVersions` in @mcp-abap-adt/adt-strategies reads it into entries. A
|
|
12
12
|
* system without the resource answers 404 or 406, which comes back as the
|
|
@@ -2,7 +2,7 @@ import type { IAbapConnection, IAdtWireResponse } from '@mcp-abap-adt/interfaces
|
|
|
2
2
|
import type { ITableTypeConfig } from './types';
|
|
3
3
|
/**
|
|
4
4
|
* The version history — the Atom feed, as it arrived (candidate URI,
|
|
5
|
-
* probe-verify on
|
|
5
|
+
* probe-verify on the ABAP Cloud system).
|
|
6
6
|
*
|
|
7
7
|
* `objectVersions` in @mcp-abap-adt/adt-strategies reads it into entries. A
|
|
8
8
|
* system without the resource answers 404 or 406, which comes back as the
|
|
@@ -6,7 +6,7 @@ const objects_1 = require("../../endpoints/objects");
|
|
|
6
6
|
const ACCEPT_VERSION_FEED = 'application/atom+xml;type=feed';
|
|
7
7
|
/**
|
|
8
8
|
* The version history — the Atom feed, as it arrived (candidate URI,
|
|
9
|
-
* probe-verify on
|
|
9
|
+
* probe-verify on the ABAP Cloud system).
|
|
10
10
|
*
|
|
11
11
|
* `objectVersions` in @mcp-abap-adt/adt-strategies reads it into entries. A
|
|
12
12
|
* system without the resource answers 404 or 406, which comes back as the
|