arc-1 0.6.0 → 0.6.2
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 +25 -26
- package/dist/adt/btp.d.ts +19 -12
- package/dist/adt/btp.d.ts.map +1 -1
- package/dist/adt/btp.js +48 -35
- package/dist/adt/btp.js.map +1 -1
- package/dist/adt/client.d.ts +20 -2
- package/dist/adt/client.d.ts.map +1 -1
- package/dist/adt/client.js +65 -2
- package/dist/adt/client.js.map +1 -1
- package/dist/adt/codeintel.d.ts.map +1 -1
- package/dist/adt/codeintel.js +1 -10
- package/dist/adt/codeintel.js.map +1 -1
- package/dist/adt/config.d.ts +2 -0
- package/dist/adt/config.d.ts.map +1 -1
- package/dist/adt/config.js +2 -0
- package/dist/adt/config.js.map +1 -1
- package/dist/adt/crud.d.ts +7 -0
- package/dist/adt/crud.d.ts.map +1 -1
- package/dist/adt/crud.js +35 -3
- package/dist/adt/crud.js.map +1 -1
- package/dist/adt/ddic-xml.d.ts +78 -0
- package/dist/adt/ddic-xml.d.ts.map +1 -0
- package/dist/adt/ddic-xml.js +203 -0
- package/dist/adt/ddic-xml.js.map +1 -0
- package/dist/adt/devtools.d.ts +30 -7
- package/dist/adt/devtools.d.ts.map +1 -1
- package/dist/adt/devtools.js +215 -76
- package/dist/adt/devtools.js.map +1 -1
- package/dist/adt/diagnostics.d.ts.map +1 -1
- package/dist/adt/diagnostics.js +105 -184
- package/dist/adt/diagnostics.js.map +1 -1
- package/dist/adt/errors.d.ts +12 -0
- package/dist/adt/errors.d.ts.map +1 -1
- package/dist/adt/errors.js +42 -0
- package/dist/adt/errors.js.map +1 -1
- package/dist/adt/features.d.ts.map +1 -1
- package/dist/adt/features.js +23 -3
- package/dist/adt/features.js.map +1 -1
- package/dist/adt/flp.d.ts +43 -0
- package/dist/adt/flp.d.ts.map +1 -0
- package/dist/adt/flp.js +213 -0
- package/dist/adt/flp.js.map +1 -0
- package/dist/adt/http.d.ts +2 -0
- package/dist/adt/http.d.ts.map +1 -1
- package/dist/adt/http.js +162 -4
- package/dist/adt/http.js.map +1 -1
- package/dist/adt/transport.d.ts +48 -3
- package/dist/adt/transport.d.ts.map +1 -1
- package/dist/adt/transport.js +222 -32
- package/dist/adt/transport.js.map +1 -1
- package/dist/adt/types.d.ts +107 -0
- package/dist/adt/types.d.ts.map +1 -1
- package/dist/adt/ui5-repository.d.ts +19 -0
- package/dist/adt/ui5-repository.d.ts.map +1 -0
- package/dist/adt/ui5-repository.js +43 -0
- package/dist/adt/ui5-repository.js.map +1 -0
- package/dist/adt/xml-parser.d.ts +43 -1
- package/dist/adt/xml-parser.d.ts.map +1 -1
- package/dist/adt/xml-parser.js +208 -3
- package/dist/adt/xml-parser.js.map +1 -1
- package/dist/handlers/intent.d.ts +20 -6
- package/dist/handlers/intent.d.ts.map +1 -1
- package/dist/handlers/intent.js +1304 -104
- package/dist/handlers/intent.js.map +1 -1
- package/dist/handlers/schemas.d.ts +240 -0
- package/dist/handlers/schemas.d.ts.map +1 -1
- package/dist/handlers/schemas.js +201 -5
- package/dist/handlers/schemas.js.map +1 -1
- package/dist/handlers/tools.d.ts.map +1 -1
- package/dist/handlers/tools.js +339 -49
- package/dist/handlers/tools.js.map +1 -1
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +15 -3
- package/dist/server/config.js.map +1 -1
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +32 -3
- package/dist/server/http.js.map +1 -1
- package/dist/server/server.d.ts +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +38 -6
- package/dist/server/server.js.map +1 -1
- package/dist/server/types.d.ts +2 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +5 -3
- package/dist/server/types.js.map +1 -1
- package/dist/server/xsuaa.d.ts +13 -0
- package/dist/server/xsuaa.d.ts.map +1 -1
- package/dist/server/xsuaa.js +28 -2
- package/dist/server/xsuaa.js.map +1 -1
- package/package.json +19 -6
package/dist/handlers/intent.js
CHANGED
|
@@ -10,13 +10,16 @@
|
|
|
10
10
|
* leaked to the LLM — only user-friendly error messages.
|
|
11
11
|
*/
|
|
12
12
|
import { findDefinition, findReferences, findWhereUsed, getCompletion, } from '../adt/codeintel.js';
|
|
13
|
-
import { createObject, deleteObject, lockObject, safeUpdateSource, unlockObject } from '../adt/crud.js';
|
|
14
|
-
import {
|
|
13
|
+
import { createObject, deleteObject, lockObject, safeUpdateObject, safeUpdateSource, unlockObject, updateObject, } from '../adt/crud.js';
|
|
14
|
+
import { buildDataElementXml, buildDomainXml, buildMessageClassXml, buildPackageXml, buildServiceBindingXml, } from '../adt/ddic-xml.js';
|
|
15
|
+
import { activate, activateBatch, publishServiceBinding, runAtcCheck, runUnitTests, syntaxCheck, unpublishServiceBinding, } from '../adt/devtools.js';
|
|
15
16
|
import { getDump, getTraceDbAccesses, getTraceHitlist, getTraceStatements, listDumps, listTraces, } from '../adt/diagnostics.js';
|
|
16
17
|
import { AdtApiError, AdtNetworkError, AdtSafetyError, isNotFoundError } from '../adt/errors.js';
|
|
17
18
|
import { classifyTextSearchError, mapSapReleaseToAbaplintVersion, probeFeatures } from '../adt/features.js';
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
19
|
+
import { addTileToGroup, createCatalog, createGroup, createTile, deleteCatalog, listCatalogs, listGroups, listTiles, } from '../adt/flp.js';
|
|
20
|
+
import { checkOperation, checkPackage, isOperationAllowed, OperationType } from '../adt/safety.js';
|
|
21
|
+
import { createTransport, deleteTransport, getTransport, getTransportInfo, listTransports, reassignTransport, releaseTransport, releaseTransportRecursive, } from '../adt/transport.js';
|
|
22
|
+
import { getAppInfo } from '../adt/ui5-repository.js';
|
|
20
23
|
import { validateAffHeader } from '../aff/validator.js';
|
|
21
24
|
import { extractCdsElements } from '../context/cds-deps.js';
|
|
22
25
|
import { compressCdsContext, compressContext } from '../context/compressor.js';
|
|
@@ -74,24 +77,132 @@ function textResult(text) {
|
|
|
74
77
|
function errorResult(message) {
|
|
75
78
|
return { content: [{ type: 'text', text: message }], isError: true };
|
|
76
79
|
}
|
|
77
|
-
|
|
80
|
+
// ─── Search Helpers ─────────────────────────────────────────────────
|
|
81
|
+
/**
|
|
82
|
+
* Transliterate non-ASCII characters in search queries.
|
|
83
|
+
* SAP object names are ASCII-only, so umlauts and accented characters
|
|
84
|
+
* never appear in object names. This prevents wasted searches with
|
|
85
|
+
* German terms like "*Schätzung*" that silently return empty results.
|
|
86
|
+
*/
|
|
87
|
+
export function transliterateQuery(query) {
|
|
88
|
+
// Explicit German umlaut replacements (must come before NFD decomposition)
|
|
89
|
+
let result = query
|
|
90
|
+
.replace(/ä/g, 'AE')
|
|
91
|
+
.replace(/Ä/g, 'AE')
|
|
92
|
+
.replace(/ö/g, 'OE')
|
|
93
|
+
.replace(/Ö/g, 'OE')
|
|
94
|
+
.replace(/ü/g, 'UE')
|
|
95
|
+
.replace(/Ü/g, 'UE')
|
|
96
|
+
.replace(/ß/g, 'SS');
|
|
97
|
+
// General fallback: strip remaining diacritics (é→e, ñ→n, etc.)
|
|
98
|
+
result = result.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
|
99
|
+
return { normalized: result, changed: result !== query };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Detect if a search query looks like a field/column name rather than
|
|
103
|
+
* an object name. Field names are short, uppercase, and typically don't
|
|
104
|
+
* start with Z/Y (which are custom object prefixes).
|
|
105
|
+
*/
|
|
106
|
+
export function looksLikeFieldName(query) {
|
|
107
|
+
// Wildcard patterns are object searches, not field names
|
|
108
|
+
if (query.includes('*'))
|
|
109
|
+
return false;
|
|
110
|
+
if (query.length === 0 || query.length > 15)
|
|
111
|
+
return false;
|
|
112
|
+
// Must be uppercase letters, digits, underscores only
|
|
113
|
+
if (!/^[A-Z0-9_]+$/.test(query))
|
|
114
|
+
return false;
|
|
115
|
+
// Z/Y prefix → more likely an object name
|
|
116
|
+
if (/^[ZY]/.test(query))
|
|
117
|
+
return false;
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
78
120
|
/** Format error messages with LLM-friendly remediation hints */
|
|
79
121
|
function formatErrorForLLM(err, message, _tool, args) {
|
|
80
122
|
if (err instanceof AdtApiError) {
|
|
123
|
+
// Append additional SAP messages (line numbers, secondary errors) if available
|
|
124
|
+
const enriched = enrichWithSapDetails(err, message);
|
|
81
125
|
if (err.isNotFound) {
|
|
82
126
|
const name = String(args.name ?? '');
|
|
83
127
|
const type = String(args.type ?? '');
|
|
84
|
-
return `${
|
|
128
|
+
return `${enriched}\n\nHint: Object "${name}" (type ${type}) was not found. Use SAPSearch with query "${name}" to verify the name exists and check the correct type.`;
|
|
85
129
|
}
|
|
86
130
|
if (err.isUnauthorized || err.isForbidden) {
|
|
87
|
-
return `${
|
|
131
|
+
return `${enriched}\n\nHint: Authorization error. Check SAP_CLIENT (default: '100'), SAP_USER, and SAP_PASSWORD. The configured SAP user may lack permissions for this object.`;
|
|
132
|
+
}
|
|
133
|
+
// Transport / corrNr specific hints
|
|
134
|
+
const transportHint = getTransportHint(err);
|
|
135
|
+
if (transportHint) {
|
|
136
|
+
return `${enriched}\n\nHint: ${transportHint}`;
|
|
137
|
+
}
|
|
138
|
+
// Server errors (500, 502, 503, etc.)
|
|
139
|
+
if (err.isServerError) {
|
|
140
|
+
return `${enriched}\n\nHint: SAP application server error (${err.statusCode}). This is often transient — wait 10-30 seconds and retry. If the error persists, check SAPDiagnose(action="dumps") for short dumps, or verify the SAP system is responding via SAPRead(type="SYSTEM").`;
|
|
88
141
|
}
|
|
142
|
+
return enriched;
|
|
89
143
|
}
|
|
90
144
|
if (err instanceof AdtNetworkError) {
|
|
91
145
|
return `${message}\n\nHint: Cannot reach the SAP system. This is a connectivity issue, not a usage error.`;
|
|
92
146
|
}
|
|
93
147
|
return message;
|
|
94
148
|
}
|
|
149
|
+
/** Enrich error message with additional SAP XML diagnostic detail (extra messages, properties) */
|
|
150
|
+
function enrichWithSapDetails(err, message) {
|
|
151
|
+
if (!err.responseBody)
|
|
152
|
+
return message;
|
|
153
|
+
const extraMessages = AdtApiError.extractAllMessages(err.responseBody);
|
|
154
|
+
const props = AdtApiError.extractProperties(err.responseBody);
|
|
155
|
+
const parts = [message];
|
|
156
|
+
if (extraMessages.length > 0) {
|
|
157
|
+
parts.push(`\nAdditional detail:\n${extraMessages.map((m) => ` - ${m}`).join('\n')}`);
|
|
158
|
+
}
|
|
159
|
+
// Surface line/column info from properties if present
|
|
160
|
+
const lineInfo = props.LINE || props['T100KEY-NO'];
|
|
161
|
+
if (lineInfo || Object.keys(props).length > 0) {
|
|
162
|
+
const propStr = Object.entries(props)
|
|
163
|
+
.slice(0, 5) // Limit to avoid overwhelming output
|
|
164
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
165
|
+
.join(', ');
|
|
166
|
+
if (propStr)
|
|
167
|
+
parts.push(`Properties: ${propStr}`);
|
|
168
|
+
}
|
|
169
|
+
return parts.join('\n');
|
|
170
|
+
}
|
|
171
|
+
/** Detect transport/corrNr failure signatures and return a remediation hint, or undefined if not transport-related. */
|
|
172
|
+
function getTransportHint(err) {
|
|
173
|
+
const body = (err.responseBody ?? '').toLowerCase();
|
|
174
|
+
// Use the clean SAP error message, NOT err.message which includes the URL path.
|
|
175
|
+
// The URL path contains `corrNr=<id>` when a transport IS provided, causing false positives
|
|
176
|
+
// if we check for "corrnr" in the full message string.
|
|
177
|
+
const cleanMsg = AdtApiError.extractCleanMessage(err.responseBody ?? '').toLowerCase();
|
|
178
|
+
const combined = `${cleanMsg} ${body}`;
|
|
179
|
+
// Missing or invalid transport/correction number
|
|
180
|
+
if (combined.includes('correction number') ||
|
|
181
|
+
combined.includes('corrnr') ||
|
|
182
|
+
(combined.includes('transport request') &&
|
|
183
|
+
(combined.includes('missing') || combined.includes('required') || combined.includes('invalid')))) {
|
|
184
|
+
return 'A transport/correction number is required but was not provided or is invalid. Provide an explicit "transport" parameter with a valid transport request ID, or check SE09 in SAP GUI that an open transport exists for your user and target package.';
|
|
185
|
+
}
|
|
186
|
+
// Transport not found or not modifiable
|
|
187
|
+
if (combined.includes('e070') ||
|
|
188
|
+
(combined.includes('transport') &&
|
|
189
|
+
(combined.includes('not found') || combined.includes('does not exist') || combined.includes('not modifiable')))) {
|
|
190
|
+
return 'The specified transport request was not found or is not modifiable. Verify the transport ID in SE09, ensure it is not yet released, and that it belongs to the correct user and target package.';
|
|
191
|
+
}
|
|
192
|
+
// Package / transport layer mismatch
|
|
193
|
+
if (combined.includes('transport layer') ||
|
|
194
|
+
(combined.includes('package') &&
|
|
195
|
+
combined.includes('transport') &&
|
|
196
|
+
(combined.includes('mismatch') || combined.includes('not assigned') || combined.includes('no transport layer')))) {
|
|
197
|
+
return 'The target package has no transport layer or a transport layer mismatch. Check that the package is configured for transport in SE80/TDEVC, or use a local package ($TMP) if no transport is needed.';
|
|
198
|
+
}
|
|
199
|
+
// Authorization for transport operations
|
|
200
|
+
if (combined.includes('s_transprt') ||
|
|
201
|
+
(combined.includes('transport') && (combined.includes('no authorization') || combined.includes('not authorized')))) {
|
|
202
|
+
return 'The SAP user lacks transport authorization (S_TRANSPRT). Contact your SAP basis administrator to grant the required transport permissions.';
|
|
203
|
+
}
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
95
206
|
function classifyError(err) {
|
|
96
207
|
if (err instanceof AdtApiError)
|
|
97
208
|
return 'AdtApiError';
|
|
@@ -292,20 +403,26 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
292
403
|
if (isBtpSystem() && BTP_HINTS[type]) {
|
|
293
404
|
return errorResult(BTP_HINTS[type]);
|
|
294
405
|
}
|
|
295
|
-
// Helper: get source with cache support
|
|
406
|
+
// Helper: get source with cache support, returns cache hit status
|
|
296
407
|
const cachedGet = async (objType, objName, fetcher) => {
|
|
297
408
|
if (!cachingLayer)
|
|
298
|
-
return fetcher();
|
|
299
|
-
const { source } = await cachingLayer.getSource(objType, objName, fetcher);
|
|
300
|
-
return source;
|
|
409
|
+
return { source: await fetcher(), cacheHit: false };
|
|
410
|
+
const { source, hit } = await cachingLayer.getSource(objType, objName, fetcher);
|
|
411
|
+
return { source, cacheHit: hit };
|
|
412
|
+
};
|
|
413
|
+
/** Prepend [cached] indicator when result came from cache */
|
|
414
|
+
const cachedTextResult = (source, cacheHit) => {
|
|
415
|
+
return textResult(cacheHit ? `[cached]\n${source}` : source);
|
|
301
416
|
};
|
|
302
417
|
// Structured format is only supported for CLAS type
|
|
303
418
|
if (args.format === 'structured' && type !== 'CLAS') {
|
|
304
419
|
return errorResult('The "structured" format is only supported for CLAS type. Other types return text format.');
|
|
305
420
|
}
|
|
306
421
|
switch (type) {
|
|
307
|
-
case 'PROG':
|
|
308
|
-
|
|
422
|
+
case 'PROG': {
|
|
423
|
+
const { source, cacheHit } = await cachedGet('PROG', name, () => client.getProgram(name));
|
|
424
|
+
return cachedTextResult(source, cacheHit);
|
|
425
|
+
}
|
|
309
426
|
case 'CLAS': {
|
|
310
427
|
// Structured format: return JSON with metadata + decomposed source
|
|
311
428
|
if (args.format === 'structured') {
|
|
@@ -314,8 +431,8 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
314
431
|
}
|
|
315
432
|
const methodParam = args.method;
|
|
316
433
|
if (methodParam && !args.include) {
|
|
317
|
-
// Method-level read — fetch full source then extract
|
|
318
|
-
const fullSource = await cachedGet('CLAS', name, () => client.getClass(name));
|
|
434
|
+
// Method-level read — fetch full source then extract (no cache indicator for derived results)
|
|
435
|
+
const { source: fullSource } = await cachedGet('CLAS', name, () => client.getClass(name));
|
|
319
436
|
const abaplintVer = cachedFeatures?.abapRelease
|
|
320
437
|
? mapSapReleaseToAbaplintVersion(cachedFeatures.abapRelease)
|
|
321
438
|
: undefined;
|
|
@@ -331,12 +448,15 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
331
448
|
}
|
|
332
449
|
// Only cache the full merged source (no include param), not individual includes
|
|
333
450
|
if (!args.include) {
|
|
334
|
-
|
|
451
|
+
const { source, cacheHit } = await cachedGet('CLAS', name, () => client.getClass(name));
|
|
452
|
+
return cachedTextResult(source, cacheHit);
|
|
335
453
|
}
|
|
336
454
|
return textResult(await client.getClass(name, args.include));
|
|
337
455
|
}
|
|
338
|
-
case 'INTF':
|
|
339
|
-
|
|
456
|
+
case 'INTF': {
|
|
457
|
+
const { source, cacheHit } = await cachedGet('INTF', name, () => client.getInterface(name));
|
|
458
|
+
return cachedTextResult(source, cacheHit);
|
|
459
|
+
}
|
|
340
460
|
case 'FUNC': {
|
|
341
461
|
let group = String(args.group ?? '');
|
|
342
462
|
if (!group) {
|
|
@@ -349,7 +469,8 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
349
469
|
}
|
|
350
470
|
group = resolved;
|
|
351
471
|
}
|
|
352
|
-
|
|
472
|
+
const { source, cacheHit } = await cachedGet('FUNC', name, () => client.getFunction(group, name));
|
|
473
|
+
return cachedTextResult(source, cacheHit);
|
|
353
474
|
}
|
|
354
475
|
case 'FUGR': {
|
|
355
476
|
const expand = Boolean(args.expand_includes);
|
|
@@ -374,22 +495,34 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
374
495
|
const fg = await client.getFunctionGroup(name);
|
|
375
496
|
return textResult(JSON.stringify(fg, null, 2));
|
|
376
497
|
}
|
|
377
|
-
case 'INCL':
|
|
378
|
-
|
|
498
|
+
case 'INCL': {
|
|
499
|
+
const { source, cacheHit } = await cachedGet('INCL', name, () => client.getInclude(name));
|
|
500
|
+
return cachedTextResult(source, cacheHit);
|
|
501
|
+
}
|
|
379
502
|
case 'DDLS': {
|
|
380
|
-
const ddlSource = await cachedGet('DDLS', name, () => client.getDdls(name));
|
|
503
|
+
const { source: ddlSource, cacheHit } = await cachedGet('DDLS', name, () => client.getDdls(name));
|
|
504
|
+
if (ddlSource.trim() === '') {
|
|
505
|
+
return textResult(`DDLS ${name} exists in the object directory but has no source code stored. ` +
|
|
506
|
+
`The DDL source may need to be written via SAPWrite(action="create" or "update", type="DDLS", name="${name}", source="...").`);
|
|
507
|
+
}
|
|
381
508
|
if (args.include?.toLowerCase() === 'elements') {
|
|
509
|
+
// Elements extraction is derived from source — no cache indicator
|
|
382
510
|
return textResult(extractCdsElements(ddlSource, name));
|
|
383
511
|
}
|
|
384
|
-
return
|
|
512
|
+
return cachedTextResult(ddlSource, cacheHit);
|
|
513
|
+
}
|
|
514
|
+
case 'BDEF': {
|
|
515
|
+
const { source, cacheHit } = await cachedGet('BDEF', name, () => client.getBdef(name));
|
|
516
|
+
return cachedTextResult(source, cacheHit);
|
|
517
|
+
}
|
|
518
|
+
case 'SRVD': {
|
|
519
|
+
const { source, cacheHit } = await cachedGet('SRVD', name, () => client.getSrvd(name));
|
|
520
|
+
return cachedTextResult(source, cacheHit);
|
|
385
521
|
}
|
|
386
|
-
case 'BDEF':
|
|
387
|
-
return textResult(await cachedGet('BDEF', name, () => client.getBdef(name)));
|
|
388
|
-
case 'SRVD':
|
|
389
|
-
return textResult(await cachedGet('SRVD', name, () => client.getSrvd(name)));
|
|
390
522
|
case 'DDLX': {
|
|
391
523
|
try {
|
|
392
|
-
|
|
524
|
+
const { source, cacheHit } = await cachedGet('DDLX', name, () => client.getDdlx(name));
|
|
525
|
+
return cachedTextResult(source, cacheHit);
|
|
393
526
|
}
|
|
394
527
|
catch (err) {
|
|
395
528
|
if (isNotFoundError(err)) {
|
|
@@ -398,14 +531,22 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
398
531
|
throw err;
|
|
399
532
|
}
|
|
400
533
|
}
|
|
401
|
-
case 'SRVB':
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
case '
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
534
|
+
case 'SRVB': {
|
|
535
|
+
const { source, cacheHit } = await cachedGet('SRVB', name, () => client.getSrvb(name));
|
|
536
|
+
return cachedTextResult(source, cacheHit);
|
|
537
|
+
}
|
|
538
|
+
case 'TABL': {
|
|
539
|
+
const { source, cacheHit } = await cachedGet('TABL', name, () => client.getTable(name));
|
|
540
|
+
return cachedTextResult(source, cacheHit);
|
|
541
|
+
}
|
|
542
|
+
case 'VIEW': {
|
|
543
|
+
const { source, cacheHit } = await cachedGet('VIEW', name, () => client.getView(name));
|
|
544
|
+
return cachedTextResult(source, cacheHit);
|
|
545
|
+
}
|
|
546
|
+
case 'STRU': {
|
|
547
|
+
const { source, cacheHit } = await cachedGet('STRU', name, () => client.getStructure(name));
|
|
548
|
+
return cachedTextResult(source, cacheHit);
|
|
549
|
+
}
|
|
409
550
|
case 'DOMA': {
|
|
410
551
|
const domain = await client.getDomain(name);
|
|
411
552
|
return textResult(JSON.stringify(domain, null, 2));
|
|
@@ -431,6 +572,18 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
431
572
|
}
|
|
432
573
|
return textResult(JSON.stringify(tran, null, 2));
|
|
433
574
|
}
|
|
575
|
+
case 'API_STATE': {
|
|
576
|
+
// Determine object type for URL construction — use explicit objectType, infer from name, or error
|
|
577
|
+
const explicitType = String(args.objectType ?? '').toUpperCase();
|
|
578
|
+
const inferredType = explicitType || inferObjectType(name);
|
|
579
|
+
if (!inferredType) {
|
|
580
|
+
return errorResult(`Cannot infer object type from name "${name}". Please specify objectType explicitly (e.g., objectType="CLAS", "INTF", "PROG", "TABL", "DDLS", "FUGR", "DOMA", "DTEL", "SRVD", "SRVB", "BDEF").`);
|
|
581
|
+
}
|
|
582
|
+
// Use raw URI (no name encoding) — getApiReleaseState encodes the full URI as a single path segment
|
|
583
|
+
const objectUri = objectUrlForTypeRaw(inferredType, name);
|
|
584
|
+
const releaseState = await client.getApiReleaseState(objectUri);
|
|
585
|
+
return textResult(JSON.stringify(releaseState, null, 2));
|
|
586
|
+
}
|
|
434
587
|
case 'TABLE_CONTENTS': {
|
|
435
588
|
const maxRows = Number(args.maxRows ?? 100);
|
|
436
589
|
const data = await client.getTableContents(name, maxRows, args.sqlFilter);
|
|
@@ -474,24 +627,81 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
474
627
|
const components = await client.getInstalledComponents();
|
|
475
628
|
return textResult(JSON.stringify(components, null, 2));
|
|
476
629
|
}
|
|
477
|
-
case 'MESSAGES':
|
|
478
|
-
|
|
630
|
+
case 'MESSAGES': {
|
|
631
|
+
try {
|
|
632
|
+
const mcInfo = await client.getMessageClassInfo(name);
|
|
633
|
+
return textResult(JSON.stringify(mcInfo, null, 2));
|
|
634
|
+
}
|
|
635
|
+
catch {
|
|
636
|
+
// Fall back to legacy endpoint if messageclass endpoint unavailable
|
|
637
|
+
return textResult(await client.getMessages(name));
|
|
638
|
+
}
|
|
639
|
+
}
|
|
479
640
|
case 'TEXT_ELEMENTS':
|
|
480
641
|
return textResult(await client.getTextElements(name));
|
|
481
642
|
case 'VARIANTS':
|
|
482
643
|
return textResult(await client.getVariants(name));
|
|
644
|
+
case 'BSP': {
|
|
645
|
+
if (cachedFeatures?.ui5 && !cachedFeatures.ui5.available) {
|
|
646
|
+
return errorResult('UI5/Fiori BSP Filestore is not available on this SAP system. ' +
|
|
647
|
+
'Run SAPManage(action="probe") to verify feature availability.');
|
|
648
|
+
}
|
|
649
|
+
const include = args.include;
|
|
650
|
+
if (!name) {
|
|
651
|
+
// List all BSP apps (optional search via query param not used here since name is empty)
|
|
652
|
+
const apps = await client.listBspApps();
|
|
653
|
+
return textResult(JSON.stringify(apps, null, 2));
|
|
654
|
+
}
|
|
655
|
+
if (!include) {
|
|
656
|
+
// Browse root structure of the app
|
|
657
|
+
return textResult(JSON.stringify(await client.getBspAppStructure(name), null, 2));
|
|
658
|
+
}
|
|
659
|
+
// If include contains a dot, treat as file read; otherwise browse subfolder
|
|
660
|
+
if (include.includes('.')) {
|
|
661
|
+
return textResult(await client.getBspFileContent(name, include));
|
|
662
|
+
}
|
|
663
|
+
return textResult(JSON.stringify(await client.getBspAppStructure(name, `/${include}`), null, 2));
|
|
664
|
+
}
|
|
665
|
+
case 'BSP_DEPLOY': {
|
|
666
|
+
if (cachedFeatures?.ui5repo && !cachedFeatures.ui5repo.available) {
|
|
667
|
+
return errorResult('ABAP Repository OData Service is not available on this SAP system. ' +
|
|
668
|
+
'Run SAPManage(action="probe") to verify feature availability.');
|
|
669
|
+
}
|
|
670
|
+
if (!name) {
|
|
671
|
+
return errorResult('BSP_DEPLOY requires a name parameter (e.g., name="ZAPP_BOOKING").');
|
|
672
|
+
}
|
|
673
|
+
const info = await getAppInfo(client.http, client.safety, name);
|
|
674
|
+
if (!info) {
|
|
675
|
+
return textResult(`App "${name}" not found in ABAP Repository.`);
|
|
676
|
+
}
|
|
677
|
+
return textResult(JSON.stringify(info, null, 2));
|
|
678
|
+
}
|
|
679
|
+
case 'INACTIVE_OBJECTS': {
|
|
680
|
+
try {
|
|
681
|
+
const objects = await client.getInactiveObjects();
|
|
682
|
+
return textResult(JSON.stringify({ count: objects.length, objects }, null, 2));
|
|
683
|
+
}
|
|
684
|
+
catch (err) {
|
|
685
|
+
if (isNotFoundError(err)) {
|
|
686
|
+
return textResult('Inactive objects listing is not available on this SAP system ' +
|
|
687
|
+
'(the /sap/bc/adt/activation/inactive endpoint returned 404). ' +
|
|
688
|
+
'Use SAPDiagnose(action="syntax", type="...", name="...") to check specific objects instead.');
|
|
689
|
+
}
|
|
690
|
+
throw err;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
483
693
|
default:
|
|
484
|
-
return errorResult(`Unknown SAPRead type: "${type}". Supported types: PROG, CLAS, INTF, FUNC, FUGR, INCL, DDLS, DDLX, BDEF, SRVD, SRVB, TABL, VIEW, STRU, DOMA, DTEL, TRAN, TABLE_CONTENTS, DEVC, SOBJ, SYSTEM, COMPONENTS, MESSAGES, TEXT_ELEMENTS, VARIANTS. ` +
|
|
694
|
+
return errorResult(`Unknown SAPRead type: "${type}". Supported types: PROG, CLAS, INTF, FUNC, FUGR, INCL, DDLS, DDLX, BDEF, SRVD, SRVB, TABL, VIEW, STRU, DOMA, DTEL, TRAN, TABLE_CONTENTS, DEVC, SOBJ, SYSTEM, COMPONENTS, MESSAGES, TEXT_ELEMENTS, VARIANTS, BSP, BSP_DEPLOY, API_STATE, INACTIVE_OBJECTS. ` +
|
|
485
695
|
'Tip: Map objectType from SAPSearch results by dropping the slash suffix (e.g., DDLS/DF → type="DDLS", CLAS/OC → type="CLAS", PROG/P → type="PROG"). ' +
|
|
486
696
|
'Do not pass a URI — use the "type" and "name" parameters instead.');
|
|
487
697
|
}
|
|
488
698
|
}
|
|
489
699
|
async function handleSAPSearch(client, args) {
|
|
490
|
-
const
|
|
700
|
+
const rawQuery = String(args.query ?? '');
|
|
491
701
|
const maxResults = Number(args.maxResults ?? 100);
|
|
492
702
|
const searchType = String(args.searchType ?? 'object');
|
|
493
703
|
if (searchType === 'source_code') {
|
|
494
|
-
//
|
|
704
|
+
// Source code search: do NOT transliterate — source can contain umlauts in strings/comments
|
|
495
705
|
if (cachedFeatures?.textSearch && !cachedFeatures.textSearch.available) {
|
|
496
706
|
return errorResult(`Source code search is not available on this SAP system. ${cachedFeatures.textSearch.reason ?? ''}` +
|
|
497
707
|
`\nUse SAPSearch with searchType="object" to search by object name instead, or use SAPQuery to search metadata tables.`);
|
|
@@ -499,7 +709,7 @@ async function handleSAPSearch(client, args) {
|
|
|
499
709
|
const objectType = args.objectType;
|
|
500
710
|
const packageName = args.packageName;
|
|
501
711
|
try {
|
|
502
|
-
const results = await client.searchSource(
|
|
712
|
+
const results = await client.searchSource(rawQuery, maxResults, objectType, packageName);
|
|
503
713
|
return textResult(JSON.stringify(results, null, 2));
|
|
504
714
|
}
|
|
505
715
|
catch (err) {
|
|
@@ -514,8 +724,25 @@ async function handleSAPSearch(client, args) {
|
|
|
514
724
|
throw err;
|
|
515
725
|
}
|
|
516
726
|
}
|
|
727
|
+
// Object search: transliterate non-ASCII (SAP object names are ASCII-only)
|
|
728
|
+
const { normalized: query, changed: wasTransliterated } = transliterateQuery(rawQuery);
|
|
729
|
+
const transliterationNote = wasTransliterated
|
|
730
|
+
? `Note: Query contained non-ASCII characters. Transliterated "${rawQuery}" → "${query}" (SAP object names are ASCII-only).\n\n`
|
|
731
|
+
: '';
|
|
517
732
|
const results = await client.searchObject(query, maxResults);
|
|
518
|
-
|
|
733
|
+
if (Array.isArray(results) && results.length === 0) {
|
|
734
|
+
let hint = '[]' +
|
|
735
|
+
'\n\n' +
|
|
736
|
+
transliterationNote +
|
|
737
|
+
'No objects found. If searching for custom objects, try Z* or Y* prefixes (e.g., "Z*ESTIM*"). ' +
|
|
738
|
+
'If you already found objects in a package, use SAPRead with type=DEVC to list all package contents instead of more searches.';
|
|
739
|
+
if (looksLikeFieldName(query)) {
|
|
740
|
+
const stripped = query.replace(/\*/g, '');
|
|
741
|
+
hint += `\nThis looks like a field/column name. Use SAPQuery("SELECT fieldname, rollname, domname FROM dd03l WHERE fieldname = '${stripped}'") or SAPRead(type='DDLS', include='elements') to find fields.`;
|
|
742
|
+
}
|
|
743
|
+
return textResult(hint);
|
|
744
|
+
}
|
|
745
|
+
return textResult(transliterationNote + JSON.stringify(results, null, 2));
|
|
519
746
|
}
|
|
520
747
|
async function handleSAPQuery(client, args) {
|
|
521
748
|
const sql = String(args.sql ?? '');
|
|
@@ -616,6 +843,187 @@ function buildLintConfigOptions(config, ruleOverrides) {
|
|
|
616
843
|
};
|
|
617
844
|
}
|
|
618
845
|
// ─── Object Creation XML ─────────────────────────────────────────────
|
|
846
|
+
const DOMAIN_V2_CONTENT_TYPE = 'application/vnd.sap.adt.domains.v2+xml; charset=utf-8';
|
|
847
|
+
const DATAELEMENT_V2_CONTENT_TYPE = 'application/vnd.sap.adt.dataelements.v2+xml; charset=utf-8';
|
|
848
|
+
const SERVICEBINDING_V2_CONTENT_TYPE = 'application/vnd.sap.adt.businessservices.servicebinding.v2+xml; charset=utf-8';
|
|
849
|
+
const BDEF_CONTENT_TYPE = 'application/vnd.sap.adt.blues.v1+xml';
|
|
850
|
+
const MESSAGECLASS_CONTENT_TYPE = 'application/vnd.sap.adt.mc.messageclass+xml';
|
|
851
|
+
function isMetadataWriteType(type) {
|
|
852
|
+
return type === 'DOMA' || type === 'DTEL' || type === 'MSAG' || type === 'SRVB';
|
|
853
|
+
}
|
|
854
|
+
/** Types that require a specific vendor content type for creation (not application/*) */
|
|
855
|
+
function needsVendorContentType(type) {
|
|
856
|
+
return type === 'DOMA' || type === 'DTEL' || type === 'BDEF' || type === 'MSAG';
|
|
857
|
+
}
|
|
858
|
+
/** Content type used for create POST */
|
|
859
|
+
function createContentTypeForType(type) {
|
|
860
|
+
// SRVB creation works with wildcard content type; updates use vendor v2 type.
|
|
861
|
+
if (type === 'SRVB')
|
|
862
|
+
return 'application/*';
|
|
863
|
+
return needsVendorContentType(type) ? vendorContentTypeForType(type) : 'application/*';
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Check if a DTEL create has properties that SAP ignores on POST but accepts on PUT.
|
|
867
|
+
* SAP's DTEL POST only stores the shell (name, description, package, typeKind, typeName, dataType, length).
|
|
868
|
+
* Labels, searchHelp, setGetParameter, etc. require a follow-up PUT to take effect.
|
|
869
|
+
*/
|
|
870
|
+
function dtelNeedsPostCreateUpdate(props) {
|
|
871
|
+
return Boolean(props.shortLabel ||
|
|
872
|
+
props.mediumLabel ||
|
|
873
|
+
props.longLabel ||
|
|
874
|
+
props.headingLabel ||
|
|
875
|
+
props.searchHelp ||
|
|
876
|
+
props.searchHelpParameter ||
|
|
877
|
+
props.setGetParameter ||
|
|
878
|
+
props.defaultComponentName ||
|
|
879
|
+
props.changeDocument);
|
|
880
|
+
}
|
|
881
|
+
function vendorContentTypeForType(type) {
|
|
882
|
+
switch (type) {
|
|
883
|
+
case 'DOMA':
|
|
884
|
+
return DOMAIN_V2_CONTENT_TYPE;
|
|
885
|
+
case 'DTEL':
|
|
886
|
+
return DATAELEMENT_V2_CONTENT_TYPE;
|
|
887
|
+
case 'SRVB':
|
|
888
|
+
return SERVICEBINDING_V2_CONTENT_TYPE;
|
|
889
|
+
case 'BDEF':
|
|
890
|
+
return BDEF_CONTENT_TYPE;
|
|
891
|
+
case 'MSAG':
|
|
892
|
+
return MESSAGECLASS_CONTENT_TYPE;
|
|
893
|
+
default:
|
|
894
|
+
// Wildcard lets the SAP server resolve the correct handler.
|
|
895
|
+
// Sending 'application/xml' causes 415 on DDL-based endpoints
|
|
896
|
+
// (DDLS, SRVD, DDLX) whose resource classes reject that literal type.
|
|
897
|
+
return 'application/*';
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
function toBoolean(value) {
|
|
901
|
+
if (typeof value === 'boolean')
|
|
902
|
+
return value;
|
|
903
|
+
if (typeof value === 'number')
|
|
904
|
+
return value !== 0;
|
|
905
|
+
if (typeof value === 'string') {
|
|
906
|
+
const normalized = value.trim().toLowerCase();
|
|
907
|
+
if (normalized === 'true')
|
|
908
|
+
return true;
|
|
909
|
+
if (normalized === 'false')
|
|
910
|
+
return false;
|
|
911
|
+
}
|
|
912
|
+
return undefined;
|
|
913
|
+
}
|
|
914
|
+
function getMetadataWriteProperties(input) {
|
|
915
|
+
const props = {
|
|
916
|
+
dataType: input.dataType,
|
|
917
|
+
length: input.length,
|
|
918
|
+
decimals: input.decimals,
|
|
919
|
+
outputLength: input.outputLength,
|
|
920
|
+
conversionExit: input.conversionExit,
|
|
921
|
+
signExists: input.signExists,
|
|
922
|
+
lowercase: input.lowercase,
|
|
923
|
+
fixedValues: input.fixedValues,
|
|
924
|
+
valueTable: input.valueTable,
|
|
925
|
+
typeKind: input.typeKind,
|
|
926
|
+
typeName: input.typeName,
|
|
927
|
+
domainName: input.domainName,
|
|
928
|
+
shortLabel: input.shortLabel,
|
|
929
|
+
mediumLabel: input.mediumLabel,
|
|
930
|
+
longLabel: input.longLabel,
|
|
931
|
+
headingLabel: input.headingLabel,
|
|
932
|
+
searchHelp: input.searchHelp,
|
|
933
|
+
searchHelpParameter: input.searchHelpParameter,
|
|
934
|
+
setGetParameter: input.setGetParameter,
|
|
935
|
+
defaultComponentName: input.defaultComponentName,
|
|
936
|
+
changeDocument: input.changeDocument,
|
|
937
|
+
messages: input.messages,
|
|
938
|
+
serviceDefinition: input.serviceDefinition,
|
|
939
|
+
bindingType: input.bindingType,
|
|
940
|
+
category: input.category,
|
|
941
|
+
version: input.version,
|
|
942
|
+
};
|
|
943
|
+
return props;
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Fetch existing DDIC metadata and merge with provided properties.
|
|
947
|
+
* This ensures that updating a single field (e.g., shortLabel) doesn't
|
|
948
|
+
* reset other fields (e.g., dataType, typeKind) to defaults, since
|
|
949
|
+
* DDIC updates are full-XML-replace operations.
|
|
950
|
+
*
|
|
951
|
+
* Internal _description and _package fields carry the existing values
|
|
952
|
+
* for the caller to use as fallbacks.
|
|
953
|
+
*/
|
|
954
|
+
function normalizeSrvbCategory(value) {
|
|
955
|
+
if (value === '0' || value === 0 || value === 'UI')
|
|
956
|
+
return '0';
|
|
957
|
+
if (value === '1' || value === 1 || value === 'Web API')
|
|
958
|
+
return '1';
|
|
959
|
+
return undefined;
|
|
960
|
+
}
|
|
961
|
+
async function mergeMetadataWriteProperties(client, type, name, provided) {
|
|
962
|
+
try {
|
|
963
|
+
if (type === 'MSAG') {
|
|
964
|
+
const existing = await client.getMessageClassInfo(name);
|
|
965
|
+
return {
|
|
966
|
+
_description: existing.description,
|
|
967
|
+
_package: existing.package,
|
|
968
|
+
messages: provided.messages ?? existing.messages,
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
if (type === 'DOMA') {
|
|
972
|
+
const existing = await client.getDomain(name);
|
|
973
|
+
return {
|
|
974
|
+
_description: existing.description,
|
|
975
|
+
_package: existing.package,
|
|
976
|
+
dataType: provided.dataType ?? existing.dataType,
|
|
977
|
+
length: provided.length ?? existing.length,
|
|
978
|
+
decimals: provided.decimals ?? existing.decimals,
|
|
979
|
+
outputLength: provided.outputLength ?? existing.outputLength,
|
|
980
|
+
conversionExit: provided.conversionExit ?? existing.conversionExit,
|
|
981
|
+
signExists: provided.signExists ?? existing.signExists,
|
|
982
|
+
lowercase: provided.lowercase ?? existing.lowercase,
|
|
983
|
+
fixedValues: provided.fixedValues ?? existing.fixedValues,
|
|
984
|
+
valueTable: provided.valueTable ?? existing.valueTable,
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
if (type === 'DTEL') {
|
|
988
|
+
const existing = await client.getDataElement(name);
|
|
989
|
+
return {
|
|
990
|
+
_description: existing.description,
|
|
991
|
+
_package: existing.package,
|
|
992
|
+
dataType: provided.dataType ?? existing.dataType,
|
|
993
|
+
length: provided.length ?? existing.length,
|
|
994
|
+
decimals: provided.decimals ?? existing.decimals,
|
|
995
|
+
typeKind: provided.typeKind ?? existing.typeKind,
|
|
996
|
+
typeName: provided.typeName ?? existing.typeName,
|
|
997
|
+
domainName: provided.domainName ?? existing.typeName, // DTEL stores domain in typeName
|
|
998
|
+
shortLabel: provided.shortLabel ?? existing.shortLabel,
|
|
999
|
+
mediumLabel: provided.mediumLabel ?? existing.mediumLabel,
|
|
1000
|
+
longLabel: provided.longLabel ?? existing.longLabel,
|
|
1001
|
+
headingLabel: provided.headingLabel ?? existing.headingLabel,
|
|
1002
|
+
searchHelp: provided.searchHelp ?? existing.searchHelp,
|
|
1003
|
+
searchHelpParameter: provided.searchHelpParameter,
|
|
1004
|
+
setGetParameter: provided.setGetParameter,
|
|
1005
|
+
defaultComponentName: provided.defaultComponentName ?? existing.defaultComponentName,
|
|
1006
|
+
changeDocument: provided.changeDocument,
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
if (type === 'SRVB') {
|
|
1010
|
+
const existingRaw = await client.getSrvb(name);
|
|
1011
|
+
const existing = JSON.parse(existingRaw);
|
|
1012
|
+
return {
|
|
1013
|
+
_description: existing.description,
|
|
1014
|
+
_package: existing.package,
|
|
1015
|
+
serviceDefinition: provided.serviceDefinition ?? existing.serviceDefinition,
|
|
1016
|
+
bindingType: provided.bindingType ?? existing.bindingType,
|
|
1017
|
+
category: provided.category ?? normalizeSrvbCategory(existing.bindingCategory),
|
|
1018
|
+
version: provided.version ?? existing.serviceVersion,
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
catch {
|
|
1023
|
+
// If we can't read existing metadata (e.g., object is new/inactive), fall through
|
|
1024
|
+
}
|
|
1025
|
+
return provided;
|
|
1026
|
+
}
|
|
619
1027
|
/**
|
|
620
1028
|
* Build the type-specific XML body for ADT object creation.
|
|
621
1029
|
*
|
|
@@ -623,7 +1031,101 @@ function buildLintConfigOptions(config, ruleOverrides) {
|
|
|
623
1031
|
* Using a generic body (e.g. adtcore:objectReferences) returns 400:
|
|
624
1032
|
* "System expected the element '{http://www.sap.com/adt/programs/programs}abapProgram'"
|
|
625
1033
|
*/
|
|
626
|
-
|
|
1034
|
+
// ─── CDS Pre-Write Validation ──────────────────────────────────────
|
|
1035
|
+
/** Common CDS reserved/function keywords that cause silent DDL save failures when used as field names */
|
|
1036
|
+
const CDS_RESERVED_KEYWORDS = new Set([
|
|
1037
|
+
'position',
|
|
1038
|
+
'value',
|
|
1039
|
+
'type',
|
|
1040
|
+
'data',
|
|
1041
|
+
'timestamp',
|
|
1042
|
+
'language',
|
|
1043
|
+
'text',
|
|
1044
|
+
'source',
|
|
1045
|
+
'target',
|
|
1046
|
+
'name',
|
|
1047
|
+
'description',
|
|
1048
|
+
'concat',
|
|
1049
|
+
'replace',
|
|
1050
|
+
'substring',
|
|
1051
|
+
'length',
|
|
1052
|
+
'left',
|
|
1053
|
+
'right',
|
|
1054
|
+
'round',
|
|
1055
|
+
'abs',
|
|
1056
|
+
'floor',
|
|
1057
|
+
'ceiling',
|
|
1058
|
+
'division',
|
|
1059
|
+
'mod',
|
|
1060
|
+
'case',
|
|
1061
|
+
'when',
|
|
1062
|
+
'then',
|
|
1063
|
+
'else',
|
|
1064
|
+
'end',
|
|
1065
|
+
'cast',
|
|
1066
|
+
'coalesce',
|
|
1067
|
+
'uuid',
|
|
1068
|
+
]);
|
|
1069
|
+
/**
|
|
1070
|
+
* Guard CDS syntax against known version-dependent features.
|
|
1071
|
+
* Returns an error result if the source uses unsupported syntax, or undefined to proceed.
|
|
1072
|
+
* Best-effort: if cachedFeatures is not available (no probe yet), always proceeds.
|
|
1073
|
+
*/
|
|
1074
|
+
function guardCdsSyntax(type, source, features) {
|
|
1075
|
+
if (type !== 'DDLS' || !source)
|
|
1076
|
+
return undefined;
|
|
1077
|
+
// Guard: "define table entity" requires ABAP Cloud (BTP) or SAP_BASIS >= 757
|
|
1078
|
+
if (/\bdefine\s+table\s+(entity|function)\b/i.test(source)) {
|
|
1079
|
+
const release = features?.abapRelease;
|
|
1080
|
+
const isBtp = features?.systemType === 'btp';
|
|
1081
|
+
if (!isBtp && release) {
|
|
1082
|
+
const releaseNum = Number.parseInt(release.replace(/\D/g, ''), 10);
|
|
1083
|
+
if (releaseNum > 0 && releaseNum < 757) {
|
|
1084
|
+
return errorResult(`"define table entity" syntax requires ABAP Cloud (BTP) or S/4HANA on-premise with SAP_BASIS >= 757. ` +
|
|
1085
|
+
`This system reports SAP_BASIS ${release}. ` +
|
|
1086
|
+
`Use DDIC transparent tables (SAPWrite type="TABL" or SE11) + CDS view entities ("define [root] view entity") instead.`);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
// Advisory: warn about CDS reserved keywords used as field names
|
|
1091
|
+
const keywordWarning = warnCdsReservedKeywords(source);
|
|
1092
|
+
if (keywordWarning) {
|
|
1093
|
+
// Non-blocking — return undefined to proceed, but the warning will be
|
|
1094
|
+
// appended to the success message by the caller if needed.
|
|
1095
|
+
// For now we return it as an advisory error only when the keyword is
|
|
1096
|
+
// highly likely to cause issues (position is the most common).
|
|
1097
|
+
// We don't block the write — just append it as advisory context.
|
|
1098
|
+
}
|
|
1099
|
+
return undefined;
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Detect CDS reserved keywords used as field names in DDL source.
|
|
1103
|
+
* Returns a warning string listing suspicious field names, or undefined if none found.
|
|
1104
|
+
*/
|
|
1105
|
+
export function warnCdsReservedKeywords(source) {
|
|
1106
|
+
// Extract field-name-like tokens: lines inside { } that define fields
|
|
1107
|
+
// Pattern: whitespace + identifier + colon (field definitions)
|
|
1108
|
+
const fieldNames = [];
|
|
1109
|
+
const braceStart = source.indexOf('{');
|
|
1110
|
+
const braceEnd = source.lastIndexOf('}');
|
|
1111
|
+
if (braceStart === -1 || braceEnd === -1)
|
|
1112
|
+
return undefined;
|
|
1113
|
+
const body = source.slice(braceStart + 1, braceEnd);
|
|
1114
|
+
// Match field definitions: leading whitespace, optional "key", then identifier before ":"
|
|
1115
|
+
const fieldPattern = /^\s*(?:key\s+)?(\w+)\s*:/gim;
|
|
1116
|
+
let match;
|
|
1117
|
+
while ((match = fieldPattern.exec(body)) !== null) {
|
|
1118
|
+
const fieldName = match[1]?.toLowerCase();
|
|
1119
|
+
if (fieldName && CDS_RESERVED_KEYWORDS.has(fieldName)) {
|
|
1120
|
+
fieldNames.push(match[1]);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
if (fieldNames.length === 0)
|
|
1124
|
+
return undefined;
|
|
1125
|
+
return (`Warning: field name(s) ${fieldNames.map((f) => `'${f}'`).join(', ')} may be CDS reserved keywords. ` +
|
|
1126
|
+
`If the DDL save fails with a generic syntax error, rename them (e.g., 'position' → 'playing_position', 'type' → 'obj_type').`);
|
|
1127
|
+
}
|
|
1128
|
+
export function buildCreateXml(type, name, pkg, description, properties) {
|
|
627
1129
|
switch (type) {
|
|
628
1130
|
case 'PROG':
|
|
629
1131
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -682,45 +1184,145 @@ export function buildCreateXml(type, name, pkg, description) {
|
|
|
682
1184
|
adtcore:type="DDLS/DF"
|
|
683
1185
|
adtcore:masterLanguage="EN"
|
|
684
1186
|
adtcore:masterSystem="H00"
|
|
685
|
-
|
|
1187
|
+
adtcore:responsible="DEVELOPER">
|
|
686
1188
|
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
687
1189
|
</ddl:ddlSource>`;
|
|
1190
|
+
case 'TABL':
|
|
1191
|
+
// TABL creation also uses SAP's "blue" framework envelope, then source is written via /source/main.
|
|
1192
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
1193
|
+
<blue:blueSource xmlns:blue="http://www.sap.com/wbobj/blue"
|
|
1194
|
+
xmlns:adtcore="http://www.sap.com/adt/core"
|
|
1195
|
+
adtcore:description="${escapeXml(description)}"
|
|
1196
|
+
adtcore:name="${escapeXml(name)}"
|
|
1197
|
+
adtcore:type="TABL/DT"
|
|
1198
|
+
adtcore:masterLanguage="EN"
|
|
1199
|
+
adtcore:masterSystem="H00"
|
|
1200
|
+
adtcore:responsible="DEVELOPER">
|
|
1201
|
+
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
1202
|
+
</blue:blueSource>`;
|
|
688
1203
|
case 'BDEF':
|
|
1204
|
+
// BDEF uses SAP's "blue" framework — blue:blueSource with http://www.sap.com/wbobj/blue namespace.
|
|
1205
|
+
// Confirmed by vibing-steampunk (Go) and fr0ster (TypeScript) reference implementations.
|
|
689
1206
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
690
|
-
<
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
1207
|
+
<blue:blueSource xmlns:blue="http://www.sap.com/wbobj/blue"
|
|
1208
|
+
xmlns:adtcore="http://www.sap.com/adt/core"
|
|
1209
|
+
adtcore:description="${escapeXml(description)}"
|
|
1210
|
+
adtcore:name="${escapeXml(name)}"
|
|
1211
|
+
adtcore:type="BDEF/BDO"
|
|
1212
|
+
adtcore:masterLanguage="EN"
|
|
1213
|
+
adtcore:masterSystem="H00"
|
|
1214
|
+
adtcore:responsible="DEVELOPER">
|
|
698
1215
|
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
699
|
-
</
|
|
1216
|
+
</blue:blueSource>`;
|
|
700
1217
|
case 'SRVD':
|
|
701
1218
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
702
|
-
<srvd:srvdSource xmlns:srvd="http://www.sap.com/adt/ddic/
|
|
1219
|
+
<srvd:srvdSource xmlns:srvd="http://www.sap.com/adt/ddic/srvdsources"
|
|
703
1220
|
xmlns:adtcore="http://www.sap.com/adt/core"
|
|
704
1221
|
adtcore:description="${escapeXml(description)}"
|
|
705
1222
|
adtcore:name="${escapeXml(name)}"
|
|
706
1223
|
adtcore:type="SRVD/SRV"
|
|
707
1224
|
adtcore:masterLanguage="EN"
|
|
708
1225
|
adtcore:masterSystem="H00"
|
|
709
|
-
adtcore:responsible="DEVELOPER"
|
|
1226
|
+
adtcore:responsible="DEVELOPER"
|
|
1227
|
+
srvd:srvdSourceType="S">
|
|
710
1228
|
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
711
1229
|
</srvd:srvdSource>`;
|
|
1230
|
+
case 'SRVB': {
|
|
1231
|
+
const serviceDefinition = String(properties?.serviceDefinition ?? '').trim();
|
|
1232
|
+
if (!serviceDefinition) {
|
|
1233
|
+
throw new Error('SRVB create/update requires "serviceDefinition" (referenced SRVD name).');
|
|
1234
|
+
}
|
|
1235
|
+
const categoryRaw = properties?.category;
|
|
1236
|
+
const category = categoryRaw === '1' || categoryRaw === 1 ? '1' : '0';
|
|
1237
|
+
const params = {
|
|
1238
|
+
name,
|
|
1239
|
+
description,
|
|
1240
|
+
package: pkg,
|
|
1241
|
+
serviceDefinition,
|
|
1242
|
+
bindingType: properties?.bindingType ? String(properties.bindingType) : undefined,
|
|
1243
|
+
category,
|
|
1244
|
+
version: properties?.version ? String(properties.version) : undefined,
|
|
1245
|
+
};
|
|
1246
|
+
return buildServiceBindingXml(params);
|
|
1247
|
+
}
|
|
712
1248
|
case 'DDLX':
|
|
713
1249
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
714
|
-
<ddlx:ddlxSource xmlns:ddlx="http://www.sap.com/adt/ddic/
|
|
1250
|
+
<ddlx:ddlxSource xmlns:ddlx="http://www.sap.com/adt/ddic/ddlxsources"
|
|
715
1251
|
xmlns:adtcore="http://www.sap.com/adt/core"
|
|
716
1252
|
adtcore:description="${escapeXml(description)}"
|
|
717
1253
|
adtcore:name="${escapeXml(name)}"
|
|
718
1254
|
adtcore:type="DDLX/EX"
|
|
719
1255
|
adtcore:masterLanguage="EN"
|
|
720
1256
|
adtcore:masterSystem="H00"
|
|
721
|
-
|
|
1257
|
+
adtcore:responsible="DEVELOPER">
|
|
722
1258
|
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
723
1259
|
</ddlx:ddlxSource>`;
|
|
1260
|
+
case 'DOMA': {
|
|
1261
|
+
const fixedValuesRaw = Array.isArray(properties?.fixedValues) ? properties.fixedValues : [];
|
|
1262
|
+
const fixedValues = fixedValuesRaw
|
|
1263
|
+
.filter((value) => typeof value === 'object' && value !== null)
|
|
1264
|
+
.map((value) => ({
|
|
1265
|
+
low: String(value.low ?? ''),
|
|
1266
|
+
high: value.high === undefined ? undefined : String(value.high),
|
|
1267
|
+
description: value.description === undefined ? undefined : String(value.description),
|
|
1268
|
+
}));
|
|
1269
|
+
const params = {
|
|
1270
|
+
name,
|
|
1271
|
+
description,
|
|
1272
|
+
package: pkg,
|
|
1273
|
+
dataType: String(properties?.dataType ?? 'CHAR'),
|
|
1274
|
+
length: properties?.length ?? 0,
|
|
1275
|
+
decimals: properties?.decimals,
|
|
1276
|
+
outputLength: properties?.outputLength,
|
|
1277
|
+
conversionExit: properties?.conversionExit ? String(properties.conversionExit) : undefined,
|
|
1278
|
+
signExists: toBoolean(properties?.signExists),
|
|
1279
|
+
lowercase: toBoolean(properties?.lowercase),
|
|
1280
|
+
fixedValues,
|
|
1281
|
+
valueTable: properties?.valueTable ? String(properties.valueTable) : undefined,
|
|
1282
|
+
};
|
|
1283
|
+
return buildDomainXml(params);
|
|
1284
|
+
}
|
|
1285
|
+
case 'DTEL': {
|
|
1286
|
+
const typeKindRaw = String(properties?.typeKind ?? '');
|
|
1287
|
+
const typeKind = typeKindRaw === 'domain' || typeKindRaw === 'predefinedAbapType' ? typeKindRaw : undefined;
|
|
1288
|
+
const params = {
|
|
1289
|
+
name,
|
|
1290
|
+
description,
|
|
1291
|
+
package: pkg,
|
|
1292
|
+
typeKind,
|
|
1293
|
+
typeName: properties?.typeName ? String(properties.typeName) : undefined,
|
|
1294
|
+
domainName: properties?.domainName ? String(properties.domainName) : undefined,
|
|
1295
|
+
dataType: properties?.dataType ? String(properties.dataType) : undefined,
|
|
1296
|
+
length: properties?.length,
|
|
1297
|
+
decimals: properties?.decimals,
|
|
1298
|
+
shortLabel: properties?.shortLabel ? String(properties.shortLabel) : undefined,
|
|
1299
|
+
mediumLabel: properties?.mediumLabel ? String(properties.mediumLabel) : undefined,
|
|
1300
|
+
longLabel: properties?.longLabel ? String(properties.longLabel) : undefined,
|
|
1301
|
+
headingLabel: properties?.headingLabel ? String(properties.headingLabel) : undefined,
|
|
1302
|
+
searchHelp: properties?.searchHelp ? String(properties.searchHelp) : undefined,
|
|
1303
|
+
searchHelpParameter: properties?.searchHelpParameter ? String(properties.searchHelpParameter) : undefined,
|
|
1304
|
+
setGetParameter: properties?.setGetParameter ? String(properties.setGetParameter) : undefined,
|
|
1305
|
+
defaultComponentName: properties?.defaultComponentName ? String(properties.defaultComponentName) : undefined,
|
|
1306
|
+
changeDocument: toBoolean(properties?.changeDocument),
|
|
1307
|
+
};
|
|
1308
|
+
return buildDataElementXml(params);
|
|
1309
|
+
}
|
|
1310
|
+
case 'MSAG': {
|
|
1311
|
+
const messagesRaw = Array.isArray(properties?.messages) ? properties.messages : [];
|
|
1312
|
+
const messages = messagesRaw
|
|
1313
|
+
.filter((m) => typeof m === 'object' && m !== null)
|
|
1314
|
+
.map((m) => ({
|
|
1315
|
+
number: String(m.number ?? ''),
|
|
1316
|
+
shortText: String(m.shortText ?? ''),
|
|
1317
|
+
}));
|
|
1318
|
+
const params = {
|
|
1319
|
+
name,
|
|
1320
|
+
description,
|
|
1321
|
+
package: pkg,
|
|
1322
|
+
messages: messages.length > 0 ? messages : undefined,
|
|
1323
|
+
};
|
|
1324
|
+
return buildMessageClassXml(params);
|
|
1325
|
+
}
|
|
724
1326
|
default:
|
|
725
1327
|
// Fallback — generic objectReferences using the correct URL for the type
|
|
726
1328
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -739,46 +1341,71 @@ function escapeXml(s) {
|
|
|
739
1341
|
.replace(/>/g, '>');
|
|
740
1342
|
}
|
|
741
1343
|
// ─── Object URL Mapping ──────────────────────────────────────────────
|
|
742
|
-
/**
|
|
743
|
-
function
|
|
744
|
-
const encoded = encodeURIComponent(name);
|
|
1344
|
+
/** Base path for an object type. Returns path prefix without trailing name segment. */
|
|
1345
|
+
function objectBasePath(type) {
|
|
745
1346
|
switch (type) {
|
|
746
1347
|
case 'PROG':
|
|
747
|
-
return
|
|
1348
|
+
return '/sap/bc/adt/programs/programs/';
|
|
748
1349
|
case 'CLAS':
|
|
749
|
-
return
|
|
1350
|
+
return '/sap/bc/adt/oo/classes/';
|
|
750
1351
|
case 'INTF':
|
|
751
|
-
return
|
|
1352
|
+
return '/sap/bc/adt/oo/interfaces/';
|
|
752
1353
|
case 'FUNC':
|
|
753
|
-
return
|
|
1354
|
+
return '/sap/bc/adt/functions/groups/';
|
|
754
1355
|
case 'INCL':
|
|
755
|
-
return
|
|
1356
|
+
return '/sap/bc/adt/programs/includes/';
|
|
756
1357
|
case 'FUGR':
|
|
757
|
-
return
|
|
1358
|
+
return '/sap/bc/adt/functions/groups/';
|
|
758
1359
|
case 'DDLS':
|
|
759
|
-
return
|
|
1360
|
+
return '/sap/bc/adt/ddic/ddl/sources/';
|
|
760
1361
|
case 'BDEF':
|
|
761
|
-
return
|
|
1362
|
+
return '/sap/bc/adt/bo/behaviordefinitions/';
|
|
762
1363
|
case 'SRVD':
|
|
763
|
-
return
|
|
1364
|
+
return '/sap/bc/adt/ddic/srvd/sources/';
|
|
764
1365
|
case 'DDLX':
|
|
765
|
-
return
|
|
1366
|
+
return '/sap/bc/adt/ddic/ddlx/sources/';
|
|
766
1367
|
case 'SRVB':
|
|
767
|
-
return
|
|
1368
|
+
return '/sap/bc/adt/businessservices/bindings/';
|
|
768
1369
|
case 'TABL':
|
|
769
|
-
return
|
|
1370
|
+
return '/sap/bc/adt/ddic/tables/';
|
|
770
1371
|
case 'STRU':
|
|
771
|
-
return
|
|
1372
|
+
return '/sap/bc/adt/ddic/structures/';
|
|
772
1373
|
case 'DOMA':
|
|
773
|
-
return
|
|
1374
|
+
return '/sap/bc/adt/ddic/domains/';
|
|
774
1375
|
case 'DTEL':
|
|
775
|
-
return
|
|
1376
|
+
return '/sap/bc/adt/ddic/dataelements/';
|
|
1377
|
+
case 'MSAG':
|
|
1378
|
+
return '/sap/bc/adt/messageclass/';
|
|
1379
|
+
case 'DEVC':
|
|
1380
|
+
return '/sap/bc/adt/packages/';
|
|
776
1381
|
case 'TRAN':
|
|
777
|
-
return
|
|
1382
|
+
return '/sap/bc/adt/vit/wb/object_type/trant/object_name/';
|
|
778
1383
|
default:
|
|
779
|
-
return
|
|
1384
|
+
return '/sap/bc/adt/programs/programs/';
|
|
780
1385
|
}
|
|
781
1386
|
}
|
|
1387
|
+
/** Map object type + name to the ADT object URL used by CRUD/DevTools/etc. Name is URI-encoded. */
|
|
1388
|
+
function objectUrlForType(type, name) {
|
|
1389
|
+
return `${objectBasePath(type)}${encodeURIComponent(name)}`;
|
|
1390
|
+
}
|
|
1391
|
+
/** Infer SAP object type from naming conventions. Returns empty string if type cannot be determined. */
|
|
1392
|
+
function inferObjectType(name) {
|
|
1393
|
+
const upper = name.toUpperCase();
|
|
1394
|
+
if (upper.startsWith('IF_') || upper.startsWith('ZIF_') || upper.startsWith('YIF_'))
|
|
1395
|
+
return 'INTF';
|
|
1396
|
+
if (upper.startsWith('CL_') || upper.startsWith('ZCL_') || upper.startsWith('YCL_'))
|
|
1397
|
+
return 'CLAS';
|
|
1398
|
+
if (upper.startsWith('CX_') || upper.startsWith('ZCX_') || upper.startsWith('YCX_'))
|
|
1399
|
+
return 'CLAS';
|
|
1400
|
+
return '';
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Map object type + name to the ADT object URL WITHOUT encoding the name.
|
|
1404
|
+
* Used for API release state where the full URI is encoded as a single path segment by the caller.
|
|
1405
|
+
*/
|
|
1406
|
+
function objectUrlForTypeRaw(type, name) {
|
|
1407
|
+
return `${objectBasePath(type)}${name}`;
|
|
1408
|
+
}
|
|
782
1409
|
/** Get the source URL for an object (appends /source/main) */
|
|
783
1410
|
function sourceUrlForType(type, name) {
|
|
784
1411
|
return `${objectUrlForType(type, name)}/source/main`;
|
|
@@ -796,8 +1423,36 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
796
1423
|
}
|
|
797
1424
|
const objectUrl = objectUrlForType(type, name);
|
|
798
1425
|
const srcUrl = sourceUrlForType(type, name);
|
|
1426
|
+
// Helper: enforce allowedPackages for existing objects (update/delete/edit_method).
|
|
1427
|
+
// Only fetches metadata when package restrictions are configured — no extra HTTP call otherwise.
|
|
1428
|
+
async function enforcePackageForExistingObject() {
|
|
1429
|
+
if (client.safety.allowedPackages.length === 0)
|
|
1430
|
+
return undefined;
|
|
1431
|
+
const pkg = await client.resolveObjectPackage(objectUrl);
|
|
1432
|
+
if (pkg)
|
|
1433
|
+
checkPackage(client.safety, pkg);
|
|
1434
|
+
return pkg;
|
|
1435
|
+
}
|
|
799
1436
|
switch (action) {
|
|
800
1437
|
case 'update': {
|
|
1438
|
+
const existingPackage = await enforcePackageForExistingObject();
|
|
1439
|
+
if (isMetadataWriteType(type)) {
|
|
1440
|
+
// Metadata updates are full-XML-replace — we must fetch existing metadata
|
|
1441
|
+
// and merge with provided fields so omitted fields keep their current values.
|
|
1442
|
+
// Without this, updating just labels would reset dataType/typeKind to defaults.
|
|
1443
|
+
const metadataProps = getMetadataWriteProperties(args);
|
|
1444
|
+
const mergedProps = await mergeMetadataWriteProperties(client, type, name, metadataProps);
|
|
1445
|
+
const description = String(args.description ?? mergedProps._description ?? name);
|
|
1446
|
+
const pkg = String(args.package ?? existingPackage ?? mergedProps._package ?? '$TMP');
|
|
1447
|
+
const body = buildCreateXml(type, name, pkg, description, mergedProps);
|
|
1448
|
+
await safeUpdateObject(client.http, client.safety, objectUrl, body, vendorContentTypeForType(type), transport);
|
|
1449
|
+
cachingLayer?.invalidate(type, name);
|
|
1450
|
+
return textResult(`Successfully updated ${type} ${name}.`);
|
|
1451
|
+
}
|
|
1452
|
+
// CDS pre-write validation: reject unsupported syntax early
|
|
1453
|
+
const cdsGuardUpdate = guardCdsSyntax(type, source, cachedFeatures);
|
|
1454
|
+
if (cdsGuardUpdate)
|
|
1455
|
+
return cdsGuardUpdate;
|
|
801
1456
|
// Pre-write lint validation
|
|
802
1457
|
const lintWarnings = runPreWriteLint(source, type, name, config);
|
|
803
1458
|
if (lintWarnings.blocked)
|
|
@@ -811,7 +1466,44 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
811
1466
|
const pkg = String(args.package ?? '$TMP');
|
|
812
1467
|
checkPackage(client.safety, pkg);
|
|
813
1468
|
const description = String(args.description ?? name);
|
|
814
|
-
|
|
1469
|
+
// Pre-flight: check transport requirements for non-$TMP packages when no transport provided.
|
|
1470
|
+
// SAP requires a transport number for objects in transportable packages.
|
|
1471
|
+
// Instead of letting SAP return a cryptic error, we detect this early and return
|
|
1472
|
+
// an actionable error message guiding the LLM to use SAPTransport first.
|
|
1473
|
+
let effectiveTransport = transport;
|
|
1474
|
+
if (!transport && pkg.toUpperCase() !== '$TMP') {
|
|
1475
|
+
try {
|
|
1476
|
+
const transportInfo = await getTransportInfo(client.http, client.safety, objectUrl, pkg, 'I');
|
|
1477
|
+
if (transportInfo.lockedTransport) {
|
|
1478
|
+
// Object is already locked in a transport — use it automatically
|
|
1479
|
+
effectiveTransport = transportInfo.lockedTransport;
|
|
1480
|
+
}
|
|
1481
|
+
else if (!transportInfo.isLocal && transportInfo.recording) {
|
|
1482
|
+
// Transport IS required but none provided — return guidance
|
|
1483
|
+
const existingList = transportInfo.existingTransports.length > 0
|
|
1484
|
+
? `\n\nExisting transports for this package:\n${transportInfo.existingTransports
|
|
1485
|
+
.slice(0, 10)
|
|
1486
|
+
.map((t) => ` - ${t.id}: ${t.description} (${t.owner})`)
|
|
1487
|
+
.join('\n')}`
|
|
1488
|
+
: '';
|
|
1489
|
+
return errorResult(`Package "${pkg}" requires a transport number for object creation, but none was provided.\n\n` +
|
|
1490
|
+
`To fix this, either:\n` +
|
|
1491
|
+
`1. Use SAPTransport(action="list") to find an existing modifiable transport\n` +
|
|
1492
|
+
`2. Use SAPTransport(action="create", description="...") to create a new one\n` +
|
|
1493
|
+
`3. Then retry SAPWrite(action="create", ..., transport="<transport_id>")` +
|
|
1494
|
+
existingList);
|
|
1495
|
+
}
|
|
1496
|
+
// isLocal=true or recording=false → no transport needed, proceed without one
|
|
1497
|
+
}
|
|
1498
|
+
catch {
|
|
1499
|
+
// If transportInfo check fails (older system, permissions, etc.), proceed without it.
|
|
1500
|
+
// SAP will return its own error if a transport is actually needed.
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
// CDS pre-write validation: reject unsupported syntax early
|
|
1504
|
+
const cdsGuard = guardCdsSyntax(type, source, cachedFeatures);
|
|
1505
|
+
if (cdsGuard)
|
|
1506
|
+
return cdsGuard;
|
|
815
1507
|
// AFF header validation (if schema available for this type)
|
|
816
1508
|
const affResult = validateAffHeader(type, { description, originalLanguage: 'en' });
|
|
817
1509
|
if (!affResult.valid) {
|
|
@@ -820,10 +1512,52 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
820
1512
|
// Build type-specific creation XML body.
|
|
821
1513
|
// SAP ADT requires the root element to match the object type —
|
|
822
1514
|
// a generic objectReferences body returns 400 "System expected the element ...".
|
|
823
|
-
const
|
|
1515
|
+
const metadataProperties = getMetadataWriteProperties(args);
|
|
1516
|
+
const body = buildCreateXml(type, name, pkg, description, metadataProperties);
|
|
824
1517
|
// Step 1: Create the object (metadata only)
|
|
825
1518
|
const createUrl = objectUrl.replace(/\/[^/]+$/, ''); // parent collection URL
|
|
826
|
-
|
|
1519
|
+
// DOMA/DTEL/BDEF require vendor-specific content types; all other types use
|
|
1520
|
+
// 'application/*' — the wildcard lets the SAP server resolve the correct
|
|
1521
|
+
// handler (matching how ADT Eclipse and abap-adt-api send requests).
|
|
1522
|
+
const contentType = createContentTypeForType(type);
|
|
1523
|
+
const result = await createObject(client.http, client.safety, createUrl, body, contentType, effectiveTransport);
|
|
1524
|
+
if (isMetadataWriteType(type)) {
|
|
1525
|
+
// SAP's DTEL POST ignores labels, searchHelp, etc. — they require a follow-up PUT.
|
|
1526
|
+
// Use withStatefulSession directly (not safeUpdateObject) to keep the lock cycle
|
|
1527
|
+
// on the main client's session, avoiding lock contention with subsequent operations.
|
|
1528
|
+
if (type === 'DTEL' && dtelNeedsPostCreateUpdate(metadataProperties)) {
|
|
1529
|
+
const ct = vendorContentTypeForType(type);
|
|
1530
|
+
await client.http.withStatefulSession(async (session) => {
|
|
1531
|
+
const lock = await lockObject(session, client.safety, objectUrl);
|
|
1532
|
+
const lockTransport = effectiveTransport ?? (lock.corrNr || undefined);
|
|
1533
|
+
try {
|
|
1534
|
+
await updateObject(session, client.safety, objectUrl, body, lock.lockHandle, ct, lockTransport);
|
|
1535
|
+
}
|
|
1536
|
+
finally {
|
|
1537
|
+
await unlockObject(session, objectUrl, lock.lockHandle);
|
|
1538
|
+
}
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
// MSAG: POST creates empty container — follow-up PUT to write messages
|
|
1542
|
+
if (type === 'MSAG' && Array.isArray(metadataProperties.messages) && metadataProperties.messages.length > 0) {
|
|
1543
|
+
const ct = vendorContentTypeForType(type);
|
|
1544
|
+
await client.http.withStatefulSession(async (session) => {
|
|
1545
|
+
const lock = await lockObject(session, client.safety, objectUrl);
|
|
1546
|
+
const lockTransport = effectiveTransport ?? (lock.corrNr || undefined);
|
|
1547
|
+
try {
|
|
1548
|
+
await updateObject(session, client.safety, objectUrl, body, lock.lockHandle, ct, lockTransport);
|
|
1549
|
+
}
|
|
1550
|
+
finally {
|
|
1551
|
+
await unlockObject(session, objectUrl, lock.lockHandle);
|
|
1552
|
+
}
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
cachingLayer?.invalidate(type, name);
|
|
1556
|
+
const followUpHint = type === 'SRVB'
|
|
1557
|
+
? `\n\nNext steps:\n1. SAPActivate(type="SRVB", name="${name}")\n2. SAPActivate(action="publish_srvb", name="${name}")`
|
|
1558
|
+
: '';
|
|
1559
|
+
return textResult(`Created ${type} ${name} in package ${pkg}.\n${result}${followUpHint}`);
|
|
1560
|
+
}
|
|
827
1561
|
// Step 2: Write source code if provided
|
|
828
1562
|
if (source) {
|
|
829
1563
|
// Pre-write lint validation
|
|
@@ -831,7 +1565,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
831
1565
|
if (lintWarnings.blocked) {
|
|
832
1566
|
return textResult(`Created ${type} ${name} in package ${pkg}, but source was rejected by lint:\n${lintWarnings.result.content[0].text}`);
|
|
833
1567
|
}
|
|
834
|
-
await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, source,
|
|
1568
|
+
await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, source, effectiveTransport);
|
|
835
1569
|
cachingLayer?.invalidate(type, name);
|
|
836
1570
|
const msg = `Created ${type} ${name} in package ${pkg} and wrote source code.`;
|
|
837
1571
|
return lintWarnings.warnings ? textResult(`${msg}\n\n${lintWarnings.warnings}`) : textResult(msg);
|
|
@@ -846,6 +1580,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
846
1580
|
return errorResult('"source" (new method body) is required for edit_method action.');
|
|
847
1581
|
if (type !== 'CLAS')
|
|
848
1582
|
return errorResult('edit_method is only supported for type=CLAS.');
|
|
1583
|
+
await enforcePackageForExistingObject();
|
|
849
1584
|
// Fetch current full source (use cache if available)
|
|
850
1585
|
const currentSource = cachingLayer
|
|
851
1586
|
? (await cachingLayer.getSource('CLAS', name, () => client.getClass(name))).source
|
|
@@ -870,11 +1605,13 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
870
1605
|
return lintWarnings.warnings ? textResult(`${msg}\n\n${lintWarnings.warnings}`) : textResult(msg);
|
|
871
1606
|
}
|
|
872
1607
|
case 'delete': {
|
|
873
|
-
|
|
1608
|
+
await enforcePackageForExistingObject();
|
|
1609
|
+
// Lock, delete, unlock pattern — auto-propagate lock corrNr if no explicit transport
|
|
874
1610
|
await client.http.withStatefulSession(async (session) => {
|
|
875
1611
|
const lock = await lockObject(session, client.safety, objectUrl);
|
|
1612
|
+
const effectiveTransport = transport ?? (lock.corrNr || undefined);
|
|
876
1613
|
try {
|
|
877
|
-
await deleteObject(session, client.safety, objectUrl, lock.lockHandle,
|
|
1614
|
+
await deleteObject(session, client.safety, objectUrl, lock.lockHandle, effectiveTransport);
|
|
878
1615
|
}
|
|
879
1616
|
finally {
|
|
880
1617
|
try {
|
|
@@ -896,10 +1633,41 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
896
1633
|
const pkg = String(args.package ?? '$TMP');
|
|
897
1634
|
// Check package is allowed before starting any creates
|
|
898
1635
|
checkPackage(client.safety, pkg);
|
|
1636
|
+
// Pre-flight transport check for batch_create (same logic as single create)
|
|
1637
|
+
let batchTransport = transport;
|
|
1638
|
+
if (!transport && pkg.toUpperCase() !== '$TMP') {
|
|
1639
|
+
try {
|
|
1640
|
+
// Use first object's URL for the transport check
|
|
1641
|
+
const firstObj = objects[0];
|
|
1642
|
+
const firstUrl = objectUrlForType(String(firstObj.type ?? ''), String(firstObj.name ?? ''));
|
|
1643
|
+
const transportInfo = await getTransportInfo(client.http, client.safety, firstUrl, pkg, 'I');
|
|
1644
|
+
if (transportInfo.lockedTransport) {
|
|
1645
|
+
batchTransport = transportInfo.lockedTransport;
|
|
1646
|
+
}
|
|
1647
|
+
else if (!transportInfo.isLocal && transportInfo.recording) {
|
|
1648
|
+
const existingList = transportInfo.existingTransports.length > 0
|
|
1649
|
+
? `\n\nExisting transports for this package:\n${transportInfo.existingTransports
|
|
1650
|
+
.slice(0, 10)
|
|
1651
|
+
.map((t) => ` - ${t.id}: ${t.description} (${t.owner})`)
|
|
1652
|
+
.join('\n')}`
|
|
1653
|
+
: '';
|
|
1654
|
+
return errorResult(`Package "${pkg}" requires a transport number for object creation, but none was provided.\n\n` +
|
|
1655
|
+
`To fix this, either:\n` +
|
|
1656
|
+
`1. Use SAPTransport(action="list") to find an existing modifiable transport\n` +
|
|
1657
|
+
`2. Use SAPTransport(action="create", description="...") to create a new one\n` +
|
|
1658
|
+
`3. Then retry SAPWrite(action="batch_create", ..., transport="<transport_id>")` +
|
|
1659
|
+
existingList);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
catch {
|
|
1663
|
+
// If transportInfo check fails, proceed — SAP will return its own error if needed.
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
899
1666
|
const results = [];
|
|
900
1667
|
for (const obj of objects) {
|
|
901
1668
|
const objType = String(obj.type ?? '');
|
|
902
1669
|
const objName = String(obj.name ?? '');
|
|
1670
|
+
const metadataObject = isMetadataWriteType(objType);
|
|
903
1671
|
const objSource = obj.source ? String(obj.source) : undefined;
|
|
904
1672
|
const objDescription = String(obj.description ?? objName);
|
|
905
1673
|
// AFF header validation per object (if schema available)
|
|
@@ -914,8 +1682,9 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
914
1682
|
break;
|
|
915
1683
|
}
|
|
916
1684
|
try {
|
|
917
|
-
// Pre-validate source with lint BEFORE creating the object to avoid orphaned objects
|
|
918
|
-
|
|
1685
|
+
// Pre-validate source with lint BEFORE creating the object to avoid orphaned objects.
|
|
1686
|
+
// Metadata objects (DOMA/DTEL) are XML-only and intentionally skip source lint.
|
|
1687
|
+
if (!metadataObject && objSource) {
|
|
919
1688
|
const lintWarnings = runPreWriteLint(objSource, objType, objName, config);
|
|
920
1689
|
if (lintWarnings.blocked) {
|
|
921
1690
|
results.push({
|
|
@@ -930,12 +1699,27 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
930
1699
|
// Step 1: Create the object
|
|
931
1700
|
const objUrl = objectUrlForType(objType, objName);
|
|
932
1701
|
const createUrl = objUrl.replace(/\/[^/]+$/, '');
|
|
933
|
-
const
|
|
934
|
-
|
|
1702
|
+
const objMetadataProps = getMetadataWriteProperties(obj);
|
|
1703
|
+
const body = buildCreateXml(objType, objName, pkg, objDescription, objMetadataProps);
|
|
1704
|
+
const contentType = createContentTypeForType(objType);
|
|
1705
|
+
await createObject(client.http, client.safety, createUrl, body, contentType, batchTransport);
|
|
1706
|
+
// Step 1b: DTEL POST ignores labels — follow up with PUT on main session
|
|
1707
|
+
if (objType === 'DTEL' && dtelNeedsPostCreateUpdate(objMetadataProps)) {
|
|
1708
|
+
await client.http.withStatefulSession(async (session) => {
|
|
1709
|
+
const lock = await lockObject(session, client.safety, objUrl);
|
|
1710
|
+
const lockTransport = batchTransport ?? (lock.corrNr || undefined);
|
|
1711
|
+
try {
|
|
1712
|
+
await updateObject(session, client.safety, objUrl, body, lock.lockHandle, contentType, lockTransport);
|
|
1713
|
+
}
|
|
1714
|
+
finally {
|
|
1715
|
+
await unlockObject(session, objUrl, lock.lockHandle);
|
|
1716
|
+
}
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
935
1719
|
// Step 2: Write source if provided
|
|
936
|
-
if (objSource) {
|
|
1720
|
+
if (!metadataObject && objSource) {
|
|
937
1721
|
const srcUrl = sourceUrlForType(objType, objName);
|
|
938
|
-
await safeUpdateSource(client.http, client.safety, objUrl, srcUrl, objSource,
|
|
1722
|
+
await safeUpdateSource(client.http, client.safety, objUrl, srcUrl, objSource, batchTransport);
|
|
939
1723
|
}
|
|
940
1724
|
// Step 3: Activate the object
|
|
941
1725
|
const activationResult = await activate(client.http, client.safety, objUrl);
|
|
@@ -1039,29 +1823,128 @@ function runPreWriteLint(source, type, name, config) {
|
|
|
1039
1823
|
}
|
|
1040
1824
|
// ─── SAPActivate Handler ─────────────────────────────────────────────
|
|
1041
1825
|
async function handleSAPActivate(client, args) {
|
|
1042
|
-
const
|
|
1826
|
+
const action = String(args.action ?? 'activate');
|
|
1827
|
+
const name = String(args.name ?? '');
|
|
1828
|
+
const version = String(args.version ?? '0001');
|
|
1829
|
+
// Publish service binding
|
|
1830
|
+
if (action === 'publish_srvb') {
|
|
1831
|
+
if (!name) {
|
|
1832
|
+
return errorResult('Missing required "name" parameter for publish_srvb action.');
|
|
1833
|
+
}
|
|
1834
|
+
const result = await publishServiceBinding(client.http, client.safety, name, version);
|
|
1835
|
+
if (result.severity === 'ERROR') {
|
|
1836
|
+
return errorResult(`Failed to publish service binding ${name}: ${result.shortText}${result.longText ? ` — ${result.longText}` : ''}`);
|
|
1837
|
+
}
|
|
1838
|
+
let srvbInfo;
|
|
1839
|
+
try {
|
|
1840
|
+
srvbInfo = await client.getSrvb(name);
|
|
1841
|
+
}
|
|
1842
|
+
catch {
|
|
1843
|
+
if (result.severity === 'UNKNOWN') {
|
|
1844
|
+
return errorResult(`Publish response for ${name} could not be parsed and readback failed — use SAPRead to verify publish status.`);
|
|
1845
|
+
}
|
|
1846
|
+
return textResult(`Successfully published service binding ${name} (readback of binding metadata failed — use SAPRead to verify)`);
|
|
1847
|
+
}
|
|
1848
|
+
// Verify the published flag from the SRVB readback
|
|
1849
|
+
try {
|
|
1850
|
+
const srvbData = JSON.parse(srvbInfo);
|
|
1851
|
+
if (srvbData.published === false) {
|
|
1852
|
+
return errorResult(`Publish of service binding ${name} may have failed — binding is still unpublished.\n\n${srvbInfo}`);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
catch {
|
|
1856
|
+
// If we can't parse the readback JSON, fall through — better to return what we have
|
|
1857
|
+
}
|
|
1858
|
+
if (result.severity === 'UNKNOWN') {
|
|
1859
|
+
return textResult(`Publish request for ${name} completed but response could not be fully parsed. Verify status below:\n\n${srvbInfo}`);
|
|
1860
|
+
}
|
|
1861
|
+
return textResult(`Successfully published service binding ${name}.\n\n${srvbInfo}`);
|
|
1862
|
+
}
|
|
1863
|
+
// Unpublish service binding
|
|
1864
|
+
if (action === 'unpublish_srvb') {
|
|
1865
|
+
if (!name) {
|
|
1866
|
+
return errorResult('Missing required "name" parameter for unpublish_srvb action.');
|
|
1867
|
+
}
|
|
1868
|
+
const result = await unpublishServiceBinding(client.http, client.safety, name, version);
|
|
1869
|
+
if (result.severity === 'ERROR') {
|
|
1870
|
+
return errorResult(`Failed to unpublish service binding ${name}: ${result.shortText}${result.longText ? ` — ${result.longText}` : ''}`);
|
|
1871
|
+
}
|
|
1872
|
+
let srvbInfo;
|
|
1873
|
+
try {
|
|
1874
|
+
srvbInfo = await client.getSrvb(name);
|
|
1875
|
+
}
|
|
1876
|
+
catch {
|
|
1877
|
+
// Readback failed — fall through with what we have
|
|
1878
|
+
}
|
|
1879
|
+
// Verify the published flag from the SRVB readback
|
|
1880
|
+
if (srvbInfo) {
|
|
1881
|
+
try {
|
|
1882
|
+
const srvbData = JSON.parse(srvbInfo);
|
|
1883
|
+
if (srvbData.published === true) {
|
|
1884
|
+
return errorResult(`Unpublish of service binding ${name} may have failed — binding is still published.\n\n${srvbInfo}`);
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
catch {
|
|
1888
|
+
// If we can't parse the readback JSON, fall through
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
if (result.severity === 'UNKNOWN') {
|
|
1892
|
+
return textResult(`Unpublish request for ${name} completed but response could not be fully parsed.${srvbInfo ? ` Verify status below:\n\n${srvbInfo}` : ' Use SAPRead to verify status.'}`);
|
|
1893
|
+
}
|
|
1894
|
+
return textResult(`Successfully unpublished service binding ${name}.${srvbInfo ? `\n\n${srvbInfo}` : ''}`);
|
|
1895
|
+
}
|
|
1043
1896
|
// Batch activation: multiple objects at once (for RAP stacks etc.)
|
|
1897
|
+
const type = String(args.type ?? '');
|
|
1898
|
+
const preaudit = args.preaudit !== undefined ? Boolean(args.preaudit) : undefined;
|
|
1899
|
+
const activateOpts = preaudit !== undefined ? { preaudit } : undefined;
|
|
1044
1900
|
if (args.objects && Array.isArray(args.objects)) {
|
|
1045
1901
|
const objects = args.objects.map((o) => {
|
|
1046
1902
|
const objType = String(o.type ?? type);
|
|
1047
1903
|
const objName = String(o.name ?? '');
|
|
1048
1904
|
return { url: objectUrlForType(objType, objName), name: objName };
|
|
1049
1905
|
});
|
|
1050
|
-
const result = await activateBatch(client.http, client.safety, objects);
|
|
1906
|
+
const result = await activateBatch(client.http, client.safety, objects, activateOpts);
|
|
1051
1907
|
const names = objects.map((o) => o.name).join(', ');
|
|
1052
1908
|
if (result.success) {
|
|
1053
|
-
return textResult(`Successfully activated ${objects.length} objects: ${names}.${result
|
|
1909
|
+
return textResult(`Successfully activated ${objects.length} objects: ${names}.${formatActivationMessages(result)}`);
|
|
1054
1910
|
}
|
|
1055
|
-
return errorResult(`Batch activation failed for: ${names}.\
|
|
1911
|
+
return errorResult(`Batch activation failed for: ${names}.\n${formatActivationMessages(result)}`);
|
|
1056
1912
|
}
|
|
1057
1913
|
// Single activation (existing behavior)
|
|
1058
|
-
const name = String(args.name ?? '');
|
|
1059
1914
|
const objectUrl = objectUrlForType(type, name);
|
|
1060
|
-
const result = await activate(client.http, client.safety, objectUrl);
|
|
1915
|
+
const result = await activate(client.http, client.safety, objectUrl, activateOpts);
|
|
1061
1916
|
if (result.success) {
|
|
1062
|
-
return textResult(`Successfully activated ${type} ${name}.${result
|
|
1917
|
+
return textResult(`Successfully activated ${type} ${name}.${formatActivationMessages(result)}`);
|
|
1918
|
+
}
|
|
1919
|
+
return errorResult(`Activation failed for ${type} ${name}.\n${formatActivationMessages(result)}`);
|
|
1920
|
+
}
|
|
1921
|
+
/** Format activation result messages with structured detail (line numbers, URIs) when available */
|
|
1922
|
+
function formatActivationMessages(result) {
|
|
1923
|
+
if (result.details.length === 0)
|
|
1924
|
+
return '';
|
|
1925
|
+
const errors = result.details.filter((d) => d.severity === 'error');
|
|
1926
|
+
const warnings = result.details.filter((d) => d.severity === 'warning');
|
|
1927
|
+
const parts = [];
|
|
1928
|
+
if (errors.length > 0) {
|
|
1929
|
+
const formatted = errors.map((e) => {
|
|
1930
|
+
const prefix = e.line ? `[line ${e.line}] ` : '';
|
|
1931
|
+
const suffix = e.uri ? ` (${e.uri})` : '';
|
|
1932
|
+
return `- ${prefix}${e.text}${suffix}`;
|
|
1933
|
+
});
|
|
1934
|
+
parts.push(`Errors:\n${formatted.join('\n')}`);
|
|
1935
|
+
}
|
|
1936
|
+
if (warnings.length > 0) {
|
|
1937
|
+
const formatted = warnings.map((w) => {
|
|
1938
|
+
const prefix = w.line ? `[line ${w.line}] ` : '';
|
|
1939
|
+
return `- ${prefix}${w.text}`;
|
|
1940
|
+
});
|
|
1941
|
+
parts.push(`Warnings:\n${formatted.join('\n')}`);
|
|
1942
|
+
}
|
|
1943
|
+
// Fall back to flat messages if no errors/warnings but info messages exist
|
|
1944
|
+
if (parts.length === 0 && result.messages.length > 0) {
|
|
1945
|
+
return `\nMessages: ${result.messages.join('; ')}`;
|
|
1063
1946
|
}
|
|
1064
|
-
return
|
|
1947
|
+
return parts.length > 0 ? `\n${parts.join('\n')}` : '';
|
|
1065
1948
|
}
|
|
1066
1949
|
// ─── SAPNavigate Handler ─────────────────────────────────────────────
|
|
1067
1950
|
async function handleSAPNavigate(client, args) {
|
|
@@ -1137,8 +2020,63 @@ async function handleSAPNavigate(client, args) {
|
|
|
1137
2020
|
const proposals = await getCompletion(client.http, client.safety, uri, line, column, source);
|
|
1138
2021
|
return textResult(JSON.stringify(proposals, null, 2));
|
|
1139
2022
|
}
|
|
2023
|
+
case 'hierarchy': {
|
|
2024
|
+
const className = String(args.name ?? '').toUpperCase();
|
|
2025
|
+
if (!className) {
|
|
2026
|
+
return errorResult('Provide name (class name) for hierarchy lookup.');
|
|
2027
|
+
}
|
|
2028
|
+
// Sanitize to prevent SQL injection — class names are alphanumeric + underscore + namespace slash
|
|
2029
|
+
const safeName = className.replace(/[^A-Z0-9_/]/g, '');
|
|
2030
|
+
if (safeName !== className) {
|
|
2031
|
+
return errorResult(`Invalid class name: "${className}". Only alphanumeric characters, underscores, and slashes are allowed.`);
|
|
2032
|
+
}
|
|
2033
|
+
const canFreeSQL = isOperationAllowed(client.safety, OperationType.FreeSQL);
|
|
2034
|
+
const canQuery = isOperationAllowed(client.safety, OperationType.Query);
|
|
2035
|
+
if (!canFreeSQL && !canQuery) {
|
|
2036
|
+
return errorResult('Class hierarchy requires data access permissions. ' +
|
|
2037
|
+
'Enable free SQL (--block-free-sql=false) or table preview (--block-data=false).');
|
|
2038
|
+
}
|
|
2039
|
+
try {
|
|
2040
|
+
let ownRels;
|
|
2041
|
+
let subRels;
|
|
2042
|
+
if (canFreeSQL) {
|
|
2043
|
+
ownRels = await client.runQuery(`SELECT CLSNAME, REFCLSNAME, RELTYPE FROM SEOMETAREL WHERE CLSNAME = '${safeName}'`, 100);
|
|
2044
|
+
subRels = await client.runQuery(`SELECT CLSNAME FROM SEOMETAREL WHERE REFCLSNAME = '${safeName}' AND RELTYPE = '2'`, 100);
|
|
2045
|
+
}
|
|
2046
|
+
else {
|
|
2047
|
+
// Fall back to named table preview (Query op type)
|
|
2048
|
+
ownRels = await client.getTableContents('SEOMETAREL', 100, `CLSNAME = '${safeName}'`);
|
|
2049
|
+
subRels = await client.getTableContents('SEOMETAREL', 100, `REFCLSNAME = '${safeName}' AND RELTYPE = '2'`);
|
|
2050
|
+
}
|
|
2051
|
+
let superclass = null;
|
|
2052
|
+
const interfaces = [];
|
|
2053
|
+
for (let i = 0; i < ownRels.rows.length; i++) {
|
|
2054
|
+
const row = ownRels.rows[i];
|
|
2055
|
+
const reltype = String(row.RELTYPE ?? '').trim();
|
|
2056
|
+
const refName = String(row.REFCLSNAME ?? '').trim();
|
|
2057
|
+
if (reltype === '2') {
|
|
2058
|
+
superclass = refName;
|
|
2059
|
+
}
|
|
2060
|
+
else if (reltype === '1') {
|
|
2061
|
+
interfaces.push(refName);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
const subclasses = [];
|
|
2065
|
+
for (let i = 0; i < subRels.rows.length; i++) {
|
|
2066
|
+
subclasses.push(String(subRels.rows[i].CLSNAME ?? '').trim());
|
|
2067
|
+
}
|
|
2068
|
+
const result = { className: safeName, superclass, interfaces, subclasses };
|
|
2069
|
+
return textResult(JSON.stringify(result, null, 2));
|
|
2070
|
+
}
|
|
2071
|
+
catch (err) {
|
|
2072
|
+
if (err instanceof AdtApiError && err.statusCode === 404) {
|
|
2073
|
+
return errorResult('Cannot query SEOMETAREL — table may not be accessible on this system.');
|
|
2074
|
+
}
|
|
2075
|
+
throw err;
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
1140
2078
|
default:
|
|
1141
|
-
return errorResult(`Unknown SAPNavigate action: ${action}. Supported: definition, references, completion`);
|
|
2079
|
+
return errorResult(`Unknown SAPNavigate action: ${action}. Supported: definition, references, completion, hierarchy`);
|
|
1142
2080
|
}
|
|
1143
2081
|
}
|
|
1144
2082
|
// ─── SAPDiagnose Handler ─────────────────────────────────────────────
|
|
@@ -1211,8 +2149,9 @@ async function handleSAPTransport(client, args) {
|
|
|
1211
2149
|
const action = String(args.action ?? '');
|
|
1212
2150
|
switch (action) {
|
|
1213
2151
|
case 'list': {
|
|
1214
|
-
const user = args.user;
|
|
1215
|
-
const
|
|
2152
|
+
const user = args.user || client.username;
|
|
2153
|
+
const status = args.status ?? 'D';
|
|
2154
|
+
const transports = await listTransports(client.http, client.safety, user, status === '*' ? undefined : status);
|
|
1216
2155
|
return textResult(JSON.stringify(transports, null, 2));
|
|
1217
2156
|
}
|
|
1218
2157
|
case 'get': {
|
|
@@ -1228,7 +2167,10 @@ async function handleSAPTransport(client, args) {
|
|
|
1228
2167
|
const description = String(args.description ?? '');
|
|
1229
2168
|
if (!description)
|
|
1230
2169
|
return errorResult('Description is required for "create" action.');
|
|
1231
|
-
const
|
|
2170
|
+
const transportType = String(args.type ?? 'K');
|
|
2171
|
+
const id = await createTransport(client.http, client.safety, description, undefined, transportType);
|
|
2172
|
+
if (!id)
|
|
2173
|
+
return errorResult('Transport creation succeeded but no transport ID was returned. Check the SAP system manually.');
|
|
1232
2174
|
return textResult(`Created transport request: ${id}`);
|
|
1233
2175
|
}
|
|
1234
2176
|
case 'release': {
|
|
@@ -1238,8 +2180,61 @@ async function handleSAPTransport(client, args) {
|
|
|
1238
2180
|
await releaseTransport(client.http, client.safety, id);
|
|
1239
2181
|
return textResult(`Released transport request: ${id}`);
|
|
1240
2182
|
}
|
|
2183
|
+
case 'delete': {
|
|
2184
|
+
const id = String(args.id ?? '');
|
|
2185
|
+
if (!id)
|
|
2186
|
+
return errorResult('Transport ID is required for "delete" action.');
|
|
2187
|
+
const recursive = Boolean(args.recursive ?? false);
|
|
2188
|
+
await deleteTransport(client.http, client.safety, id, recursive);
|
|
2189
|
+
return textResult(`Deleted transport request: ${id}${recursive ? ' (recursive)' : ''}`);
|
|
2190
|
+
}
|
|
2191
|
+
case 'reassign': {
|
|
2192
|
+
const id = String(args.id ?? '');
|
|
2193
|
+
if (!id)
|
|
2194
|
+
return errorResult('Transport ID is required for "reassign" action.');
|
|
2195
|
+
const owner = String(args.owner ?? '');
|
|
2196
|
+
if (!owner)
|
|
2197
|
+
return errorResult('Owner is required for "reassign" action.');
|
|
2198
|
+
const recursive = Boolean(args.recursive ?? false);
|
|
2199
|
+
await reassignTransport(client.http, client.safety, id, owner, recursive);
|
|
2200
|
+
return textResult(`Reassigned transport ${id} to ${owner}${recursive ? ' (recursive)' : ''}`);
|
|
2201
|
+
}
|
|
2202
|
+
case 'release_recursive': {
|
|
2203
|
+
const id = String(args.id ?? '');
|
|
2204
|
+
if (!id)
|
|
2205
|
+
return errorResult('Transport ID is required for "release_recursive" action.');
|
|
2206
|
+
const result = await releaseTransportRecursive(client.http, client.safety, id);
|
|
2207
|
+
return textResult(JSON.stringify(result, null, 2));
|
|
2208
|
+
}
|
|
2209
|
+
case 'check': {
|
|
2210
|
+
// Check transport requirements for an object/package combination.
|
|
2211
|
+
// Does NOT require enableTransports — this is a read-only check.
|
|
2212
|
+
const objectType = String(args.type ?? '');
|
|
2213
|
+
const objectName = String(args.name ?? '');
|
|
2214
|
+
const pkg = String(args.package ?? '');
|
|
2215
|
+
if (!objectType || !objectName)
|
|
2216
|
+
return errorResult('"type" and "name" are required for "check" action.');
|
|
2217
|
+
if (!pkg)
|
|
2218
|
+
return errorResult('"package" is required for "check" action.');
|
|
2219
|
+
const objectUrl = objectUrlForType(objectType, objectName);
|
|
2220
|
+
const info = await getTransportInfo(client.http, client.safety, objectUrl, pkg, 'I');
|
|
2221
|
+
const summary = info.isLocal
|
|
2222
|
+
? `Package "${pkg}" is local — no transport required.`
|
|
2223
|
+
: info.recording
|
|
2224
|
+
? `Package "${pkg}" requires a transport for object creation.`
|
|
2225
|
+
: `Package "${pkg}" does not require transport recording.`;
|
|
2226
|
+
return textResult(JSON.stringify({
|
|
2227
|
+
package: pkg,
|
|
2228
|
+
transportRequired: !info.isLocal && info.recording,
|
|
2229
|
+
isLocal: info.isLocal,
|
|
2230
|
+
deliveryUnit: info.deliveryUnit,
|
|
2231
|
+
existingTransports: info.existingTransports,
|
|
2232
|
+
...(info.lockedTransport ? { lockedTransport: info.lockedTransport } : {}),
|
|
2233
|
+
summary,
|
|
2234
|
+
}, null, 2));
|
|
2235
|
+
}
|
|
1241
2236
|
default:
|
|
1242
|
-
return errorResult(`Unknown SAPTransport action: ${action}. Supported: list, get, create, release`);
|
|
2237
|
+
return errorResult(`Unknown SAPTransport action: ${action}. Supported: list, get, create, release, delete, reassign, release_recursive, check`);
|
|
1243
2238
|
}
|
|
1244
2239
|
}
|
|
1245
2240
|
// ─── SAPContext Handler ───────────────────────────────────────────────
|
|
@@ -1354,6 +2349,7 @@ async function handleSAPContext(client, args, cachingLayer) {
|
|
|
1354
2349
|
let cachedFeatures;
|
|
1355
2350
|
async function handleSAPManage(client, config, args, cachingLayer, isPerUserClient) {
|
|
1356
2351
|
const action = String(args.action ?? '');
|
|
2352
|
+
const flpUnavailableMessage = 'FLP customization service (PAGE_BUILDER_CUST) is not available on this system. Check ICF service activation in SICF.';
|
|
1357
2353
|
switch (action) {
|
|
1358
2354
|
case 'features': {
|
|
1359
2355
|
if (!cachedFeatures) {
|
|
@@ -1361,6 +2357,208 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
|
|
|
1361
2357
|
}
|
|
1362
2358
|
return textResult(JSON.stringify(cachedFeatures, null, 2));
|
|
1363
2359
|
}
|
|
2360
|
+
case 'create_package': {
|
|
2361
|
+
const name = String(args.name ?? '').trim();
|
|
2362
|
+
const description = String(args.description ?? '').trim();
|
|
2363
|
+
const superPackage = String(args.superPackage ?? '').trim();
|
|
2364
|
+
const softwareComponent = String(args.softwareComponent ?? '').trim();
|
|
2365
|
+
const transportLayer = String(args.transportLayer ?? '').trim();
|
|
2366
|
+
const transport = String(args.transport ?? '').trim();
|
|
2367
|
+
if (!name)
|
|
2368
|
+
return errorResult('"name" is required for create_package action.');
|
|
2369
|
+
if (!description)
|
|
2370
|
+
return errorResult('"description" is required for create_package action.');
|
|
2371
|
+
checkOperation(client.safety, OperationType.Create, 'CreatePackage');
|
|
2372
|
+
// Package allowlist is enforced on the parent package, not the new package name.
|
|
2373
|
+
// This enables creating children in allowed parents like $TMP.
|
|
2374
|
+
if (superPackage) {
|
|
2375
|
+
checkPackage(client.safety, superPackage);
|
|
2376
|
+
}
|
|
2377
|
+
let effectiveTransport = transport || undefined;
|
|
2378
|
+
const packageUrl = `/sap/bc/adt/packages/${encodeURIComponent(name)}`;
|
|
2379
|
+
// Transport pre-flight for non-local parent packages when no transport is provided.
|
|
2380
|
+
if (!effectiveTransport && superPackage && superPackage.toUpperCase() !== '$TMP') {
|
|
2381
|
+
try {
|
|
2382
|
+
const transportInfo = await getTransportInfo(client.http, client.safety, packageUrl, superPackage, 'I');
|
|
2383
|
+
if (transportInfo.lockedTransport) {
|
|
2384
|
+
effectiveTransport = transportInfo.lockedTransport;
|
|
2385
|
+
}
|
|
2386
|
+
else if (!transportInfo.isLocal && transportInfo.recording) {
|
|
2387
|
+
const existingList = transportInfo.existingTransports.length > 0
|
|
2388
|
+
? `\n\nExisting transports for this package:\n${transportInfo.existingTransports
|
|
2389
|
+
.slice(0, 10)
|
|
2390
|
+
.map((t) => ` - ${t.id}: ${t.description} (${t.owner})`)
|
|
2391
|
+
.join('\n')}`
|
|
2392
|
+
: '';
|
|
2393
|
+
return errorResult(`Package "${superPackage}" requires a transport number for package creation, but none was provided.\n\n` +
|
|
2394
|
+
`To fix this, either:\n` +
|
|
2395
|
+
`1. Use SAPTransport(action="list") to find an existing modifiable transport\n` +
|
|
2396
|
+
`2. Use SAPTransport(action="create", description="...") to create a new one\n` +
|
|
2397
|
+
`3. Then retry SAPManage(action="create_package", ..., transport="<transport_id>")` +
|
|
2398
|
+
existingList);
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
catch {
|
|
2402
|
+
// Graceful fallback: let SAP enforce transport requirements if the pre-check fails.
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
const packageTypeRaw = String(args.packageType ?? '').trim();
|
|
2406
|
+
const packageType = packageTypeRaw === 'development' || packageTypeRaw === 'structure' || packageTypeRaw === 'main'
|
|
2407
|
+
? packageTypeRaw
|
|
2408
|
+
: undefined;
|
|
2409
|
+
const xml = buildPackageXml({
|
|
2410
|
+
name,
|
|
2411
|
+
description,
|
|
2412
|
+
superPackage: superPackage || undefined,
|
|
2413
|
+
softwareComponent: softwareComponent || undefined,
|
|
2414
|
+
transportLayer: transportLayer || undefined,
|
|
2415
|
+
packageType,
|
|
2416
|
+
});
|
|
2417
|
+
await createObject(client.http, client.safety, '/sap/bc/adt/packages', xml, 'application/*', effectiveTransport);
|
|
2418
|
+
return textResult(`Created package ${name}.`);
|
|
2419
|
+
}
|
|
2420
|
+
case 'delete_package': {
|
|
2421
|
+
const name = String(args.name ?? '').trim();
|
|
2422
|
+
const transport = String(args.transport ?? '').trim();
|
|
2423
|
+
if (!name)
|
|
2424
|
+
return errorResult('"name" is required for delete_package action.');
|
|
2425
|
+
checkOperation(client.safety, OperationType.Delete, 'DeletePackage');
|
|
2426
|
+
const packageUrl = `/sap/bc/adt/packages/${encodeURIComponent(name)}`;
|
|
2427
|
+
await client.http.withStatefulSession(async (session) => {
|
|
2428
|
+
const lock = await lockObject(session, client.safety, packageUrl);
|
|
2429
|
+
const effectiveTransport = transport || lock.corrNr || undefined;
|
|
2430
|
+
try {
|
|
2431
|
+
await deleteObject(session, client.safety, packageUrl, lock.lockHandle, effectiveTransport);
|
|
2432
|
+
}
|
|
2433
|
+
finally {
|
|
2434
|
+
try {
|
|
2435
|
+
await unlockObject(session, packageUrl, lock.lockHandle);
|
|
2436
|
+
}
|
|
2437
|
+
catch {
|
|
2438
|
+
// Object may already be deleted — unlock failure is expected.
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
});
|
|
2442
|
+
return textResult(`Deleted package ${name}.`);
|
|
2443
|
+
}
|
|
2444
|
+
case 'flp_list_catalogs': {
|
|
2445
|
+
const catalogs = await listCatalogs(client.http, client.safety);
|
|
2446
|
+
const customCount = catalogs.filter((c) => /^(Z|Y)/i.test(c.domainId)).length;
|
|
2447
|
+
const lines = [
|
|
2448
|
+
`${catalogs.length} catalogs (${customCount} custom Z/Y). Columns: domainId | title | type | scope | chips`,
|
|
2449
|
+
...catalogs.map((c) => `${c.domainId} | ${c.title || '(no title)'} | ${c.type || '-'} | ${c.scope || '-'} | ${c.chipCount}`),
|
|
2450
|
+
];
|
|
2451
|
+
return textResult(lines.join('\n'));
|
|
2452
|
+
}
|
|
2453
|
+
case 'flp_list_groups': {
|
|
2454
|
+
const groups = await listGroups(client.http, client.safety);
|
|
2455
|
+
const lines = [
|
|
2456
|
+
`${groups.length} groups. Columns: id | title`,
|
|
2457
|
+
...groups.map((g) => `${g.id} | ${g.title || '(no title)'}`),
|
|
2458
|
+
];
|
|
2459
|
+
return textResult(lines.join('\n'));
|
|
2460
|
+
}
|
|
2461
|
+
case 'flp_list_tiles': {
|
|
2462
|
+
const catalogId = String(args.catalogId ?? '');
|
|
2463
|
+
if (!catalogId)
|
|
2464
|
+
return errorResult('"catalogId" is required for flp_list_tiles action.');
|
|
2465
|
+
const result = await listTiles(client.http, client.safety, catalogId);
|
|
2466
|
+
if (result.backendError) {
|
|
2467
|
+
return textResult(`⚠ Backend error for catalog "${catalogId}": ${result.backendError}\n\nReturned 0 tiles.`);
|
|
2468
|
+
}
|
|
2469
|
+
const lines = [
|
|
2470
|
+
`${result.tiles.length} tiles in catalog "${catalogId}". Columns: instanceId | title | chipId | semanticObject | semanticAction`,
|
|
2471
|
+
...result.tiles.map((t) => {
|
|
2472
|
+
const so = t.configuration?.semantic_object ?? '';
|
|
2473
|
+
const sa = t.configuration?.semantic_action ?? '';
|
|
2474
|
+
return `${t.instanceId} | ${t.title || '(no title)'} | ${t.chipId} | ${so} | ${sa}`;
|
|
2475
|
+
}),
|
|
2476
|
+
];
|
|
2477
|
+
return textResult(lines.join('\n'));
|
|
2478
|
+
}
|
|
2479
|
+
case 'flp_create_catalog': {
|
|
2480
|
+
if (cachedFeatures?.flp && !cachedFeatures.flp.available) {
|
|
2481
|
+
return errorResult(flpUnavailableMessage);
|
|
2482
|
+
}
|
|
2483
|
+
const domainId = String(args.domainId ?? '');
|
|
2484
|
+
const title = String(args.title ?? '');
|
|
2485
|
+
if (!domainId)
|
|
2486
|
+
return errorResult('"domainId" is required for flp_create_catalog action.');
|
|
2487
|
+
if (!title)
|
|
2488
|
+
return errorResult('"title" is required for flp_create_catalog action.');
|
|
2489
|
+
const catalog = await createCatalog(client.http, client.safety, domainId, title);
|
|
2490
|
+
return textResult(JSON.stringify(catalog, null, 2));
|
|
2491
|
+
}
|
|
2492
|
+
case 'flp_create_group': {
|
|
2493
|
+
if (cachedFeatures?.flp && !cachedFeatures.flp.available) {
|
|
2494
|
+
return errorResult(flpUnavailableMessage);
|
|
2495
|
+
}
|
|
2496
|
+
const groupId = String(args.groupId ?? '');
|
|
2497
|
+
const title = String(args.title ?? '');
|
|
2498
|
+
if (!groupId)
|
|
2499
|
+
return errorResult('"groupId" is required for flp_create_group action.');
|
|
2500
|
+
if (!title)
|
|
2501
|
+
return errorResult('"title" is required for flp_create_group action.');
|
|
2502
|
+
const group = await createGroup(client.http, client.safety, groupId, title);
|
|
2503
|
+
return textResult(JSON.stringify(group, null, 2));
|
|
2504
|
+
}
|
|
2505
|
+
case 'flp_create_tile': {
|
|
2506
|
+
if (cachedFeatures?.flp && !cachedFeatures.flp.available) {
|
|
2507
|
+
return errorResult(flpUnavailableMessage);
|
|
2508
|
+
}
|
|
2509
|
+
const catalogId = String(args.catalogId ?? '');
|
|
2510
|
+
if (!catalogId)
|
|
2511
|
+
return errorResult('"catalogId" is required for flp_create_tile action.');
|
|
2512
|
+
const rawTile = args.tile;
|
|
2513
|
+
if (!rawTile || typeof rawTile !== 'object' || Array.isArray(rawTile)) {
|
|
2514
|
+
return errorResult('"tile" object is required for flp_create_tile action.');
|
|
2515
|
+
}
|
|
2516
|
+
const tile = rawTile;
|
|
2517
|
+
const id = String(tile.id ?? '');
|
|
2518
|
+
const title = String(tile.title ?? '');
|
|
2519
|
+
const semanticObject = String(tile.semanticObject ?? '');
|
|
2520
|
+
const semanticAction = String(tile.semanticAction ?? '');
|
|
2521
|
+
if (!id || !title || !semanticObject || !semanticAction) {
|
|
2522
|
+
return errorResult('"tile.id", "tile.title", "tile.semanticObject", and "tile.semanticAction" are required for flp_create_tile action.');
|
|
2523
|
+
}
|
|
2524
|
+
const tileInstance = await createTile(client.http, client.safety, catalogId, {
|
|
2525
|
+
id,
|
|
2526
|
+
title,
|
|
2527
|
+
semanticObject,
|
|
2528
|
+
semanticAction,
|
|
2529
|
+
icon: typeof tile.icon === 'string' ? tile.icon : undefined,
|
|
2530
|
+
url: typeof tile.url === 'string' ? tile.url : undefined,
|
|
2531
|
+
subtitle: typeof tile.subtitle === 'string' ? tile.subtitle : undefined,
|
|
2532
|
+
info: typeof tile.info === 'string' ? tile.info : undefined,
|
|
2533
|
+
});
|
|
2534
|
+
return textResult(JSON.stringify(tileInstance, null, 2));
|
|
2535
|
+
}
|
|
2536
|
+
case 'flp_add_tile_to_group': {
|
|
2537
|
+
if (cachedFeatures?.flp && !cachedFeatures.flp.available) {
|
|
2538
|
+
return errorResult(flpUnavailableMessage);
|
|
2539
|
+
}
|
|
2540
|
+
const groupId = String(args.groupId ?? '');
|
|
2541
|
+
const catalogId = String(args.catalogId ?? '');
|
|
2542
|
+
const tileInstanceId = String(args.tileInstanceId ?? '');
|
|
2543
|
+
if (!groupId)
|
|
2544
|
+
return errorResult('"groupId" is required for flp_add_tile_to_group action.');
|
|
2545
|
+
if (!catalogId)
|
|
2546
|
+
return errorResult('"catalogId" is required for flp_add_tile_to_group action.');
|
|
2547
|
+
if (!tileInstanceId)
|
|
2548
|
+
return errorResult('"tileInstanceId" is required for flp_add_tile_to_group action.');
|
|
2549
|
+
const result = await addTileToGroup(client.http, client.safety, groupId, catalogId, tileInstanceId);
|
|
2550
|
+
return textResult(JSON.stringify(result, null, 2));
|
|
2551
|
+
}
|
|
2552
|
+
case 'flp_delete_catalog': {
|
|
2553
|
+
if (cachedFeatures?.flp && !cachedFeatures.flp.available) {
|
|
2554
|
+
return errorResult(flpUnavailableMessage);
|
|
2555
|
+
}
|
|
2556
|
+
const catalogId = String(args.catalogId ?? '');
|
|
2557
|
+
if (!catalogId)
|
|
2558
|
+
return errorResult('"catalogId" is required for flp_delete_catalog action.');
|
|
2559
|
+
await deleteCatalog(client.http, client.safety, catalogId);
|
|
2560
|
+
return textResult(`Deleted FLP catalog: ${catalogId}`);
|
|
2561
|
+
}
|
|
1364
2562
|
case 'cache_stats': {
|
|
1365
2563
|
if (!cachingLayer) {
|
|
1366
2564
|
return textResult(JSON.stringify({ enabled: false, message: 'Object cache is disabled (ARC1_CACHE=none).' }));
|
|
@@ -1382,6 +2580,8 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
|
|
|
1382
2580
|
featureConfig.amdp = config.featureAmdp;
|
|
1383
2581
|
featureConfig.ui5 = config.featureUi5;
|
|
1384
2582
|
featureConfig.transport = config.featureTransport;
|
|
2583
|
+
featureConfig.ui5repo = config.featureUi5Repo;
|
|
2584
|
+
featureConfig.flp = config.featureFlp;
|
|
1385
2585
|
const probed = await probeFeatures(client.http, featureConfig, config.systemType);
|
|
1386
2586
|
// In PP mode with a per-user client, auth-sensitive results (401/403 on any
|
|
1387
2587
|
// feature) must not poison the global cache — another user may have different
|
|
@@ -1405,7 +2605,7 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
|
|
|
1405
2605
|
return textResult(JSON.stringify(probed, null, 2));
|
|
1406
2606
|
}
|
|
1407
2607
|
default:
|
|
1408
|
-
return errorResult(`Unknown SAPManage action: ${action}. Supported: features, probe, cache_stats`);
|
|
2608
|
+
return errorResult(`Unknown SAPManage action: ${action}. Supported: features, probe, cache_stats, create_package, delete_package, flp_list_catalogs, flp_list_groups, flp_list_tiles, flp_create_catalog, flp_create_group, flp_create_tile, flp_add_tile_to_group, flp_delete_catalog`);
|
|
1409
2609
|
}
|
|
1410
2610
|
}
|
|
1411
2611
|
/** Reset cached features (for testing) */
|