arc-1 0.5.0 → 0.6.1
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 +13 -12
- package/dist/adt/client.d.ts +13 -1
- package/dist/adt/client.d.ts.map +1 -1
- package/dist/adt/client.js +77 -1
- package/dist/adt/client.js.map +1 -1
- package/dist/adt/config.d.ts +1 -0
- package/dist/adt/config.d.ts.map +1 -1
- package/dist/adt/config.js +1 -0
- package/dist/adt/config.js.map +1 -1
- package/dist/adt/crud.d.ts.map +1 -1
- package/dist/adt/crud.js +3 -8
- package/dist/adt/crud.js.map +1 -1
- package/dist/adt/devtools.d.ts +15 -0
- package/dist/adt/devtools.d.ts.map +1 -1
- package/dist/adt/devtools.js +162 -67
- 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/features.d.ts.map +1 -1
- package/dist/adt/features.js +3 -0
- package/dist/adt/features.js.map +1 -1
- package/dist/adt/http.d.ts.map +1 -1
- package/dist/adt/http.js +83 -2
- package/dist/adt/http.js.map +1 -1
- package/dist/adt/safety.d.ts +0 -7
- package/dist/adt/safety.d.ts.map +1 -1
- package/dist/adt/safety.js +3 -49
- package/dist/adt/safety.js.map +1 -1
- package/dist/adt/transport.d.ts +6 -0
- package/dist/adt/transport.d.ts.map +1 -1
- package/dist/adt/transport.js +35 -22
- package/dist/adt/transport.js.map +1 -1
- package/dist/adt/types.d.ts +68 -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 +39 -1
- package/dist/adt/xml-parser.d.ts.map +1 -1
- package/dist/adt/xml-parser.js +185 -2
- package/dist/adt/xml-parser.js.map +1 -1
- package/dist/aff/schemas/bdef-v1.json +62 -0
- package/dist/aff/schemas/clas-v1.json +276 -0
- package/dist/aff/schemas/ddls-v1.json +144 -0
- package/dist/aff/schemas/intf-v1.json +243 -0
- package/dist/aff/schemas/prog-v1.json +133 -0
- package/dist/aff/schemas/srvb-v1.json +115 -0
- package/dist/aff/schemas/srvd-v1.json +108 -0
- package/dist/aff/validator.d.ts +14 -0
- package/dist/aff/validator.d.ts.map +1 -0
- package/dist/aff/validator.js +83 -0
- package/dist/aff/validator.js.map +1 -0
- package/dist/handlers/hyperfocused.js +1 -1
- package/dist/handlers/hyperfocused.js.map +1 -1
- package/dist/handlers/intent.d.ts +16 -0
- package/dist/handlers/intent.d.ts.map +1 -1
- package/dist/handlers/intent.js +495 -62
- package/dist/handlers/intent.js.map +1 -1
- package/dist/handlers/schemas.d.ts +80 -24
- package/dist/handlers/schemas.d.ts.map +1 -1
- package/dist/handlers/schemas.js +34 -6
- package/dist/handlers/schemas.js.map +1 -1
- package/dist/handlers/tools.d.ts.map +1 -1
- package/dist/handlers/tools.js +81 -19
- package/dist/handlers/tools.js.map +1 -1
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +7 -14
- package/dist/server/config.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 +2 -2
- package/dist/server/server.js.map +1 -1
- package/dist/server/types.d.ts +1 -1
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +2 -2
- package/dist/server/types.js.map +1 -1
- package/package.json +14 -4
package/dist/handlers/intent.js
CHANGED
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { findDefinition, findReferences, findWhereUsed, getCompletion, } from '../adt/codeintel.js';
|
|
13
13
|
import { createObject, deleteObject, lockObject, safeUpdateSource, unlockObject } from '../adt/crud.js';
|
|
14
|
-
import { activate, activateBatch, runAtcCheck, runUnitTests, syntaxCheck } from '../adt/devtools.js';
|
|
14
|
+
import { activate, activateBatch, publishServiceBinding, runAtcCheck, runUnitTests, syntaxCheck, unpublishServiceBinding, } from '../adt/devtools.js';
|
|
15
15
|
import { getDump, getTraceDbAccesses, getTraceHitlist, getTraceStatements, listDumps, listTraces, } from '../adt/diagnostics.js';
|
|
16
16
|
import { AdtApiError, AdtNetworkError, AdtSafetyError, isNotFoundError } from '../adt/errors.js';
|
|
17
17
|
import { classifyTextSearchError, mapSapReleaseToAbaplintVersion, probeFeatures } from '../adt/features.js';
|
|
18
|
-
import { isOperationAllowed, OperationType } from '../adt/safety.js';
|
|
18
|
+
import { checkPackage, isOperationAllowed, OperationType } from '../adt/safety.js';
|
|
19
19
|
import { createTransport, getTransport, listTransports, releaseTransport } from '../adt/transport.js';
|
|
20
|
+
import { getAppInfo } from '../adt/ui5-repository.js';
|
|
21
|
+
import { validateAffHeader } from '../aff/validator.js';
|
|
20
22
|
import { extractCdsElements } from '../context/cds-deps.js';
|
|
21
23
|
import { compressCdsContext, compressContext } from '../context/compressor.js';
|
|
22
24
|
import { extractMethod, formatMethodListing, listMethods, spliceMethod } from '../context/method-surgery.js';
|
|
@@ -73,6 +75,46 @@ function textResult(text) {
|
|
|
73
75
|
function errorResult(message) {
|
|
74
76
|
return { content: [{ type: 'text', text: message }], isError: true };
|
|
75
77
|
}
|
|
78
|
+
// ─── Search Helpers ─────────────────────────────────────────────────
|
|
79
|
+
/**
|
|
80
|
+
* Transliterate non-ASCII characters in search queries.
|
|
81
|
+
* SAP object names are ASCII-only, so umlauts and accented characters
|
|
82
|
+
* never appear in object names. This prevents wasted searches with
|
|
83
|
+
* German terms like "*Schätzung*" that silently return empty results.
|
|
84
|
+
*/
|
|
85
|
+
export function transliterateQuery(query) {
|
|
86
|
+
// Explicit German umlaut replacements (must come before NFD decomposition)
|
|
87
|
+
let result = query
|
|
88
|
+
.replace(/ä/g, 'AE')
|
|
89
|
+
.replace(/Ä/g, 'AE')
|
|
90
|
+
.replace(/ö/g, 'OE')
|
|
91
|
+
.replace(/Ö/g, 'OE')
|
|
92
|
+
.replace(/ü/g, 'UE')
|
|
93
|
+
.replace(/Ü/g, 'UE')
|
|
94
|
+
.replace(/ß/g, 'SS');
|
|
95
|
+
// General fallback: strip remaining diacritics (é→e, ñ→n, etc.)
|
|
96
|
+
result = result.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
|
97
|
+
return { normalized: result, changed: result !== query };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Detect if a search query looks like a field/column name rather than
|
|
101
|
+
* an object name. Field names are short, uppercase, and typically don't
|
|
102
|
+
* start with Z/Y (which are custom object prefixes).
|
|
103
|
+
*/
|
|
104
|
+
export function looksLikeFieldName(query) {
|
|
105
|
+
// Wildcard patterns are object searches, not field names
|
|
106
|
+
if (query.includes('*'))
|
|
107
|
+
return false;
|
|
108
|
+
if (query.length === 0 || query.length > 15)
|
|
109
|
+
return false;
|
|
110
|
+
// Must be uppercase letters, digits, underscores only
|
|
111
|
+
if (!/^[A-Z0-9_]+$/.test(query))
|
|
112
|
+
return false;
|
|
113
|
+
// Z/Y prefix → more likely an object name
|
|
114
|
+
if (/^[ZY]/.test(query))
|
|
115
|
+
return false;
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
76
118
|
/** Classify error type for audit logging */
|
|
77
119
|
/** Format error messages with LLM-friendly remediation hints */
|
|
78
120
|
function formatErrorForLLM(err, message, _tool, args) {
|
|
@@ -85,12 +127,49 @@ function formatErrorForLLM(err, message, _tool, args) {
|
|
|
85
127
|
if (err.isUnauthorized || err.isForbidden) {
|
|
86
128
|
return `${message}\n\nHint: Authorization error. Check SAP_CLIENT (default: '100'), SAP_USER, and SAP_PASSWORD. The configured SAP user may lack permissions for this object.`;
|
|
87
129
|
}
|
|
130
|
+
// Transport / corrNr specific hints
|
|
131
|
+
const transportHint = getTransportHint(err);
|
|
132
|
+
if (transportHint) {
|
|
133
|
+
return `${message}\n\nHint: ${transportHint}`;
|
|
134
|
+
}
|
|
88
135
|
}
|
|
89
136
|
if (err instanceof AdtNetworkError) {
|
|
90
137
|
return `${message}\n\nHint: Cannot reach the SAP system. This is a connectivity issue, not a usage error.`;
|
|
91
138
|
}
|
|
92
139
|
return message;
|
|
93
140
|
}
|
|
141
|
+
/** Detect transport/corrNr failure signatures and return a remediation hint, or undefined if not transport-related. */
|
|
142
|
+
function getTransportHint(err) {
|
|
143
|
+
const body = (err.responseBody ?? '').toLowerCase();
|
|
144
|
+
const msg = err.message.toLowerCase();
|
|
145
|
+
const combined = `${msg} ${body}`;
|
|
146
|
+
// Missing or invalid transport/correction number
|
|
147
|
+
if (combined.includes('correction number') ||
|
|
148
|
+
combined.includes('corrnr') ||
|
|
149
|
+
(combined.includes('transport request') &&
|
|
150
|
+
(combined.includes('missing') || combined.includes('required') || combined.includes('invalid')))) {
|
|
151
|
+
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.';
|
|
152
|
+
}
|
|
153
|
+
// Transport not found or not modifiable
|
|
154
|
+
if (combined.includes('e070') ||
|
|
155
|
+
(combined.includes('transport') &&
|
|
156
|
+
(combined.includes('not found') || combined.includes('does not exist') || combined.includes('not modifiable')))) {
|
|
157
|
+
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.';
|
|
158
|
+
}
|
|
159
|
+
// Package / transport layer mismatch
|
|
160
|
+
if (combined.includes('transport layer') ||
|
|
161
|
+
(combined.includes('package') &&
|
|
162
|
+
combined.includes('transport') &&
|
|
163
|
+
(combined.includes('mismatch') || combined.includes('not assigned') || combined.includes('no transport layer')))) {
|
|
164
|
+
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.';
|
|
165
|
+
}
|
|
166
|
+
// Authorization for transport operations
|
|
167
|
+
if (combined.includes('s_transprt') ||
|
|
168
|
+
(combined.includes('transport') && (combined.includes('no authorization') || combined.includes('not authorized')))) {
|
|
169
|
+
return 'The SAP user lacks transport authorization (S_TRANSPRT). Contact your SAP basis administrator to grant the required transport permissions.';
|
|
170
|
+
}
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
94
173
|
function classifyError(err) {
|
|
95
174
|
if (err instanceof AdtApiError)
|
|
96
175
|
return 'AdtApiError';
|
|
@@ -291,21 +370,36 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
291
370
|
if (isBtpSystem() && BTP_HINTS[type]) {
|
|
292
371
|
return errorResult(BTP_HINTS[type]);
|
|
293
372
|
}
|
|
294
|
-
// Helper: get source with cache support
|
|
373
|
+
// Helper: get source with cache support, returns cache hit status
|
|
295
374
|
const cachedGet = async (objType, objName, fetcher) => {
|
|
296
375
|
if (!cachingLayer)
|
|
297
|
-
return fetcher();
|
|
298
|
-
const { source } = await cachingLayer.getSource(objType, objName, fetcher);
|
|
299
|
-
return source;
|
|
376
|
+
return { source: await fetcher(), cacheHit: false };
|
|
377
|
+
const { source, hit } = await cachingLayer.getSource(objType, objName, fetcher);
|
|
378
|
+
return { source, cacheHit: hit };
|
|
300
379
|
};
|
|
380
|
+
/** Prepend [cached] indicator when result came from cache */
|
|
381
|
+
const cachedTextResult = (source, cacheHit) => {
|
|
382
|
+
return textResult(cacheHit ? `[cached]\n${source}` : source);
|
|
383
|
+
};
|
|
384
|
+
// Structured format is only supported for CLAS type
|
|
385
|
+
if (args.format === 'structured' && type !== 'CLAS') {
|
|
386
|
+
return errorResult('The "structured" format is only supported for CLAS type. Other types return text format.');
|
|
387
|
+
}
|
|
301
388
|
switch (type) {
|
|
302
|
-
case 'PROG':
|
|
303
|
-
|
|
389
|
+
case 'PROG': {
|
|
390
|
+
const { source, cacheHit } = await cachedGet('PROG', name, () => client.getProgram(name));
|
|
391
|
+
return cachedTextResult(source, cacheHit);
|
|
392
|
+
}
|
|
304
393
|
case 'CLAS': {
|
|
394
|
+
// Structured format: return JSON with metadata + decomposed source
|
|
395
|
+
if (args.format === 'structured') {
|
|
396
|
+
const structured = await client.getClassStructured(name);
|
|
397
|
+
return textResult(JSON.stringify(structured, null, 2));
|
|
398
|
+
}
|
|
305
399
|
const methodParam = args.method;
|
|
306
400
|
if (methodParam && !args.include) {
|
|
307
|
-
// Method-level read — fetch full source then extract
|
|
308
|
-
const fullSource = await cachedGet('CLAS', name, () => client.getClass(name));
|
|
401
|
+
// Method-level read — fetch full source then extract (no cache indicator for derived results)
|
|
402
|
+
const { source: fullSource } = await cachedGet('CLAS', name, () => client.getClass(name));
|
|
309
403
|
const abaplintVer = cachedFeatures?.abapRelease
|
|
310
404
|
? mapSapReleaseToAbaplintVersion(cachedFeatures.abapRelease)
|
|
311
405
|
: undefined;
|
|
@@ -321,12 +415,15 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
321
415
|
}
|
|
322
416
|
// Only cache the full merged source (no include param), not individual includes
|
|
323
417
|
if (!args.include) {
|
|
324
|
-
|
|
418
|
+
const { source, cacheHit } = await cachedGet('CLAS', name, () => client.getClass(name));
|
|
419
|
+
return cachedTextResult(source, cacheHit);
|
|
325
420
|
}
|
|
326
421
|
return textResult(await client.getClass(name, args.include));
|
|
327
422
|
}
|
|
328
|
-
case 'INTF':
|
|
329
|
-
|
|
423
|
+
case 'INTF': {
|
|
424
|
+
const { source, cacheHit } = await cachedGet('INTF', name, () => client.getInterface(name));
|
|
425
|
+
return cachedTextResult(source, cacheHit);
|
|
426
|
+
}
|
|
330
427
|
case 'FUNC': {
|
|
331
428
|
let group = String(args.group ?? '');
|
|
332
429
|
if (!group) {
|
|
@@ -339,7 +436,8 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
339
436
|
}
|
|
340
437
|
group = resolved;
|
|
341
438
|
}
|
|
342
|
-
|
|
439
|
+
const { source, cacheHit } = await cachedGet('FUNC', name, () => client.getFunction(group, name));
|
|
440
|
+
return cachedTextResult(source, cacheHit);
|
|
343
441
|
}
|
|
344
442
|
case 'FUGR': {
|
|
345
443
|
const expand = Boolean(args.expand_includes);
|
|
@@ -364,22 +462,30 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
364
462
|
const fg = await client.getFunctionGroup(name);
|
|
365
463
|
return textResult(JSON.stringify(fg, null, 2));
|
|
366
464
|
}
|
|
367
|
-
case 'INCL':
|
|
368
|
-
|
|
465
|
+
case 'INCL': {
|
|
466
|
+
const { source, cacheHit } = await cachedGet('INCL', name, () => client.getInclude(name));
|
|
467
|
+
return cachedTextResult(source, cacheHit);
|
|
468
|
+
}
|
|
369
469
|
case 'DDLS': {
|
|
370
|
-
const ddlSource = await cachedGet('DDLS', name, () => client.getDdls(name));
|
|
470
|
+
const { source: ddlSource, cacheHit } = await cachedGet('DDLS', name, () => client.getDdls(name));
|
|
371
471
|
if (args.include?.toLowerCase() === 'elements') {
|
|
472
|
+
// Elements extraction is derived from source — no cache indicator
|
|
372
473
|
return textResult(extractCdsElements(ddlSource, name));
|
|
373
474
|
}
|
|
374
|
-
return
|
|
475
|
+
return cachedTextResult(ddlSource, cacheHit);
|
|
476
|
+
}
|
|
477
|
+
case 'BDEF': {
|
|
478
|
+
const { source, cacheHit } = await cachedGet('BDEF', name, () => client.getBdef(name));
|
|
479
|
+
return cachedTextResult(source, cacheHit);
|
|
480
|
+
}
|
|
481
|
+
case 'SRVD': {
|
|
482
|
+
const { source, cacheHit } = await cachedGet('SRVD', name, () => client.getSrvd(name));
|
|
483
|
+
return cachedTextResult(source, cacheHit);
|
|
375
484
|
}
|
|
376
|
-
case 'BDEF':
|
|
377
|
-
return textResult(await cachedGet('BDEF', name, () => client.getBdef(name)));
|
|
378
|
-
case 'SRVD':
|
|
379
|
-
return textResult(await cachedGet('SRVD', name, () => client.getSrvd(name)));
|
|
380
485
|
case 'DDLX': {
|
|
381
486
|
try {
|
|
382
|
-
|
|
487
|
+
const { source, cacheHit } = await cachedGet('DDLX', name, () => client.getDdlx(name));
|
|
488
|
+
return cachedTextResult(source, cacheHit);
|
|
383
489
|
}
|
|
384
490
|
catch (err) {
|
|
385
491
|
if (isNotFoundError(err)) {
|
|
@@ -388,14 +494,22 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
388
494
|
throw err;
|
|
389
495
|
}
|
|
390
496
|
}
|
|
391
|
-
case 'SRVB':
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
case '
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
497
|
+
case 'SRVB': {
|
|
498
|
+
const { source, cacheHit } = await cachedGet('SRVB', name, () => client.getSrvb(name));
|
|
499
|
+
return cachedTextResult(source, cacheHit);
|
|
500
|
+
}
|
|
501
|
+
case 'TABL': {
|
|
502
|
+
const { source, cacheHit } = await cachedGet('TABL', name, () => client.getTable(name));
|
|
503
|
+
return cachedTextResult(source, cacheHit);
|
|
504
|
+
}
|
|
505
|
+
case 'VIEW': {
|
|
506
|
+
const { source, cacheHit } = await cachedGet('VIEW', name, () => client.getView(name));
|
|
507
|
+
return cachedTextResult(source, cacheHit);
|
|
508
|
+
}
|
|
509
|
+
case 'STRU': {
|
|
510
|
+
const { source, cacheHit } = await cachedGet('STRU', name, () => client.getStructure(name));
|
|
511
|
+
return cachedTextResult(source, cacheHit);
|
|
512
|
+
}
|
|
399
513
|
case 'DOMA': {
|
|
400
514
|
const domain = await client.getDomain(name);
|
|
401
515
|
return textResult(JSON.stringify(domain, null, 2));
|
|
@@ -421,6 +535,18 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
421
535
|
}
|
|
422
536
|
return textResult(JSON.stringify(tran, null, 2));
|
|
423
537
|
}
|
|
538
|
+
case 'API_STATE': {
|
|
539
|
+
// Determine object type for URL construction — use explicit objectType, infer from name, or error
|
|
540
|
+
const explicitType = String(args.objectType ?? '').toUpperCase();
|
|
541
|
+
const inferredType = explicitType || inferObjectType(name);
|
|
542
|
+
if (!inferredType) {
|
|
543
|
+
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").`);
|
|
544
|
+
}
|
|
545
|
+
// Use raw URI (no name encoding) — getApiReleaseState encodes the full URI as a single path segment
|
|
546
|
+
const objectUri = objectUrlForTypeRaw(inferredType, name);
|
|
547
|
+
const releaseState = await client.getApiReleaseState(objectUri);
|
|
548
|
+
return textResult(JSON.stringify(releaseState, null, 2));
|
|
549
|
+
}
|
|
424
550
|
case 'TABLE_CONTENTS': {
|
|
425
551
|
const maxRows = Number(args.maxRows ?? 100);
|
|
426
552
|
const data = await client.getTableContents(name, maxRows, args.sqlFilter);
|
|
@@ -470,18 +596,53 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
470
596
|
return textResult(await client.getTextElements(name));
|
|
471
597
|
case 'VARIANTS':
|
|
472
598
|
return textResult(await client.getVariants(name));
|
|
599
|
+
case 'BSP': {
|
|
600
|
+
if (cachedFeatures?.ui5 && !cachedFeatures.ui5.available) {
|
|
601
|
+
return errorResult('UI5/Fiori BSP Filestore is not available on this SAP system. ' +
|
|
602
|
+
'Run SAPManage(action="probe") to verify feature availability.');
|
|
603
|
+
}
|
|
604
|
+
const include = args.include;
|
|
605
|
+
if (!name) {
|
|
606
|
+
// List all BSP apps (optional search via query param not used here since name is empty)
|
|
607
|
+
const apps = await client.listBspApps();
|
|
608
|
+
return textResult(JSON.stringify(apps, null, 2));
|
|
609
|
+
}
|
|
610
|
+
if (!include) {
|
|
611
|
+
// Browse root structure of the app
|
|
612
|
+
return textResult(JSON.stringify(await client.getBspAppStructure(name), null, 2));
|
|
613
|
+
}
|
|
614
|
+
// If include contains a dot, treat as file read; otherwise browse subfolder
|
|
615
|
+
if (include.includes('.')) {
|
|
616
|
+
return textResult(await client.getBspFileContent(name, include));
|
|
617
|
+
}
|
|
618
|
+
return textResult(JSON.stringify(await client.getBspAppStructure(name, `/${include}`), null, 2));
|
|
619
|
+
}
|
|
620
|
+
case 'BSP_DEPLOY': {
|
|
621
|
+
if (cachedFeatures?.ui5repo && !cachedFeatures.ui5repo.available) {
|
|
622
|
+
return errorResult('ABAP Repository OData Service is not available on this SAP system. ' +
|
|
623
|
+
'Run SAPManage(action="probe") to verify feature availability.');
|
|
624
|
+
}
|
|
625
|
+
if (!name) {
|
|
626
|
+
return errorResult('BSP_DEPLOY requires a name parameter (e.g., name="ZAPP_BOOKING").');
|
|
627
|
+
}
|
|
628
|
+
const info = await getAppInfo(client.http, client.safety, name);
|
|
629
|
+
if (!info) {
|
|
630
|
+
return textResult(`App "${name}" not found in ABAP Repository.`);
|
|
631
|
+
}
|
|
632
|
+
return textResult(JSON.stringify(info, null, 2));
|
|
633
|
+
}
|
|
473
634
|
default:
|
|
474
|
-
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. ` +
|
|
635
|
+
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. ` +
|
|
475
636
|
'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"). ' +
|
|
476
637
|
'Do not pass a URI — use the "type" and "name" parameters instead.');
|
|
477
638
|
}
|
|
478
639
|
}
|
|
479
640
|
async function handleSAPSearch(client, args) {
|
|
480
|
-
const
|
|
641
|
+
const rawQuery = String(args.query ?? '');
|
|
481
642
|
const maxResults = Number(args.maxResults ?? 100);
|
|
482
643
|
const searchType = String(args.searchType ?? 'object');
|
|
483
644
|
if (searchType === 'source_code') {
|
|
484
|
-
//
|
|
645
|
+
// Source code search: do NOT transliterate — source can contain umlauts in strings/comments
|
|
485
646
|
if (cachedFeatures?.textSearch && !cachedFeatures.textSearch.available) {
|
|
486
647
|
return errorResult(`Source code search is not available on this SAP system. ${cachedFeatures.textSearch.reason ?? ''}` +
|
|
487
648
|
`\nUse SAPSearch with searchType="object" to search by object name instead, or use SAPQuery to search metadata tables.`);
|
|
@@ -489,7 +650,7 @@ async function handleSAPSearch(client, args) {
|
|
|
489
650
|
const objectType = args.objectType;
|
|
490
651
|
const packageName = args.packageName;
|
|
491
652
|
try {
|
|
492
|
-
const results = await client.searchSource(
|
|
653
|
+
const results = await client.searchSource(rawQuery, maxResults, objectType, packageName);
|
|
493
654
|
return textResult(JSON.stringify(results, null, 2));
|
|
494
655
|
}
|
|
495
656
|
catch (err) {
|
|
@@ -504,8 +665,25 @@ async function handleSAPSearch(client, args) {
|
|
|
504
665
|
throw err;
|
|
505
666
|
}
|
|
506
667
|
}
|
|
668
|
+
// Object search: transliterate non-ASCII (SAP object names are ASCII-only)
|
|
669
|
+
const { normalized: query, changed: wasTransliterated } = transliterateQuery(rawQuery);
|
|
670
|
+
const transliterationNote = wasTransliterated
|
|
671
|
+
? `Note: Query contained non-ASCII characters. Transliterated "${rawQuery}" → "${query}" (SAP object names are ASCII-only).\n\n`
|
|
672
|
+
: '';
|
|
507
673
|
const results = await client.searchObject(query, maxResults);
|
|
508
|
-
|
|
674
|
+
if (Array.isArray(results) && results.length === 0) {
|
|
675
|
+
let hint = '[]' +
|
|
676
|
+
'\n\n' +
|
|
677
|
+
transliterationNote +
|
|
678
|
+
'No objects found. If searching for custom objects, try Z* or Y* prefixes (e.g., "Z*ESTIM*"). ' +
|
|
679
|
+
'If you already found objects in a package, use SAPRead with type=DEVC to list all package contents instead of more searches.';
|
|
680
|
+
if (looksLikeFieldName(query)) {
|
|
681
|
+
const stripped = query.replace(/\*/g, '');
|
|
682
|
+
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.`;
|
|
683
|
+
}
|
|
684
|
+
return textResult(hint);
|
|
685
|
+
}
|
|
686
|
+
return textResult(transliterationNote + JSON.stringify(results, null, 2));
|
|
509
687
|
}
|
|
510
688
|
async function handleSAPQuery(client, args) {
|
|
511
689
|
const sql = String(args.sql ?? '');
|
|
@@ -729,46 +907,67 @@ function escapeXml(s) {
|
|
|
729
907
|
.replace(/>/g, '>');
|
|
730
908
|
}
|
|
731
909
|
// ─── Object URL Mapping ──────────────────────────────────────────────
|
|
732
|
-
/**
|
|
733
|
-
function
|
|
734
|
-
const encoded = encodeURIComponent(name);
|
|
910
|
+
/** Base path for an object type. Returns path prefix without trailing name segment. */
|
|
911
|
+
function objectBasePath(type) {
|
|
735
912
|
switch (type) {
|
|
736
913
|
case 'PROG':
|
|
737
|
-
return
|
|
914
|
+
return '/sap/bc/adt/programs/programs/';
|
|
738
915
|
case 'CLAS':
|
|
739
|
-
return
|
|
916
|
+
return '/sap/bc/adt/oo/classes/';
|
|
740
917
|
case 'INTF':
|
|
741
|
-
return
|
|
918
|
+
return '/sap/bc/adt/oo/interfaces/';
|
|
742
919
|
case 'FUNC':
|
|
743
|
-
return
|
|
920
|
+
return '/sap/bc/adt/functions/groups/';
|
|
744
921
|
case 'INCL':
|
|
745
|
-
return
|
|
922
|
+
return '/sap/bc/adt/programs/includes/';
|
|
746
923
|
case 'FUGR':
|
|
747
|
-
return
|
|
924
|
+
return '/sap/bc/adt/functions/groups/';
|
|
748
925
|
case 'DDLS':
|
|
749
|
-
return
|
|
926
|
+
return '/sap/bc/adt/ddic/ddl/sources/';
|
|
750
927
|
case 'BDEF':
|
|
751
|
-
return
|
|
928
|
+
return '/sap/bc/adt/bo/behaviordefinitions/';
|
|
752
929
|
case 'SRVD':
|
|
753
|
-
return
|
|
930
|
+
return '/sap/bc/adt/ddic/srvd/sources/';
|
|
754
931
|
case 'DDLX':
|
|
755
|
-
return
|
|
932
|
+
return '/sap/bc/adt/ddic/ddlx/sources/';
|
|
756
933
|
case 'SRVB':
|
|
757
|
-
return
|
|
934
|
+
return '/sap/bc/adt/businessservices/bindings/';
|
|
758
935
|
case 'TABL':
|
|
759
|
-
return
|
|
936
|
+
return '/sap/bc/adt/ddic/tables/';
|
|
760
937
|
case 'STRU':
|
|
761
|
-
return
|
|
938
|
+
return '/sap/bc/adt/ddic/structures/';
|
|
762
939
|
case 'DOMA':
|
|
763
|
-
return
|
|
940
|
+
return '/sap/bc/adt/ddic/domains/';
|
|
764
941
|
case 'DTEL':
|
|
765
|
-
return
|
|
942
|
+
return '/sap/bc/adt/ddic/dataelements/';
|
|
766
943
|
case 'TRAN':
|
|
767
|
-
return
|
|
944
|
+
return '/sap/bc/adt/vit/wb/object_type/trant/object_name/';
|
|
768
945
|
default:
|
|
769
|
-
return
|
|
946
|
+
return '/sap/bc/adt/programs/programs/';
|
|
770
947
|
}
|
|
771
948
|
}
|
|
949
|
+
/** Map object type + name to the ADT object URL used by CRUD/DevTools/etc. Name is URI-encoded. */
|
|
950
|
+
function objectUrlForType(type, name) {
|
|
951
|
+
return `${objectBasePath(type)}${encodeURIComponent(name)}`;
|
|
952
|
+
}
|
|
953
|
+
/** Infer SAP object type from naming conventions. Returns empty string if type cannot be determined. */
|
|
954
|
+
function inferObjectType(name) {
|
|
955
|
+
const upper = name.toUpperCase();
|
|
956
|
+
if (upper.startsWith('IF_') || upper.startsWith('ZIF_') || upper.startsWith('YIF_'))
|
|
957
|
+
return 'INTF';
|
|
958
|
+
if (upper.startsWith('CL_') || upper.startsWith('ZCL_') || upper.startsWith('YCL_'))
|
|
959
|
+
return 'CLAS';
|
|
960
|
+
if (upper.startsWith('CX_') || upper.startsWith('ZCX_') || upper.startsWith('YCX_'))
|
|
961
|
+
return 'CLAS';
|
|
962
|
+
return '';
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Map object type + name to the ADT object URL WITHOUT encoding the name.
|
|
966
|
+
* Used for API release state where the full URI is encoded as a single path segment by the caller.
|
|
967
|
+
*/
|
|
968
|
+
function objectUrlForTypeRaw(type, name) {
|
|
969
|
+
return `${objectBasePath(type)}${name}`;
|
|
970
|
+
}
|
|
772
971
|
/** Get the source URL for an object (appends /source/main) */
|
|
773
972
|
function sourceUrlForType(type, name) {
|
|
774
973
|
return `${objectUrlForType(type, name)}/source/main`;
|
|
@@ -780,6 +979,10 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
780
979
|
const name = String(args.name ?? '');
|
|
781
980
|
const source = String(args.source ?? '');
|
|
782
981
|
const transport = args.transport;
|
|
982
|
+
// type and name are required for all actions except batch_create
|
|
983
|
+
if (action !== 'batch_create' && (!type || !name)) {
|
|
984
|
+
return errorResult('"type" and "name" are required for this action.');
|
|
985
|
+
}
|
|
783
986
|
const objectUrl = objectUrlForType(type, name);
|
|
784
987
|
const srcUrl = sourceUrlForType(type, name);
|
|
785
988
|
switch (action) {
|
|
@@ -795,7 +998,13 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
795
998
|
}
|
|
796
999
|
case 'create': {
|
|
797
1000
|
const pkg = String(args.package ?? '$TMP');
|
|
1001
|
+
checkPackage(client.safety, pkg);
|
|
798
1002
|
const description = String(args.description ?? name);
|
|
1003
|
+
// AFF header validation (if schema available for this type)
|
|
1004
|
+
const affResult = validateAffHeader(type, { description, originalLanguage: 'en' });
|
|
1005
|
+
if (!affResult.valid) {
|
|
1006
|
+
return errorResult(`AFF metadata validation failed for ${type} ${name}:\n- ${(affResult.errors ?? []).join('\n- ')}\n\nFix the metadata and retry.`);
|
|
1007
|
+
}
|
|
799
1008
|
// Build type-specific creation XML body.
|
|
800
1009
|
// SAP ADT requires the root element to match the object type —
|
|
801
1010
|
// a generic objectReferences body returns 400 "System expected the element ...".
|
|
@@ -849,11 +1058,12 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
849
1058
|
return lintWarnings.warnings ? textResult(`${msg}\n\n${lintWarnings.warnings}`) : textResult(msg);
|
|
850
1059
|
}
|
|
851
1060
|
case 'delete': {
|
|
852
|
-
// Lock, delete, unlock pattern
|
|
1061
|
+
// Lock, delete, unlock pattern — auto-propagate lock corrNr if no explicit transport
|
|
853
1062
|
await client.http.withStatefulSession(async (session) => {
|
|
854
1063
|
const lock = await lockObject(session, client.safety, objectUrl);
|
|
1064
|
+
const effectiveTransport = transport ?? (lock.corrNr || undefined);
|
|
855
1065
|
try {
|
|
856
|
-
await deleteObject(session, client.safety, objectUrl, lock.lockHandle,
|
|
1066
|
+
await deleteObject(session, client.safety, objectUrl, lock.lockHandle, effectiveTransport);
|
|
857
1067
|
}
|
|
858
1068
|
finally {
|
|
859
1069
|
try {
|
|
@@ -867,8 +1077,104 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
867
1077
|
cachingLayer?.invalidate(type, name);
|
|
868
1078
|
return textResult(`Deleted ${type} ${name}.`);
|
|
869
1079
|
}
|
|
1080
|
+
case 'batch_create': {
|
|
1081
|
+
const objects = args.objects;
|
|
1082
|
+
if (!objects || !Array.isArray(objects) || objects.length === 0) {
|
|
1083
|
+
return errorResult('"objects" array is required and must be non-empty for batch_create action.');
|
|
1084
|
+
}
|
|
1085
|
+
const pkg = String(args.package ?? '$TMP');
|
|
1086
|
+
// Check package is allowed before starting any creates
|
|
1087
|
+
checkPackage(client.safety, pkg);
|
|
1088
|
+
const results = [];
|
|
1089
|
+
for (const obj of objects) {
|
|
1090
|
+
const objType = String(obj.type ?? '');
|
|
1091
|
+
const objName = String(obj.name ?? '');
|
|
1092
|
+
const objSource = obj.source ? String(obj.source) : undefined;
|
|
1093
|
+
const objDescription = String(obj.description ?? objName);
|
|
1094
|
+
// AFF header validation per object (if schema available)
|
|
1095
|
+
const affResult = validateAffHeader(objType, { description: objDescription, originalLanguage: 'en' });
|
|
1096
|
+
if (!affResult.valid) {
|
|
1097
|
+
results.push({
|
|
1098
|
+
type: objType,
|
|
1099
|
+
name: objName,
|
|
1100
|
+
status: 'failed',
|
|
1101
|
+
error: `AFF metadata validation failed:\n- ${(affResult.errors ?? []).join('\n- ')}`,
|
|
1102
|
+
});
|
|
1103
|
+
break;
|
|
1104
|
+
}
|
|
1105
|
+
try {
|
|
1106
|
+
// Pre-validate source with lint BEFORE creating the object to avoid orphaned objects
|
|
1107
|
+
if (objSource) {
|
|
1108
|
+
const lintWarnings = runPreWriteLint(objSource, objType, objName, config);
|
|
1109
|
+
if (lintWarnings.blocked) {
|
|
1110
|
+
results.push({
|
|
1111
|
+
type: objType,
|
|
1112
|
+
name: objName,
|
|
1113
|
+
status: 'failed',
|
|
1114
|
+
error: `source rejected by lint: ${lintWarnings.result.content[0].text}`,
|
|
1115
|
+
});
|
|
1116
|
+
break;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
// Step 1: Create the object
|
|
1120
|
+
const objUrl = objectUrlForType(objType, objName);
|
|
1121
|
+
const createUrl = objUrl.replace(/\/[^/]+$/, '');
|
|
1122
|
+
const body = buildCreateXml(objType, objName, pkg, objDescription);
|
|
1123
|
+
await createObject(client.http, client.safety, createUrl, body, 'application/xml', transport);
|
|
1124
|
+
// Step 2: Write source if provided
|
|
1125
|
+
if (objSource) {
|
|
1126
|
+
const srcUrl = sourceUrlForType(objType, objName);
|
|
1127
|
+
await safeUpdateSource(client.http, client.safety, objUrl, srcUrl, objSource, transport);
|
|
1128
|
+
}
|
|
1129
|
+
// Step 3: Activate the object
|
|
1130
|
+
const activationResult = await activate(client.http, client.safety, objUrl);
|
|
1131
|
+
if (!activationResult.success) {
|
|
1132
|
+
results.push({
|
|
1133
|
+
type: objType,
|
|
1134
|
+
name: objName,
|
|
1135
|
+
status: 'failed',
|
|
1136
|
+
error: `activation failed: ${activationResult.messages.join('; ')}`,
|
|
1137
|
+
});
|
|
1138
|
+
break;
|
|
1139
|
+
}
|
|
1140
|
+
cachingLayer?.invalidate(objType, objName);
|
|
1141
|
+
results.push({ type: objType, name: objName, status: 'success' });
|
|
1142
|
+
}
|
|
1143
|
+
catch (err) {
|
|
1144
|
+
results.push({
|
|
1145
|
+
type: objType,
|
|
1146
|
+
name: objName,
|
|
1147
|
+
status: 'failed',
|
|
1148
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1149
|
+
});
|
|
1150
|
+
break;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
// Add 'skipped' entries for objects that were never attempted due to early break
|
|
1154
|
+
for (let i = results.length; i < objects.length; i++) {
|
|
1155
|
+
const skipped = objects[i];
|
|
1156
|
+
results.push({
|
|
1157
|
+
type: String(skipped.type ?? ''),
|
|
1158
|
+
name: String(skipped.name ?? ''),
|
|
1159
|
+
status: 'failed',
|
|
1160
|
+
error: 'skipped — stopped after previous failure',
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
const summary = results
|
|
1164
|
+
.map((r) => `${r.name} (${r.type}) ${r.status === 'success' ? '✓' : `✗ — ${r.error}`}`)
|
|
1165
|
+
.join(', ');
|
|
1166
|
+
const successCount = results.filter((r) => r.status === 'success').length;
|
|
1167
|
+
const hasFailure = results.some((r) => r.status === 'failed');
|
|
1168
|
+
if (hasFailure) {
|
|
1169
|
+
const cleanupHint = successCount > 0
|
|
1170
|
+
? ` Note: ${successCount} already-created object(s) remain on the SAP system and may need manual cleanup.`
|
|
1171
|
+
: '';
|
|
1172
|
+
return errorResult(`Batch created ${successCount}/${objects.length} objects in package ${pkg}: ${summary}${cleanupHint}`);
|
|
1173
|
+
}
|
|
1174
|
+
return textResult(`Batch created ${successCount} objects in package ${pkg}: ${summary}`);
|
|
1175
|
+
}
|
|
870
1176
|
default:
|
|
871
|
-
return errorResult(`Unknown SAPWrite action: ${action}. Supported: create, update, delete, edit_method`);
|
|
1177
|
+
return errorResult(`Unknown SAPWrite action: ${action}. Supported: create, update, delete, edit_method, batch_create`);
|
|
872
1178
|
}
|
|
873
1179
|
}
|
|
874
1180
|
/**
|
|
@@ -922,8 +1228,78 @@ function runPreWriteLint(source, type, name, config) {
|
|
|
922
1228
|
}
|
|
923
1229
|
// ─── SAPActivate Handler ─────────────────────────────────────────────
|
|
924
1230
|
async function handleSAPActivate(client, args) {
|
|
925
|
-
const
|
|
1231
|
+
const action = String(args.action ?? 'activate');
|
|
1232
|
+
const name = String(args.name ?? '');
|
|
1233
|
+
const version = String(args.version ?? '0001');
|
|
1234
|
+
// Publish service binding
|
|
1235
|
+
if (action === 'publish_srvb') {
|
|
1236
|
+
if (!name) {
|
|
1237
|
+
return errorResult('Missing required "name" parameter for publish_srvb action.');
|
|
1238
|
+
}
|
|
1239
|
+
const result = await publishServiceBinding(client.http, client.safety, name, version);
|
|
1240
|
+
if (result.severity === 'ERROR') {
|
|
1241
|
+
return errorResult(`Failed to publish service binding ${name}: ${result.shortText}${result.longText ? ` — ${result.longText}` : ''}`);
|
|
1242
|
+
}
|
|
1243
|
+
let srvbInfo;
|
|
1244
|
+
try {
|
|
1245
|
+
srvbInfo = await client.getSrvb(name);
|
|
1246
|
+
}
|
|
1247
|
+
catch {
|
|
1248
|
+
if (result.severity === 'UNKNOWN') {
|
|
1249
|
+
return errorResult(`Publish response for ${name} could not be parsed and readback failed — use SAPRead to verify publish status.`);
|
|
1250
|
+
}
|
|
1251
|
+
return textResult(`Successfully published service binding ${name} (readback of binding metadata failed — use SAPRead to verify)`);
|
|
1252
|
+
}
|
|
1253
|
+
// Verify the published flag from the SRVB readback
|
|
1254
|
+
try {
|
|
1255
|
+
const srvbData = JSON.parse(srvbInfo);
|
|
1256
|
+
if (srvbData.published === false) {
|
|
1257
|
+
return errorResult(`Publish of service binding ${name} may have failed — binding is still unpublished.\n\n${srvbInfo}`);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
catch {
|
|
1261
|
+
// If we can't parse the readback JSON, fall through — better to return what we have
|
|
1262
|
+
}
|
|
1263
|
+
if (result.severity === 'UNKNOWN') {
|
|
1264
|
+
return textResult(`Publish request for ${name} completed but response could not be fully parsed. Verify status below:\n\n${srvbInfo}`);
|
|
1265
|
+
}
|
|
1266
|
+
return textResult(`Successfully published service binding ${name}.\n\n${srvbInfo}`);
|
|
1267
|
+
}
|
|
1268
|
+
// Unpublish service binding
|
|
1269
|
+
if (action === 'unpublish_srvb') {
|
|
1270
|
+
if (!name) {
|
|
1271
|
+
return errorResult('Missing required "name" parameter for unpublish_srvb action.');
|
|
1272
|
+
}
|
|
1273
|
+
const result = await unpublishServiceBinding(client.http, client.safety, name, version);
|
|
1274
|
+
if (result.severity === 'ERROR') {
|
|
1275
|
+
return errorResult(`Failed to unpublish service binding ${name}: ${result.shortText}${result.longText ? ` — ${result.longText}` : ''}`);
|
|
1276
|
+
}
|
|
1277
|
+
let srvbInfo;
|
|
1278
|
+
try {
|
|
1279
|
+
srvbInfo = await client.getSrvb(name);
|
|
1280
|
+
}
|
|
1281
|
+
catch {
|
|
1282
|
+
// Readback failed — fall through with what we have
|
|
1283
|
+
}
|
|
1284
|
+
// Verify the published flag from the SRVB readback
|
|
1285
|
+
if (srvbInfo) {
|
|
1286
|
+
try {
|
|
1287
|
+
const srvbData = JSON.parse(srvbInfo);
|
|
1288
|
+
if (srvbData.published === true) {
|
|
1289
|
+
return errorResult(`Unpublish of service binding ${name} may have failed — binding is still published.\n\n${srvbInfo}`);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
catch {
|
|
1293
|
+
// If we can't parse the readback JSON, fall through
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
if (result.severity === 'UNKNOWN') {
|
|
1297
|
+
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.'}`);
|
|
1298
|
+
}
|
|
1299
|
+
return textResult(`Successfully unpublished service binding ${name}.${srvbInfo ? `\n\n${srvbInfo}` : ''}`);
|
|
1300
|
+
}
|
|
926
1301
|
// Batch activation: multiple objects at once (for RAP stacks etc.)
|
|
1302
|
+
const type = String(args.type ?? '');
|
|
927
1303
|
if (args.objects && Array.isArray(args.objects)) {
|
|
928
1304
|
const objects = args.objects.map((o) => {
|
|
929
1305
|
const objType = String(o.type ?? type);
|
|
@@ -938,7 +1314,6 @@ async function handleSAPActivate(client, args) {
|
|
|
938
1314
|
return errorResult(`Batch activation failed for: ${names}.\nErrors: ${result.messages.join('; ')}`);
|
|
939
1315
|
}
|
|
940
1316
|
// Single activation (existing behavior)
|
|
941
|
-
const name = String(args.name ?? '');
|
|
942
1317
|
const objectUrl = objectUrlForType(type, name);
|
|
943
1318
|
const result = await activate(client.http, client.safety, objectUrl);
|
|
944
1319
|
if (result.success) {
|
|
@@ -1020,8 +1395,63 @@ async function handleSAPNavigate(client, args) {
|
|
|
1020
1395
|
const proposals = await getCompletion(client.http, client.safety, uri, line, column, source);
|
|
1021
1396
|
return textResult(JSON.stringify(proposals, null, 2));
|
|
1022
1397
|
}
|
|
1398
|
+
case 'hierarchy': {
|
|
1399
|
+
const className = String(args.name ?? '').toUpperCase();
|
|
1400
|
+
if (!className) {
|
|
1401
|
+
return errorResult('Provide name (class name) for hierarchy lookup.');
|
|
1402
|
+
}
|
|
1403
|
+
// Sanitize to prevent SQL injection — class names are alphanumeric + underscore + namespace slash
|
|
1404
|
+
const safeName = className.replace(/[^A-Z0-9_/]/g, '');
|
|
1405
|
+
if (safeName !== className) {
|
|
1406
|
+
return errorResult(`Invalid class name: "${className}". Only alphanumeric characters, underscores, and slashes are allowed.`);
|
|
1407
|
+
}
|
|
1408
|
+
const canFreeSQL = isOperationAllowed(client.safety, OperationType.FreeSQL);
|
|
1409
|
+
const canQuery = isOperationAllowed(client.safety, OperationType.Query);
|
|
1410
|
+
if (!canFreeSQL && !canQuery) {
|
|
1411
|
+
return errorResult('Class hierarchy requires data access permissions. ' +
|
|
1412
|
+
'Enable free SQL (--block-free-sql=false) or table preview (--block-data=false).');
|
|
1413
|
+
}
|
|
1414
|
+
try {
|
|
1415
|
+
let ownRels;
|
|
1416
|
+
let subRels;
|
|
1417
|
+
if (canFreeSQL) {
|
|
1418
|
+
ownRels = await client.runQuery(`SELECT CLSNAME, REFCLSNAME, RELTYPE FROM SEOMETAREL WHERE CLSNAME = '${safeName}'`, 100);
|
|
1419
|
+
subRels = await client.runQuery(`SELECT CLSNAME FROM SEOMETAREL WHERE REFCLSNAME = '${safeName}' AND RELTYPE = '2'`, 100);
|
|
1420
|
+
}
|
|
1421
|
+
else {
|
|
1422
|
+
// Fall back to named table preview (Query op type)
|
|
1423
|
+
ownRels = await client.getTableContents('SEOMETAREL', 100, `CLSNAME = '${safeName}'`);
|
|
1424
|
+
subRels = await client.getTableContents('SEOMETAREL', 100, `REFCLSNAME = '${safeName}' AND RELTYPE = '2'`);
|
|
1425
|
+
}
|
|
1426
|
+
let superclass = null;
|
|
1427
|
+
const interfaces = [];
|
|
1428
|
+
for (let i = 0; i < ownRels.rows.length; i++) {
|
|
1429
|
+
const row = ownRels.rows[i];
|
|
1430
|
+
const reltype = String(row.RELTYPE ?? '').trim();
|
|
1431
|
+
const refName = String(row.REFCLSNAME ?? '').trim();
|
|
1432
|
+
if (reltype === '2') {
|
|
1433
|
+
superclass = refName;
|
|
1434
|
+
}
|
|
1435
|
+
else if (reltype === '1') {
|
|
1436
|
+
interfaces.push(refName);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
const subclasses = [];
|
|
1440
|
+
for (let i = 0; i < subRels.rows.length; i++) {
|
|
1441
|
+
subclasses.push(String(subRels.rows[i].CLSNAME ?? '').trim());
|
|
1442
|
+
}
|
|
1443
|
+
const result = { className: safeName, superclass, interfaces, subclasses };
|
|
1444
|
+
return textResult(JSON.stringify(result, null, 2));
|
|
1445
|
+
}
|
|
1446
|
+
catch (err) {
|
|
1447
|
+
if (err instanceof AdtApiError && err.statusCode === 404) {
|
|
1448
|
+
return errorResult('Cannot query SEOMETAREL — table may not be accessible on this system.');
|
|
1449
|
+
}
|
|
1450
|
+
throw err;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1023
1453
|
default:
|
|
1024
|
-
return errorResult(`Unknown SAPNavigate action: ${action}. Supported: definition, references, completion`);
|
|
1454
|
+
return errorResult(`Unknown SAPNavigate action: ${action}. Supported: definition, references, completion, hierarchy`);
|
|
1025
1455
|
}
|
|
1026
1456
|
}
|
|
1027
1457
|
// ─── SAPDiagnose Handler ─────────────────────────────────────────────
|
|
@@ -1112,6 +1542,8 @@ async function handleSAPTransport(client, args) {
|
|
|
1112
1542
|
if (!description)
|
|
1113
1543
|
return errorResult('Description is required for "create" action.');
|
|
1114
1544
|
const id = await createTransport(client.http, client.safety, description);
|
|
1545
|
+
if (!id)
|
|
1546
|
+
return errorResult('Transport creation succeeded but no transport ID was returned. Check the SAP system manually.');
|
|
1115
1547
|
return textResult(`Created transport request: ${id}`);
|
|
1116
1548
|
}
|
|
1117
1549
|
case 'release': {
|
|
@@ -1265,6 +1697,7 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
|
|
|
1265
1697
|
featureConfig.amdp = config.featureAmdp;
|
|
1266
1698
|
featureConfig.ui5 = config.featureUi5;
|
|
1267
1699
|
featureConfig.transport = config.featureTransport;
|
|
1700
|
+
featureConfig.ui5repo = config.featureUi5Repo;
|
|
1268
1701
|
const probed = await probeFeatures(client.http, featureConfig, config.systemType);
|
|
1269
1702
|
// In PP mode with a per-user client, auth-sensitive results (401/403 on any
|
|
1270
1703
|
// feature) must not poison the global cache — another user may have different
|