arc-1 0.9.5 → 0.9.7
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 +21 -3
- package/dist/adt/abapgit.d.ts +2 -1
- package/dist/adt/abapgit.d.ts.map +1 -1
- package/dist/adt/abapgit.js +2 -2
- package/dist/adt/abapgit.js.map +1 -1
- package/dist/adt/btp.d.ts.map +1 -1
- package/dist/adt/btp.js +7 -3
- package/dist/adt/btp.js.map +1 -1
- package/dist/adt/class-structure.d.ts +176 -0
- package/dist/adt/class-structure.d.ts.map +1 -0
- package/dist/adt/class-structure.js +317 -0
- package/dist/adt/class-structure.js.map +1 -0
- package/dist/adt/client.d.ts +150 -8
- package/dist/adt/client.d.ts.map +1 -1
- package/dist/adt/client.js +345 -12
- package/dist/adt/client.js.map +1 -1
- package/dist/adt/config.d.ts +7 -1
- package/dist/adt/config.d.ts.map +1 -1
- package/dist/adt/config.js.map +1 -1
- package/dist/adt/crud.d.ts +38 -0
- package/dist/adt/crud.d.ts.map +1 -1
- package/dist/adt/crud.js +73 -1
- package/dist/adt/crud.js.map +1 -1
- package/dist/adt/errors.d.ts +2 -2
- package/dist/adt/errors.d.ts.map +1 -1
- package/dist/adt/errors.js +50 -6
- package/dist/adt/errors.js.map +1 -1
- package/dist/adt/features.d.ts.map +1 -1
- package/dist/adt/features.js +27 -3
- package/dist/adt/features.js.map +1 -1
- package/dist/adt/gcts.d.ts +3 -2
- package/dist/adt/gcts.d.ts.map +1 -1
- package/dist/adt/gcts.js +4 -4
- package/dist/adt/gcts.js.map +1 -1
- package/dist/adt/http.d.ts +41 -0
- package/dist/adt/http.d.ts.map +1 -1
- package/dist/adt/http.js +132 -45
- package/dist/adt/http.js.map +1 -1
- package/dist/adt/package-hierarchy.d.ts +67 -0
- package/dist/adt/package-hierarchy.d.ts.map +1 -0
- package/dist/adt/package-hierarchy.js +100 -0
- package/dist/adt/package-hierarchy.js.map +1 -0
- package/dist/adt/release.d.ts +35 -0
- package/dist/adt/release.d.ts.map +1 -0
- package/dist/adt/release.js +48 -0
- package/dist/adt/release.js.map +1 -0
- package/dist/adt/safety.d.ts +39 -3
- package/dist/adt/safety.d.ts.map +1 -1
- package/dist/adt/safety.js +136 -15
- package/dist/adt/safety.js.map +1 -1
- package/dist/adt/types.d.ts +74 -0
- package/dist/adt/types.d.ts.map +1 -1
- package/dist/adt/xml-parser.d.ts +68 -1
- package/dist/adt/xml-parser.d.ts.map +1 -1
- package/dist/adt/xml-parser.js +263 -0
- package/dist/adt/xml-parser.js.map +1 -1
- package/dist/authz/policy.d.ts.map +1 -1
- package/dist/authz/policy.js +12 -0
- package/dist/authz/policy.js.map +1 -1
- package/dist/context/grep.d.ts +48 -0
- package/dist/context/grep.d.ts.map +1 -0
- package/dist/context/grep.js +146 -0
- package/dist/context/grep.js.map +1 -0
- package/dist/handlers/intent.d.ts +2 -1
- package/dist/handlers/intent.d.ts.map +1 -1
- package/dist/handlers/intent.js +614 -50
- package/dist/handlers/intent.js.map +1 -1
- package/dist/handlers/schemas.d.ts +52 -6
- package/dist/handlers/schemas.d.ts.map +1 -1
- package/dist/handlers/schemas.js +90 -9
- package/dist/handlers/schemas.js.map +1 -1
- package/dist/handlers/tools.d.ts.map +1 -1
- package/dist/handlers/tools.js +73 -12
- package/dist/handlers/tools.js.map +1 -1
- package/dist/lint/lint.d.ts.map +1 -1
- package/dist/lint/lint.js +6 -0
- package/dist/lint/lint.js.map +1 -1
- package/dist/lint/pre-write-hints.d.ts +45 -0
- package/dist/lint/pre-write-hints.d.ts.map +1 -0
- package/dist/lint/pre-write-hints.js +145 -0
- package/dist/lint/pre-write-hints.js.map +1 -0
- package/dist/server/audit.d.ts +27 -1
- package/dist/server/audit.d.ts.map +1 -1
- package/dist/server/audit.js.map +1 -1
- package/dist/server/auth-rate-limit.d.ts +78 -0
- package/dist/server/auth-rate-limit.d.ts.map +1 -0
- package/dist/server/auth-rate-limit.js +95 -0
- package/dist/server/auth-rate-limit.js.map +1 -0
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +32 -0
- package/dist/server/config.js.map +1 -1
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +73 -2
- package/dist/server/http.js.map +1 -1
- package/dist/server/mcp-rate-limit.d.ts +69 -0
- package/dist/server/mcp-rate-limit.d.ts.map +1 -0
- package/dist/server/mcp-rate-limit.js +92 -0
- package/dist/server/mcp-rate-limit.js.map +1 -0
- package/dist/server/server.d.ts +26 -6
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +87 -28
- package/dist/server/server.js.map +1 -1
- package/dist/server/types.d.ts +20 -1
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +2 -0
- package/dist/server/types.js.map +1 -1
- package/package.json +14 -12
package/dist/handlers/intent.js
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { checkRepo as abapGitCheckRepo, createBranch as abapGitCreateBranch, createRepo as abapGitCreateRepo, getExternalInfo as abapGitGetExternalInfo, listRepos as abapGitListRepos, pullRepo as abapGitPullRepo, pushRepo as abapGitPushRepo, stageRepo as abapGitStageRepo, switchBranch as abapGitSwitchBranch, unlinkRepo as abapGitUnlinkRepo, } from '../adt/abapgit.js';
|
|
13
13
|
import { buildSiblingExtensionFinding, classifyCdsImpact, deriveSiblingStem, isSiblingNameMatch, } from '../adt/cds-impact.js';
|
|
14
|
+
import { diffMethodSets, extractMethodNameFromClause, findSectionAnchor, insertMethodPair, moveMethodDefinition, removeMethodPair, spliceClassDefinition, spliceMethodSignature, } from '../adt/class-structure.js';
|
|
14
15
|
import { findDefinition, findInterfaceImplementersViaSeoMetaRel, findReferences, findWhereUsed, getCompletion, getWhereUsedScope, } from '../adt/codeintel.js';
|
|
15
|
-
import { createObject, deleteObject, lockObject, safeUpdateObject, safeUpdateSource, unlockObject, updateObject, updateSource, } from '../adt/crud.js';
|
|
16
|
+
import { createObject, deleteObject, lockObject, safeUpdateClassInclude, safeUpdateObject, safeUpdateSource, unlockObject, updateObject, updateSource, } from '../adt/crud.js';
|
|
16
17
|
import { buildDataElementXml, buildDomainXml, buildMessageClassXml, buildPackageXml, buildServiceBindingXml, decodeKtdText, rewriteKtdText, } from '../adt/ddic-xml.js';
|
|
17
18
|
import { activate, activateBatch, applyFixProposal, getFixProposals, getPrettyPrinterSettings, prettyPrint, publishServiceBinding, runAtcCheck, runUnitTests, setPrettyPrinterSettings, syntaxCheck, unpublishServiceBinding, } from '../adt/devtools.js';
|
|
18
19
|
import { getDump, getGatewayErrorDetail, getObjectState, getTraceDbAccesses, getTraceHitlist, getTraceStatements, listDumps, listGatewayErrors, listSystemMessages, listTraces, } from '../adt/diagnostics.js';
|
|
@@ -31,12 +32,14 @@ import { getAppInfo } from '../adt/ui5-repository.js';
|
|
|
31
32
|
import { validateAffHeader } from '../aff/validator.js';
|
|
32
33
|
import { extractCdsDependencies, extractCdsElements } from '../context/cds-deps.js';
|
|
33
34
|
import { compressCdsContext, compressContext } from '../context/compressor.js';
|
|
35
|
+
import { grepSource } from '../context/grep.js';
|
|
34
36
|
import { extractMethod, formatMethodListing, listMethods, spliceMethod } from '../context/method-surgery.js';
|
|
35
37
|
import { buildLintConfig, listRulesFromConfig, } from '../lint/config-builder.js';
|
|
36
38
|
import { detectFilename, lintAbapSource, lintAndFix, validateBeforeWrite } from '../lint/lint.js';
|
|
37
39
|
import { sanitizeArgs } from '../server/audit.js';
|
|
38
40
|
import { generateRequestId, requestContext } from '../server/context.js';
|
|
39
41
|
import { logger } from '../server/logger.js';
|
|
42
|
+
import { resolveRateLimitUserKey } from '../server/mcp-rate-limit.js';
|
|
40
43
|
import { expandHyperfocusedArgs } from './hyperfocused.js';
|
|
41
44
|
import { getToolSchema } from './schemas.js';
|
|
42
45
|
import { formatZodError } from './zod-errors.js';
|
|
@@ -225,8 +228,12 @@ function buildBaseErrorMessage(err, message, tool, args, config) {
|
|
|
225
228
|
if (err instanceof AdtApiError) {
|
|
226
229
|
// Append additional SAP messages (line numbers, secondary errors) if available
|
|
227
230
|
const enriched = enrichWithSapDetails(err, message);
|
|
228
|
-
const argType = String(args.type ?? '').toUpperCase();
|
|
229
|
-
|
|
231
|
+
const argType = canonicalTablType(String(args.type ?? '').toUpperCase());
|
|
232
|
+
// Pass the detected SAP_BASIS release so the 423 lock-handle hint can specialize
|
|
233
|
+
// (< 7.51 → point at abapfs_extensions; see issue #293). cachedFeatures is set by the
|
|
234
|
+
// startup probe; config.abapRelease is the manual SAP_ABAP_RELEASE override fallback.
|
|
235
|
+
const abapRelease = cachedFeatures?.abapRelease ?? config.abapRelease;
|
|
236
|
+
const classification = classifySapDomainError(err.statusCode, err.responseBody, err.path, abapRelease);
|
|
230
237
|
if (classification) {
|
|
231
238
|
const transactionLine = classification.transaction ? `\nSAP Transaction: ${classification.transaction}` : '';
|
|
232
239
|
return `${enriched}\n\nHint: ${classification.hint}${transactionLine}`;
|
|
@@ -262,6 +269,14 @@ function buildBaseErrorMessage(err, message, tool, args, config) {
|
|
|
262
269
|
`sqlFilter="MANDT = '100'" or sqlFilter="MATNR LIKE 'Z%'".`);
|
|
263
270
|
}
|
|
264
271
|
}
|
|
272
|
+
if (tool === 'SAPRead' && argType === 'TABLE_QUERY' && err.statusCode === 400) {
|
|
273
|
+
const combined = `${err.message}\n${err.responseBody ?? ''}`;
|
|
274
|
+
if (/is invalid here|due to grammar/i.test(combined)) {
|
|
275
|
+
return (`${enriched}\n\nHint: TABLE_QUERY parser error — check field names match the actual column names ` +
|
|
276
|
+
'exposed by the table/CDS view (use SAPRead(type="DDLS", include="elements") to inspect CDS view fields). ' +
|
|
277
|
+
'Also verify value formats (e.g. FiscalPeriod is C(2,0) so use "01" not "001").');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
265
280
|
const behaviorPoolHint = getBehaviorPoolSaveFailureHint(err, args);
|
|
266
281
|
if (behaviorPoolHint) {
|
|
267
282
|
return `${enriched}\n\nHint: ${behaviorPoolHint}`;
|
|
@@ -295,7 +310,7 @@ function buildBaseErrorMessage(err, message, tool, args, config) {
|
|
|
295
310
|
return enriched;
|
|
296
311
|
}
|
|
297
312
|
if (err instanceof AdtSafetyError) {
|
|
298
|
-
const argType = String(args.type ?? '').toUpperCase();
|
|
313
|
+
const argType = canonicalTablType(String(args.type ?? '').toUpperCase());
|
|
299
314
|
if (tool === 'SAPRead' && argType === 'TABLE_CONTENTS') {
|
|
300
315
|
return (`${message}\n\nHint: TABLE_CONTENTS is blocked by safety configuration or missing data scope. ` +
|
|
301
316
|
'Set SAP_ALLOW_DATA_PREVIEW=true at the server level and, in authenticated HTTP mode, ' +
|
|
@@ -658,7 +673,7 @@ async function inactiveSyntaxDiagnostic(client, type, name) {
|
|
|
658
673
|
}
|
|
659
674
|
}
|
|
660
675
|
async function tryPostSaveSyntaxCheck(client, type, name) {
|
|
661
|
-
if (!DDIC_POST_SAVE_CHECK_TYPES.has(type.toUpperCase()))
|
|
676
|
+
if (!DDIC_POST_SAVE_CHECK_TYPES.has(canonicalTablType(type.toUpperCase())))
|
|
662
677
|
return '';
|
|
663
678
|
return inactiveSyntaxDiagnostic(client, type, name);
|
|
664
679
|
}
|
|
@@ -746,7 +761,7 @@ function classifyError(err) {
|
|
|
746
761
|
* all tools are allowed (backward compatibility).
|
|
747
762
|
* @param server - MCP Server instance for elicitation support.
|
|
748
763
|
*/
|
|
749
|
-
export async function handleToolCall(client, config, toolName, args, authInfo, _server, cachingLayer, isPerUserClient) {
|
|
764
|
+
export async function handleToolCall(client, config, toolName, args, authInfo, _server, cachingLayer, isPerUserClient, mcpRateLimiter) {
|
|
750
765
|
const reqId = generateRequestId();
|
|
751
766
|
const start = Date.now();
|
|
752
767
|
// Build user context for audit logging
|
|
@@ -763,6 +778,47 @@ export async function handleToolCall(client, config, toolName, args, authInfo, _
|
|
|
763
778
|
tool: toolName,
|
|
764
779
|
args: sanitizeArgs(args),
|
|
765
780
|
});
|
|
781
|
+
// ─── Layer 2: per-user MCP tool-call rate limit ─────────────────────
|
|
782
|
+
// Applied immediately so we don't waste any work on denied calls. Stdio mode
|
|
783
|
+
// (no authInfo) is exempt — there's no user identity to key on. On denial we
|
|
784
|
+
// return an MCP tool error (not HTTP 429) so the LLM client surfaces it as a
|
|
785
|
+
// tool failure and the agent loop backs off via its own retry policy.
|
|
786
|
+
// See docs_page/rate-limiting.md (Layer 2). Cost weighting per tool is deferred
|
|
787
|
+
// to v2 — every consume call counts as one point.
|
|
788
|
+
if (mcpRateLimiter && authInfo) {
|
|
789
|
+
// Walks the most-specific identity claim first (userName → email → sub →
|
|
790
|
+
// preferred_username → clientId) so OIDC users sharing one `azp` clientId
|
|
791
|
+
// don't collapse into a single bucket. See resolveRateLimitUserKey.
|
|
792
|
+
const userKey = resolveRateLimitUserKey(authInfo);
|
|
793
|
+
const decision = await mcpRateLimiter.consume(userKey, toolName);
|
|
794
|
+
if (!decision.allowed) {
|
|
795
|
+
const retryAfter = Math.ceil(decision.retryAfterMs / 1000);
|
|
796
|
+
logger.emitAudit({
|
|
797
|
+
timestamp: new Date().toISOString(),
|
|
798
|
+
level: 'warn',
|
|
799
|
+
event: 'mcp_rate_limited',
|
|
800
|
+
requestId: reqId,
|
|
801
|
+
clientId,
|
|
802
|
+
user: userKey,
|
|
803
|
+
tool: toolName,
|
|
804
|
+
limitPerMinute: decision.limitPerMinute,
|
|
805
|
+
retryAfterMs: decision.retryAfterMs,
|
|
806
|
+
});
|
|
807
|
+
return {
|
|
808
|
+
content: [
|
|
809
|
+
{
|
|
810
|
+
type: 'text',
|
|
811
|
+
text: JSON.stringify({
|
|
812
|
+
error: 'rate_limited',
|
|
813
|
+
retryAfter,
|
|
814
|
+
message: `Rate limit exceeded (${decision.limitPerMinute}/min per user). Retry after ${retryAfter} seconds.`,
|
|
815
|
+
}),
|
|
816
|
+
},
|
|
817
|
+
],
|
|
818
|
+
isError: true,
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
}
|
|
766
822
|
// Unified scope enforcement via ACTION_POLICY — routes through action/type-aware lookup.
|
|
767
823
|
// For SAPRead, the policy key is Tool.{type}; for other action-bearing tools, Tool.{action};
|
|
768
824
|
// for tools without an action/type enum (SAPSearch, SAPQuery), the tool-level default applies.
|
|
@@ -945,6 +1001,28 @@ export async function handleToolCall(client, config, toolName, args, authInfo, _
|
|
|
945
1001
|
function isBtpSystem() {
|
|
946
1002
|
return cachedFeatures?.systemType === 'btp';
|
|
947
1003
|
}
|
|
1004
|
+
/** Return whether the SAP ADT discovery feed advertises the /sap/bc/adt/ddic/tables
|
|
1005
|
+
* collection (the transparent-table editor endpoint). Absent on NW 7.50/7.51 —
|
|
1006
|
+
* SAP added it in NW 7.52 along with the new database-table editor. When the
|
|
1007
|
+
* discovery cache is empty (e.g. probe never ran, tests that bypass SAPManage),
|
|
1008
|
+
* returns `undefined` so callers can decide whether to default-allow.
|
|
1009
|
+
* See issue #285. */
|
|
1010
|
+
function isTablesEndpointAvailable() {
|
|
1011
|
+
const map = cachedFeatures?.discoveryMap ?? cachedDiscovery;
|
|
1012
|
+
if (!map || map.size === 0)
|
|
1013
|
+
return undefined;
|
|
1014
|
+
return map.has('/sap/bc/adt/ddic/tables');
|
|
1015
|
+
}
|
|
1016
|
+
/** Stable hint surfaced when ARC-1 refuses a TABL/DT write because the connected
|
|
1017
|
+
* system does not expose /sap/bc/adt/ddic/tables/. Shared between the
|
|
1018
|
+
* resolver-driven update/delete/activate paths and the discovery-gated create
|
|
1019
|
+
* paths so the LLM always sees the same recovery instructions. */
|
|
1020
|
+
const TABL_DT_WRITE_UNAVAILABLE_HINT = 'Transparent table writes via ADT REST are not available on this system ' +
|
|
1021
|
+
'(/sap/bc/adt/ddic/tables/ is not exposed — NW 7.50/7.51 ship the DDIC ' +
|
|
1022
|
+
'structures endpoint only; the table editor was added in NW 7.52). ' +
|
|
1023
|
+
'Use SE11 in SAPGUI, or connect ARC-1 to an SAP_BASIS ≥ 7.52 system. ' +
|
|
1024
|
+
'Writing the source via /sap/bc/adt/ddic/structures/ would silently flip ' +
|
|
1025
|
+
'DD02L-TABCLASS to INTTAB and corrupt the table.';
|
|
948
1026
|
/** BTP-specific error messages for unavailable operations */
|
|
949
1027
|
const BTP_HINTS = {
|
|
950
1028
|
PROG: 'Executable programs (reports) are not available on BTP ABAP Environment. Use CLAS with IF_OO_ADT_CLASSRUN for console applications.',
|
|
@@ -1041,6 +1119,11 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
1041
1119
|
const indicator = cacheHit && revalidated ? '[cached:revalidated]\n' : '';
|
|
1042
1120
|
return textResult(`${note}${indicator}${source}`);
|
|
1043
1121
|
};
|
|
1122
|
+
/** When args.grep is set, return only matching source lines (+context) instead of full source. */
|
|
1123
|
+
const grepText = (source) => {
|
|
1124
|
+
const g = grepSource(source, String(args.grep));
|
|
1125
|
+
return g.invalidPattern ? errorResult(g.output) : textResult(g.output);
|
|
1126
|
+
};
|
|
1044
1127
|
// Structured format is only supported for CLAS type
|
|
1045
1128
|
if (args.format === 'structured' && type !== 'CLAS') {
|
|
1046
1129
|
return errorResult('The "structured" format is only supported for CLAS type. Other types return text format.');
|
|
@@ -1048,9 +1131,45 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
1048
1131
|
switch (type) {
|
|
1049
1132
|
case 'PROG': {
|
|
1050
1133
|
const { source, cacheHit, revalidated } = await cachedGet('PROG', name, effectiveVersion, (ifNoneMatch) => client.getProgram(name, { ifNoneMatch, version: effectiveVersion }));
|
|
1134
|
+
if (args.grep)
|
|
1135
|
+
return grepText(source);
|
|
1051
1136
|
return cachedTextResult(source, cacheHit, revalidated, versionWarning);
|
|
1052
1137
|
}
|
|
1053
1138
|
case 'CLAS': {
|
|
1139
|
+
// grep: return only matching source lines (+context), annotated with the owning class/method.
|
|
1140
|
+
if (args.grep) {
|
|
1141
|
+
if (args.method) {
|
|
1142
|
+
return errorResult('Do not combine grep with method. Use grep to find code, then method="<name>" to read the full method.');
|
|
1143
|
+
}
|
|
1144
|
+
const rawSection = args.include;
|
|
1145
|
+
// 'main' (and the default) live at /source/main, not /includes/main — read via the
|
|
1146
|
+
// cached main path; only real sub-includes go through the raw getClassInclude endpoint.
|
|
1147
|
+
const section = rawSection && rawSection.toLowerCase() !== 'main' ? rawSection : undefined;
|
|
1148
|
+
let clasSource;
|
|
1149
|
+
if (section) {
|
|
1150
|
+
try {
|
|
1151
|
+
clasSource = (await client.getClassInclude(name, section, { version: effectiveVersion })).source;
|
|
1152
|
+
}
|
|
1153
|
+
catch (err) {
|
|
1154
|
+
if (isNotFoundError(err)) {
|
|
1155
|
+
return textResult(`Include "${section}" is not available for class ${name}. Run grep without include= to search the full class source.`);
|
|
1156
|
+
}
|
|
1157
|
+
throw err;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
else {
|
|
1161
|
+
clasSource = (await cachedGet('CLAS', name, effectiveVersion, (ifNoneMatch) => client.getClass(name, undefined, { ifNoneMatch, version: effectiveVersion }))).source;
|
|
1162
|
+
}
|
|
1163
|
+
const abaplintVer = cachedFeatures?.abapRelease
|
|
1164
|
+
? mapSapReleaseToAbaplintVersion(cachedFeatures.abapRelease)
|
|
1165
|
+
: undefined;
|
|
1166
|
+
// MethodInfo is a structural superset of grepSource's MethodRange — pass through directly.
|
|
1167
|
+
const listing = listMethods(clasSource, name, abaplintVer);
|
|
1168
|
+
const g = grepSource(clasSource, String(args.grep), listing.success ? { methods: listing.methods } : undefined);
|
|
1169
|
+
return g.invalidPattern
|
|
1170
|
+
? errorResult(g.output)
|
|
1171
|
+
: textResult(`[${name} section=${rawSection ?? 'main'}]\n${g.output}`);
|
|
1172
|
+
}
|
|
1054
1173
|
// Structured format: return JSON with metadata + decomposed source
|
|
1055
1174
|
if (args.format === 'structured') {
|
|
1056
1175
|
const structured = await client.getClassStructured(name);
|
|
@@ -1085,6 +1204,8 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
1085
1204
|
}
|
|
1086
1205
|
case 'INTF': {
|
|
1087
1206
|
const { source, cacheHit, revalidated } = await cachedGet('INTF', name, effectiveVersion, (ifNoneMatch) => client.getInterface(name, { ifNoneMatch, version: effectiveVersion }));
|
|
1207
|
+
if (args.grep)
|
|
1208
|
+
return grepText(source);
|
|
1088
1209
|
return cachedTextResult(source, cacheHit, revalidated, versionWarning);
|
|
1089
1210
|
}
|
|
1090
1211
|
case 'FUNC': {
|
|
@@ -1120,6 +1241,8 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
1120
1241
|
};
|
|
1121
1242
|
return textResult(JSON.stringify(payload, null, 2));
|
|
1122
1243
|
}
|
|
1244
|
+
if (args.grep)
|
|
1245
|
+
return grepText(source);
|
|
1123
1246
|
return cachedTextResult(source, cacheHit, revalidated, versionWarning);
|
|
1124
1247
|
}
|
|
1125
1248
|
case 'FUGR': {
|
|
@@ -1147,6 +1270,8 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
1147
1270
|
}
|
|
1148
1271
|
case 'INCL': {
|
|
1149
1272
|
const { source, cacheHit, revalidated } = await cachedGet('INCL', name, effectiveVersion, (ifNoneMatch) => client.getInclude(name, { ifNoneMatch, version: effectiveVersion }));
|
|
1273
|
+
if (args.grep)
|
|
1274
|
+
return grepText(source);
|
|
1150
1275
|
return cachedTextResult(source, cacheHit, revalidated, versionWarning);
|
|
1151
1276
|
}
|
|
1152
1277
|
case 'DDLS': {
|
|
@@ -1159,23 +1284,33 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
1159
1284
|
// Elements extraction is derived from source — no cache indicator
|
|
1160
1285
|
return cachedTextResult(extractCdsElements(ddlSource, name), false, false, versionWarning);
|
|
1161
1286
|
}
|
|
1287
|
+
if (args.grep)
|
|
1288
|
+
return grepText(ddlSource);
|
|
1162
1289
|
return cachedTextResult(ddlSource, cacheHit, revalidated, versionWarning);
|
|
1163
1290
|
}
|
|
1164
1291
|
case 'DCLS': {
|
|
1165
1292
|
const { source, cacheHit, revalidated } = await cachedGet('DCLS', name, effectiveVersion, (ifNoneMatch) => client.getDcl(name, { ifNoneMatch, version: effectiveVersion }));
|
|
1293
|
+
if (args.grep)
|
|
1294
|
+
return grepText(source);
|
|
1166
1295
|
return cachedTextResult(source, cacheHit, revalidated, versionWarning);
|
|
1167
1296
|
}
|
|
1168
1297
|
case 'BDEF': {
|
|
1169
1298
|
const { source, cacheHit, revalidated } = await cachedGet('BDEF', name, effectiveVersion, (ifNoneMatch) => client.getBdef(name, { ifNoneMatch, version: effectiveVersion }));
|
|
1299
|
+
if (args.grep)
|
|
1300
|
+
return grepText(source);
|
|
1170
1301
|
return cachedTextResult(source, cacheHit, revalidated, versionWarning);
|
|
1171
1302
|
}
|
|
1172
1303
|
case 'SRVD': {
|
|
1173
1304
|
const { source, cacheHit, revalidated } = await cachedGet('SRVD', name, effectiveVersion, (ifNoneMatch) => client.getSrvd(name, { ifNoneMatch, version: effectiveVersion }));
|
|
1305
|
+
if (args.grep)
|
|
1306
|
+
return grepText(source);
|
|
1174
1307
|
return cachedTextResult(source, cacheHit, revalidated, versionWarning);
|
|
1175
1308
|
}
|
|
1176
1309
|
case 'DDLX': {
|
|
1177
1310
|
try {
|
|
1178
1311
|
const { source, cacheHit, revalidated } = await cachedGet('DDLX', name, effectiveVersion, (ifNoneMatch) => client.getDdlx(name, { ifNoneMatch, version: effectiveVersion }));
|
|
1312
|
+
if (args.grep)
|
|
1313
|
+
return grepText(source);
|
|
1179
1314
|
return cachedTextResult(source, cacheHit, revalidated, versionWarning);
|
|
1180
1315
|
}
|
|
1181
1316
|
catch (err) {
|
|
@@ -1209,10 +1344,14 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
1209
1344
|
// client.getTabl() handles the /tables/ → /structures/ fallback internally
|
|
1210
1345
|
// and caches the resolved URL for subsequent write/activate paths.
|
|
1211
1346
|
const { source, cacheHit, revalidated } = await cachedGet('TABL', name, effectiveVersion, (ifNoneMatch) => client.getTabl(name, { ifNoneMatch, version: effectiveVersion }));
|
|
1347
|
+
if (args.grep)
|
|
1348
|
+
return grepText(source);
|
|
1212
1349
|
return cachedTextResult(source, cacheHit, revalidated, versionWarning);
|
|
1213
1350
|
}
|
|
1214
1351
|
case 'VIEW': {
|
|
1215
1352
|
const { source, cacheHit, revalidated } = await cachedGet('VIEW', name, effectiveVersion, (ifNoneMatch) => client.getView(name, { ifNoneMatch, version: effectiveVersion }));
|
|
1353
|
+
if (args.grep)
|
|
1354
|
+
return grepText(source);
|
|
1216
1355
|
return cachedTextResult(source, cacheHit, revalidated, versionWarning);
|
|
1217
1356
|
}
|
|
1218
1357
|
case 'DOMA': {
|
|
@@ -1319,6 +1458,15 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
1319
1458
|
const data = await client.getTableContents(name, maxRows, args.sqlFilter);
|
|
1320
1459
|
return textResult(JSON.stringify(data, null, 2));
|
|
1321
1460
|
}
|
|
1461
|
+
case 'TABLE_QUERY': {
|
|
1462
|
+
const maxRows = Number(args.maxRows ?? 100);
|
|
1463
|
+
const columns = Array.isArray(args.columns) ? args.columns : undefined;
|
|
1464
|
+
const where = Array.isArray(args.where)
|
|
1465
|
+
? args.where
|
|
1466
|
+
: undefined;
|
|
1467
|
+
const data = await client.runTableQuery(name, { columns, where, maxRows });
|
|
1468
|
+
return textResult(JSON.stringify(data, null, 2));
|
|
1469
|
+
}
|
|
1322
1470
|
case 'SOBJ': {
|
|
1323
1471
|
const method = String(args.method ?? '');
|
|
1324
1472
|
// Sanitize inputs to prevent SQL injection — BOR names are alphanumeric + underscore only
|
|
@@ -1478,7 +1626,7 @@ async function handleSAPSearch(client, args) {
|
|
|
1478
1626
|
// keeps the same logical object from appearing twice in the merged matches.
|
|
1479
1627
|
// Preserve the more-specific slash form when both originate from ADT+DB.
|
|
1480
1628
|
const seen = new Map();
|
|
1481
|
-
const baseKey = (m) => `${(m.objectType.split('/')[0] || m.objectType).toUpperCase()}
|
|
1629
|
+
const baseKey = (m) => `${(m.objectType.split('/')[0] || m.objectType).toUpperCase()}\x00${m.objectName.toUpperCase()}`;
|
|
1482
1630
|
for (const m of adtMatches)
|
|
1483
1631
|
seen.set(baseKey(m), m);
|
|
1484
1632
|
for (const m of dbMatches) {
|
|
@@ -2253,18 +2401,24 @@ export function buildCreateXml(type, name, pkg, description, properties) {
|
|
|
2253
2401
|
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
2254
2402
|
</dcl:dclSource>`;
|
|
2255
2403
|
case 'TABL':
|
|
2256
|
-
|
|
2404
|
+
case 'TABL/DT':
|
|
2405
|
+
case 'TABL/DS': {
|
|
2406
|
+
// Bare TABL is the legacy alias for TABL/DT (transparent table). The same
|
|
2407
|
+
// <blue:blueSource> envelope works for both subtypes — only adtcore:type
|
|
2408
|
+
// and the POST URL differ. See docs/plans/completed/fix-tabl-ds-create-routing.md.
|
|
2409
|
+
const adtType = type === 'TABL/DS' ? 'TABL/DS' : 'TABL/DT';
|
|
2257
2410
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2258
2411
|
<blue:blueSource xmlns:blue="http://www.sap.com/wbobj/blue"
|
|
2259
2412
|
xmlns:adtcore="http://www.sap.com/adt/core"
|
|
2260
2413
|
adtcore:description="${escapeXml(description)}"
|
|
2261
2414
|
adtcore:name="${escapeXml(name)}"
|
|
2262
|
-
adtcore:type="
|
|
2415
|
+
adtcore:type="${adtType}"
|
|
2263
2416
|
adtcore:masterLanguage="EN"
|
|
2264
2417
|
adtcore:masterSystem="H00"
|
|
2265
2418
|
adtcore:responsible="DEVELOPER">
|
|
2266
2419
|
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
2267
2420
|
</blue:blueSource>`;
|
|
2421
|
+
}
|
|
2268
2422
|
case 'BDEF':
|
|
2269
2423
|
// BDEF uses SAP's "blue" framework — blue:blueSource with http://www.sap.com/wbobj/blue namespace.
|
|
2270
2424
|
// Confirmed by vibing-steampunk (Go) and fr0ster (TypeScript) reference implementations.
|
|
@@ -2569,6 +2723,37 @@ export function normalizeObjectType(type) {
|
|
|
2569
2723
|
return '';
|
|
2570
2724
|
return SLASH_TYPE_MAP[normalized] ?? normalized;
|
|
2571
2725
|
}
|
|
2726
|
+
/** TABL subtypes that SAPWrite preserves (instead of collapsing to bare 'TABL' via
|
|
2727
|
+
* SLASH_TYPE_MAP) so the create path can route TABL/DT → /ddic/tables and
|
|
2728
|
+
* TABL/DS → /ddic/structures. See docs/plans/completed/fix-tabl-ds-create-routing.md. */
|
|
2729
|
+
const TABL_WRITE_SUBTYPES = new Set(['TABL/DT', 'TABL/DS']);
|
|
2730
|
+
/** Legacy slash-form aliases SAPWrite remaps to a canonical subtype before
|
|
2731
|
+
* SLASH_TYPE_MAP runs — otherwise STRU/DS would collapse to bare 'TABL' and
|
|
2732
|
+
* route the structure create to /ddic/tables. */
|
|
2733
|
+
const SAPWRITE_TABL_ALIAS = {
|
|
2734
|
+
'STRU/DS': 'TABL/DS',
|
|
2735
|
+
};
|
|
2736
|
+
/** SAPWrite-only normalizer: preserves TABL/DT and TABL/DS and remaps STRU/DS
|
|
2737
|
+
* to TABL/DS. Every other tool keeps the global collapsing behaviour of
|
|
2738
|
+
* `normalizeObjectType`. */
|
|
2739
|
+
function normalizeWriteObjectType(type) {
|
|
2740
|
+
const normalized = String(type).trim().toUpperCase();
|
|
2741
|
+
if (!normalized)
|
|
2742
|
+
return '';
|
|
2743
|
+
const aliased = SAPWRITE_TABL_ALIAS[normalized];
|
|
2744
|
+
if (aliased)
|
|
2745
|
+
return aliased;
|
|
2746
|
+
if (TABL_WRITE_SUBTYPES.has(normalized))
|
|
2747
|
+
return normalized;
|
|
2748
|
+
return SLASH_TYPE_MAP[normalized] ?? normalized;
|
|
2749
|
+
}
|
|
2750
|
+
/** Collapse TABL/DT and TABL/DS back to bare 'TABL' for downstream Set-membership
|
|
2751
|
+
* checks (DDIC hints, RAP preflight, CDS dependency hints, cache invalidation)
|
|
2752
|
+
* that only know about canonical types. The slash form survives at URL routing
|
|
2753
|
+
* + XML envelope sites. */
|
|
2754
|
+
function canonicalTablType(type) {
|
|
2755
|
+
return type === 'TABL/DT' || type === 'TABL/DS' ? 'TABL' : type;
|
|
2756
|
+
}
|
|
2572
2757
|
/** Normalize type fields before schema validation so slash/case aliases are accepted. */
|
|
2573
2758
|
function normalizeTypeArgsForValidation(toolName, args) {
|
|
2574
2759
|
switch (toolName) {
|
|
@@ -2579,14 +2764,15 @@ function normalizeTypeArgsForValidation(toolName, args) {
|
|
|
2579
2764
|
objectType: args.objectType === undefined ? undefined : normalizeObjectType(String(args.objectType ?? '')),
|
|
2580
2765
|
};
|
|
2581
2766
|
case 'SAPWrite':
|
|
2767
|
+
// SAPWrite preserves TABL/DT and TABL/DS so the create path can route by subtype.
|
|
2582
2768
|
return {
|
|
2583
2769
|
...args,
|
|
2584
|
-
type: args.type === undefined ? undefined :
|
|
2770
|
+
type: args.type === undefined ? undefined : normalizeWriteObjectType(String(args.type ?? '')),
|
|
2585
2771
|
objects: Array.isArray(args.objects)
|
|
2586
2772
|
? args.objects.map((obj) => typeof obj === 'object' && obj !== null
|
|
2587
2773
|
? {
|
|
2588
2774
|
...obj,
|
|
2589
|
-
type:
|
|
2775
|
+
type: normalizeWriteObjectType(String(obj.type ?? '')),
|
|
2590
2776
|
}
|
|
2591
2777
|
: obj)
|
|
2592
2778
|
: args.objects,
|
|
@@ -2688,10 +2874,13 @@ export function objectBasePath(type) {
|
|
|
2688
2874
|
case 'SRVB':
|
|
2689
2875
|
return '/sap/bc/adt/businessservices/bindings/';
|
|
2690
2876
|
case 'TABL':
|
|
2691
|
-
|
|
2692
|
-
//
|
|
2693
|
-
// AdtClient.resolveTablObjectUrl(name) which falls back on 404.
|
|
2877
|
+
case 'TABL/DT':
|
|
2878
|
+
// Bare TABL defaults to transparent table. For reads, callers should use
|
|
2879
|
+
// AdtClient.resolveTablObjectUrl(name) which falls back to /structures/ on 404.
|
|
2694
2880
|
return '/sap/bc/adt/ddic/tables/';
|
|
2881
|
+
case 'TABL/DS':
|
|
2882
|
+
// DDIC structures only route through this collection; see follow-up to #285.
|
|
2883
|
+
return '/sap/bc/adt/ddic/structures/';
|
|
2695
2884
|
case 'DOMA':
|
|
2696
2885
|
return '/sap/bc/adt/ddic/domains/';
|
|
2697
2886
|
case 'DTEL':
|
|
@@ -2819,13 +3008,23 @@ function stripIncludeHeader(source) {
|
|
|
2819
3008
|
return source.replace(/^=== \w+ ===\n/, '');
|
|
2820
3009
|
}
|
|
2821
3010
|
// ─── SAPWrite Handler ────────────────────────────────────────────────
|
|
3011
|
+
/**
|
|
3012
|
+
* Single-object actions whose top-level `name` is an SAP object name and must be
|
|
3013
|
+
* uppercase (TADIR convention). `batch_create` is excluded — its names live in
|
|
3014
|
+
* the `objects[]` items and are validated per item in the batch_create branch.
|
|
3015
|
+
*/
|
|
3016
|
+
const NAME_CASE_GUARD_ACTIONS = new Set(['create', 'update', 'edit_method', 'delete']);
|
|
2822
3017
|
async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
2823
3018
|
const action = String(args.action ?? '');
|
|
2824
|
-
const type =
|
|
3019
|
+
const type = normalizeWriteObjectType(String(args.type ?? ''));
|
|
2825
3020
|
const name = String(args.name ?? '');
|
|
2826
3021
|
const source = String(args.source ?? '');
|
|
2827
3022
|
const hasSource = typeof args.source === 'string';
|
|
2828
3023
|
const include = normalizeClassWriteInclude(args.include);
|
|
3024
|
+
// Whether a non-empty include was actually requested. Some MCP clients serialize
|
|
3025
|
+
// an omitted optional string as "" — treat empty/whitespace as "not provided" so
|
|
3026
|
+
// those clients aren't rejected with a bogus "Invalid CLAS include" on the MAIN path.
|
|
3027
|
+
const includeProvided = typeof args.include === 'string' && args.include.trim() !== '';
|
|
2829
3028
|
const transport = args.transport;
|
|
2830
3029
|
const lintOverride = args.lintBeforeWrite;
|
|
2831
3030
|
const preflightOverride = args.preflightBeforeWrite;
|
|
@@ -2835,12 +3034,16 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2835
3034
|
return errorResult('"type" and "name" are required for this action.');
|
|
2836
3035
|
}
|
|
2837
3036
|
// SAP TADIR stores object names uppercase. Mixed-case names cause silent corruption
|
|
2838
|
-
// (e.g. DDLS
|
|
2839
|
-
// still contains "Zc_MyView", confusing every downstream tool)
|
|
2840
|
-
//
|
|
3037
|
+
// on create (e.g. DDLS "Zc_MyView" registers as "ZC_MYVIEW" in TADIR but the source body
|
|
3038
|
+
// still contains "Zc_MyView", confusing every downstream tool) and broken URL lookups on
|
|
3039
|
+
// mutate/delete — the lock is held against the canonical uppercase name while the request
|
|
3040
|
+
// URL carries the mixed-case one, which surfaces on ECC as 423 "... is not locked" (issue
|
|
3041
|
+
// #293, original report used name "Z_HELLO_world"). Reject pre-flight for every name-bearing
|
|
3042
|
+
// single-object action — universal SAP convention, not a 7.50 quirk. (batch_create validates
|
|
3043
|
+
// each item separately below.)
|
|
2841
3044
|
// Note: source code INSIDE the object can use mixed case (e.g. for DDLS: name="ZC_MYVIEW"
|
|
2842
3045
|
// but `define view entity Zc_MyView` is fine inside the source body).
|
|
2843
|
-
if (action
|
|
3046
|
+
if (NAME_CASE_GUARD_ACTIONS.has(action) && name && name !== name.toUpperCase()) {
|
|
2844
3047
|
return errorResult(`Object name "${name}" contains lowercase characters. SAP object names must be uppercase (e.g. "${name.toUpperCase()}").\n\n` +
|
|
2845
3048
|
`Note: the object NAME in TADIR must be uppercase, but the source code inside the object can use mixed case ` +
|
|
2846
3049
|
`(e.g. for DDLS: name="${name.toUpperCase()}" but source can contain "define view entity ${name}").`);
|
|
@@ -2860,8 +3063,22 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2860
3063
|
// `buildCreateXml('FUNC', …, properties)` finds it.
|
|
2861
3064
|
let objectUrl;
|
|
2862
3065
|
let srcUrl;
|
|
2863
|
-
if (type === 'TABL'
|
|
2864
|
-
|
|
3066
|
+
if ((type === 'TABL' || type === 'TABL/DT' || type === 'TABL/DS') &&
|
|
3067
|
+
action !== 'create' &&
|
|
3068
|
+
action !== 'batch_create') {
|
|
3069
|
+
// All TABL forms route through the search-first resolver on update/delete/activate
|
|
3070
|
+
// so the PR #286 SE11-hint refusal applies even when callers pass an explicit slash form.
|
|
3071
|
+
try {
|
|
3072
|
+
objectUrl = await client.resolveTablObjectUrlForWrite(name, {
|
|
3073
|
+
tablesEndpointAvailable: isTablesEndpointAvailable(),
|
|
3074
|
+
});
|
|
3075
|
+
}
|
|
3076
|
+
catch (resolveErr) {
|
|
3077
|
+
if (resolveErr instanceof AdtSafetyError) {
|
|
3078
|
+
return errorResult(resolveErr.message);
|
|
3079
|
+
}
|
|
3080
|
+
throw resolveErr;
|
|
3081
|
+
}
|
|
2865
3082
|
srcUrl = `${objectUrl}/source/main`;
|
|
2866
3083
|
}
|
|
2867
3084
|
else if (type === 'FUNC') {
|
|
@@ -2886,23 +3103,53 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2886
3103
|
args.group = group;
|
|
2887
3104
|
}
|
|
2888
3105
|
else {
|
|
3106
|
+
// Discovery gate: refuse transparent-table creates upfront on systems that
|
|
3107
|
+
// don't expose /ddic/tables/ (NW 7.50/7.51). TABL/DS skips this — /structures/
|
|
3108
|
+
// is always available. See issue #285.
|
|
3109
|
+
if ((type === 'TABL' || type === 'TABL/DT') && (action === 'create' || action === 'batch_create')) {
|
|
3110
|
+
if (isTablesEndpointAvailable() === false) {
|
|
3111
|
+
return errorResult(TABL_DT_WRITE_UNAVAILABLE_HINT);
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
2889
3114
|
objectUrl = objectUrlForType(type, name);
|
|
2890
3115
|
srcUrl = sourceUrlForType(type, name);
|
|
2891
3116
|
}
|
|
2892
3117
|
const invalidateWrittenObject = (objType = type, objName = name) => {
|
|
2893
|
-
|
|
3118
|
+
// Source cache is keyed by canonical type (SAPRead collapses TABL/DT, TABL/DS).
|
|
3119
|
+
cachingLayer?.invalidate(canonicalTablType(objType), objName, 'all');
|
|
2894
3120
|
cachingLayer?.inactiveLists.invalidate(client.username);
|
|
2895
3121
|
};
|
|
2896
3122
|
// Helper: enforce allowedPackages for existing objects (update/delete/edit_method/scaffold_rap_handlers).
|
|
2897
3123
|
// Only fetches metadata when package restrictions are configured — no extra HTTP call otherwise.
|
|
3124
|
+
// Fail-closed: if the package cannot be determined from ADT metadata, refuse the write
|
|
3125
|
+
// rather than silently passing through the allowlist gate.
|
|
2898
3126
|
async function enforcePackageForExistingObject() {
|
|
2899
3127
|
if (client.safety.allowedPackages.length === 0)
|
|
2900
3128
|
return undefined;
|
|
2901
3129
|
const pkg = await client.resolveObjectPackage(objectUrl);
|
|
2902
|
-
if (pkg)
|
|
2903
|
-
|
|
3130
|
+
if (!pkg) {
|
|
3131
|
+
throw new AdtSafetyError(`Operations on ${type} '${name}' blocked: ARC-1 could not determine the object's package ` +
|
|
3132
|
+
`from ADT metadata (no adtcore:packageRef in response). Fail-closed because allowedPackages is restricted.`);
|
|
3133
|
+
}
|
|
3134
|
+
await checkPackage(client.safety, pkg, client.getPackageHierarchyResolver());
|
|
2904
3135
|
return pkg;
|
|
2905
3136
|
}
|
|
3137
|
+
// Helper for class-section surgery (issue #303): fetch the class structure AND
|
|
3138
|
+
// /source/main at the SAME effective version, so the spliced line ranges line
|
|
3139
|
+
// up with the bytes being edited. resolveVersionAndDraftInfo picks 'inactive'
|
|
3140
|
+
// when an unactivated draft exists. We pass that version to BOTH getClassStructure
|
|
3141
|
+
// (the /objectstructure?version= read) and the source read, AND to the cache opts
|
|
3142
|
+
// (so inactive bytes aren't cached under the 'active' key). Without this, a chained
|
|
3143
|
+
// surgery call on a draft would splice active-version line ranges into inactive
|
|
3144
|
+
// source and silently corrupt the draft.
|
|
3145
|
+
async function fetchClassStructureAndMain(clsName) {
|
|
3146
|
+
const { effectiveVersion } = await resolveVersionAndDraftInfo(client, cachingLayer, 'CLAS', clsName, 'auto');
|
|
3147
|
+
const structure = await client.getClassStructure(clsName, effectiveVersion);
|
|
3148
|
+
const main = cachingLayer
|
|
3149
|
+
? (await cachingLayer.getSource('CLAS', clsName, (ifNoneMatch) => client.getClass(clsName, undefined, { ifNoneMatch, version: effectiveVersion }), { version: effectiveVersion })).source
|
|
3150
|
+
: (await client.getClass(clsName, undefined, { version: effectiveVersion })).source;
|
|
3151
|
+
return { structure, main, effectiveVersion };
|
|
3152
|
+
}
|
|
2906
3153
|
switch (action) {
|
|
2907
3154
|
case 'update': {
|
|
2908
3155
|
const existingPackage = await enforcePackageForExistingObject();
|
|
@@ -2919,9 +3166,14 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2919
3166
|
if (!hasSource) {
|
|
2920
3167
|
return errorResult('"source" is required when updating a CLAS include.');
|
|
2921
3168
|
}
|
|
2922
|
-
|
|
3169
|
+
// Auto-initialise the include if it doesn't exist yet. On a fresh class
|
|
3170
|
+
// the testclasses (CCAU) include is absent — a content PUT alone fails
|
|
3171
|
+
// with HTTP 500 "…CCAU does not have any inactive version". safeUpdateClassInclude
|
|
3172
|
+
// probes the include and POST-creates it (under the same lock) before the PUT.
|
|
3173
|
+
const { initialized } = await safeUpdateClassInclude(client.http, client.safety, objectUrl, classIncludeUrl(name, include), source, transport, cachedFeatures?.abapRelease);
|
|
2923
3174
|
invalidateWrittenObject(type, name);
|
|
2924
|
-
|
|
3175
|
+
const initNote = initialized ? ` (initialised the ${include} include first)` : '';
|
|
3176
|
+
return textResult(`Successfully updated ${type} ${name} include ${include}${initNote}. Active version remains unchanged until activation; read with SAPRead(version="inactive") to verify the draft.`);
|
|
2925
3177
|
}
|
|
2926
3178
|
if (type === 'SKTD') {
|
|
2927
3179
|
// KTD update requires the full <sktd:docu> XML envelope with the Markdown
|
|
@@ -3022,7 +3274,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
3022
3274
|
}
|
|
3023
3275
|
case 'create': {
|
|
3024
3276
|
const pkg = String(args.package ?? '$TMP');
|
|
3025
|
-
checkPackage(client.safety, pkg);
|
|
3277
|
+
await checkPackage(client.safety, pkg, client.getPackageHierarchyResolver());
|
|
3026
3278
|
const description = String(args.description ?? name);
|
|
3027
3279
|
// Pre-flight: check transport requirements for non-$TMP packages when no transport provided.
|
|
3028
3280
|
// SAP requires a transport number for objects in transportable packages.
|
|
@@ -3135,7 +3387,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
3135
3387
|
// 'application/*' — the wildcard lets the SAP server resolve the correct
|
|
3136
3388
|
// handler (matching how ADT Eclipse and abap-adt-api send requests).
|
|
3137
3389
|
const contentType = createContentTypeForType(type);
|
|
3138
|
-
const needsPackageParam = type === 'BDEF' || type === 'TABL';
|
|
3390
|
+
const needsPackageParam = type === 'BDEF' || type === 'TABL' || type === 'TABL/DT' || type === 'TABL/DS';
|
|
3139
3391
|
let result;
|
|
3140
3392
|
try {
|
|
3141
3393
|
result = await createObject(client.http, client.safety, createUrl, body, contentType, effectiveTransport, needsPackageParam ? pkg : undefined, cachedFeatures?.abapRelease);
|
|
@@ -3338,6 +3590,261 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
3338
3590
|
const extras = [lintWarnings.warnings, checkNotes].filter(Boolean).join('\n\n');
|
|
3339
3591
|
return extras ? textResult(`${msg}\n\n${extras}`) : textResult(msg);
|
|
3340
3592
|
}
|
|
3593
|
+
// ─── Class-section surgery actions (issue #303) ─────────────────────
|
|
3594
|
+
//
|
|
3595
|
+
// Four actions share a common shape: fetch objectstructure → optional
|
|
3596
|
+
// diff/refuse → splice into /source/main (or /includes/<inc> when
|
|
3597
|
+
// include= is set) → PUT under lock → no auto-activate.
|
|
3598
|
+
//
|
|
3599
|
+
// Pre-write lint runs on the SPLICED FULL source (not the partial input
|
|
3600
|
+
// fragment) because a raw DEFINITION block alone fails abaplint with
|
|
3601
|
+
// "Expected CLASSIMPLEMENTATION" — verified live on a4h. Lint is skipped
|
|
3602
|
+
// for include= writes (same precedent as `update include=` path).
|
|
3603
|
+
case 'edit_class_definition': {
|
|
3604
|
+
if (type !== 'CLAS')
|
|
3605
|
+
return errorResult('edit_class_definition is only supported for type=CLAS.');
|
|
3606
|
+
if (!hasSource)
|
|
3607
|
+
return errorResult('"source" (new CLASS DEFINITION block) is required for edit_class_definition.');
|
|
3608
|
+
if (includeProvided && !include) {
|
|
3609
|
+
return errorResult(`Invalid CLAS include "${String(args.include)}". Valid values: ${CLASS_WRITE_INCLUDES.join(', ')}.`);
|
|
3610
|
+
}
|
|
3611
|
+
await enforcePackageForExistingObject();
|
|
3612
|
+
const writeUrl = include ? classIncludeUrl(name, include) : srcUrl;
|
|
3613
|
+
let spliced;
|
|
3614
|
+
if (include) {
|
|
3615
|
+
// include= path: whole-replace the local include (CCDEF/CCIMP/macros/
|
|
3616
|
+
// testclasses). The structure-based diff/refuse doesn't apply — the
|
|
3617
|
+
// /objectstructure endpoint reports the GLOBAL class, not the local
|
|
3618
|
+
// include's split DEFINITION/IMPLEMENTATION halves. SAP activation is the
|
|
3619
|
+
// validator here (same precedent as `update include=`). No structure or
|
|
3620
|
+
// source fetch is needed: the caller's `source` IS the new include body.
|
|
3621
|
+
spliced = source.endsWith('\n') ? source : `${source}\n`;
|
|
3622
|
+
}
|
|
3623
|
+
else {
|
|
3624
|
+
// MAIN path: fetch structure + source at the same effective version so
|
|
3625
|
+
// the spliced line ranges align with the bytes being edited.
|
|
3626
|
+
const { structure, main } = await fetchClassStructureAndMain(name);
|
|
3627
|
+
// Refuse-policy: compute the method-set diff against the NEW DEFINITION.
|
|
3628
|
+
const diff = diffMethodSets(structure, source);
|
|
3629
|
+
const missingImpls = [];
|
|
3630
|
+
const orphanImpls = [];
|
|
3631
|
+
for (const add of diff.added) {
|
|
3632
|
+
// Exempt declarations that never have a METHOD…ENDMETHOD body.
|
|
3633
|
+
if (add.isAbstract || add.isEvent || add.isInterface || add.isAlias)
|
|
3634
|
+
continue;
|
|
3635
|
+
// Does IMPLEMENTATION already have a METHOD <name> header? Match the
|
|
3636
|
+
// method name followed by a word-boundary so AMDP / event-handler /
|
|
3637
|
+
// multi-line headers (`METHOD x BY DATABASE PROCEDURE…`, `METHOD x FOR
|
|
3638
|
+
// EVENT…`, `METHOD x\n IMPORTING…`) are recognized — NOT only the bare
|
|
3639
|
+
// `METHOD x.` form. \b after the name prevents matching a longer name
|
|
3640
|
+
// with the same prefix (METHOD x_helper for added X).
|
|
3641
|
+
const re = new RegExp(`^\\s*METHOD\\s+${add.name}\\b`, 'im');
|
|
3642
|
+
if (!re.test(main))
|
|
3643
|
+
missingImpls.push(add.name);
|
|
3644
|
+
}
|
|
3645
|
+
for (const rem of diff.removed) {
|
|
3646
|
+
if (rem.implementation) {
|
|
3647
|
+
// Was concrete, still has impl range — caller didn't remove the body.
|
|
3648
|
+
orphanImpls.push(rem.name);
|
|
3649
|
+
}
|
|
3650
|
+
}
|
|
3651
|
+
if (missingImpls.length > 0 || orphanImpls.length > 0) {
|
|
3652
|
+
const parts = [];
|
|
3653
|
+
if (missingImpls.length > 0) {
|
|
3654
|
+
parts.push(`Cannot apply edit_class_definition: the new DEFINITION declares method(s) ${missingImpls.join(', ')} but the existing IMPLEMENTATION block has no matching METHOD…ENDMETHOD body. Either include a METHOD <name>. ENDMETHOD. block per added method in your new source, or use SAPWrite(action="add_method", name="${name}", method="<METHODS clause>") to insert each one atomically.`);
|
|
3655
|
+
}
|
|
3656
|
+
if (orphanImpls.length > 0) {
|
|
3657
|
+
parts.push(`Cannot apply edit_class_definition: the new DEFINITION removes method(s) ${orphanImpls.join(', ')} but the existing IMPLEMENTATION block still has METHOD…ENDMETHOD bodies for them (orphan implementation). Either remove those METHOD blocks in your edit, or use SAPWrite(action="delete_method", name="${name}", method="<name>") to drop each one atomically.`);
|
|
3658
|
+
}
|
|
3659
|
+
return errorResult(parts.join('\n\n'));
|
|
3660
|
+
}
|
|
3661
|
+
spliced = spliceClassDefinition(main, structure, source);
|
|
3662
|
+
}
|
|
3663
|
+
// Pre-write lint on the spliced full source (MAIN path only — include=
|
|
3664
|
+
// fragments can't be lint-parsed standalone).
|
|
3665
|
+
if (!include) {
|
|
3666
|
+
const lintWarnings = runPreWriteLint(spliced, type, name, config, lintOverride);
|
|
3667
|
+
if (lintWarnings.blocked)
|
|
3668
|
+
return lintWarnings.result;
|
|
3669
|
+
}
|
|
3670
|
+
await safeUpdateSource(client.http, client.safety, objectUrl, writeUrl, spliced, transport, cachedFeatures?.abapRelease);
|
|
3671
|
+
invalidateWrittenObject(type, name);
|
|
3672
|
+
const whereLabel = include ? ` (include: ${include})` : '';
|
|
3673
|
+
return textResult(`Successfully updated DEFINITION of ${type} ${name}${whereLabel}. Active version unchanged until activation; read with SAPRead(version="inactive") to verify, then SAPActivate.`);
|
|
3674
|
+
}
|
|
3675
|
+
case 'edit_method_signature': {
|
|
3676
|
+
if (type !== 'CLAS')
|
|
3677
|
+
return errorResult('edit_method_signature is only supported for type=CLAS.');
|
|
3678
|
+
const methodSpecifier = String(args.method ?? '').trim();
|
|
3679
|
+
if (!methodSpecifier) {
|
|
3680
|
+
return errorResult('"method" (the method NAME to re-sign) is required for edit_method_signature.');
|
|
3681
|
+
}
|
|
3682
|
+
if (!hasSource) {
|
|
3683
|
+
return errorResult('"source" (the new METHODS clause) is required for edit_method_signature.');
|
|
3684
|
+
}
|
|
3685
|
+
// MAIN-only action: include= is rejected at the schema layer (this action is
|
|
3686
|
+
// not in SAPWRITE_INCLUDE_AWARE_ACTIONS). Defensive guard for direct CLI calls
|
|
3687
|
+
// that bypass Zod.
|
|
3688
|
+
if (includeProvided) {
|
|
3689
|
+
return errorResult('edit_method_signature targets the global class DEFINITION (/source/main). For local-class (CCDEF) signatures, use edit_class_definition with include=definitions.');
|
|
3690
|
+
}
|
|
3691
|
+
await enforcePackageForExistingObject();
|
|
3692
|
+
const { structure, main } = await fetchClassStructureAndMain(name);
|
|
3693
|
+
const upperName = methodSpecifier.toUpperCase();
|
|
3694
|
+
const method = structure.methods.find((m) => m.name === upperName);
|
|
3695
|
+
if (!method) {
|
|
3696
|
+
const available = structure.methods.map((m) => m.name).join(', ');
|
|
3697
|
+
const hint = methodSpecifier.includes('~')
|
|
3698
|
+
? ' Interface-qualified names (e.g. "zif_x~m") are not addressable here — objectstructure lists the implementing method under its bare name; for interface/local-handler bodies use edit_method.'
|
|
3699
|
+
: '';
|
|
3700
|
+
return errorResult(`Method "${methodSpecifier}" not found in CLAS ${name}. Available methods: ${available || '(none)'}.${hint}`);
|
|
3701
|
+
}
|
|
3702
|
+
const spliced = spliceMethodSignature(main, method, source);
|
|
3703
|
+
// No pre-write lint: edit_method_signature changes ONLY the declaration; the
|
|
3704
|
+
// method body still references the old signature until the caller follows up
|
|
3705
|
+
// with edit_method. Linting the spliced full source here would flag legitimate
|
|
3706
|
+
// in-progress renames (e.g. "param `name` not declared"). SAP activation is the
|
|
3707
|
+
// authoritative check — same rationale as the include= lint skip on edit_method.
|
|
3708
|
+
await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, spliced, transport, cachedFeatures?.abapRelease);
|
|
3709
|
+
invalidateWrittenObject(type, name);
|
|
3710
|
+
return textResult(`Successfully updated signature of method "${method.name}" in ${type} ${name}. Active version unchanged until activation; if the body still references the old signature, follow up with edit_method, then SAPActivate.`);
|
|
3711
|
+
}
|
|
3712
|
+
case 'add_method': {
|
|
3713
|
+
if (type !== 'CLAS')
|
|
3714
|
+
return errorResult('add_method is only supported for type=CLAS.');
|
|
3715
|
+
const clause = String(args.method ?? '');
|
|
3716
|
+
if (!clause.trim()) {
|
|
3717
|
+
return errorResult('"method" (the full METHODS clause, e.g. "METHODS greet IMPORTING who TYPE string.") is required for add_method.');
|
|
3718
|
+
}
|
|
3719
|
+
const methodName = extractMethodNameFromClause(clause);
|
|
3720
|
+
if (!methodName) {
|
|
3721
|
+
return errorResult('Could not extract method name from the METHODS clause. Provide a clause starting with "METHODS <name>" or "CLASS-METHODS <name>".');
|
|
3722
|
+
}
|
|
3723
|
+
// Interface-qualified names (lhc_x~y, zif_x~m) can't be added to a global
|
|
3724
|
+
// class's DEFINITION/IMPLEMENTATION — `~` is interface-method scope and would
|
|
3725
|
+
// produce invalid ABAP in the METHOD stub. Reject with a clear pointer.
|
|
3726
|
+
if (methodName.includes('~')) {
|
|
3727
|
+
return errorResult(`add_method cannot add the interface-qualified method "${methodName}" to a global class. Implement the interface via "INTERFACES <name>." in the DEFINITION (use edit_class_definition), then provide the body with edit_method.`);
|
|
3728
|
+
}
|
|
3729
|
+
const visibility = args.visibility ?? 'public';
|
|
3730
|
+
const isAbstract = args.abstract === true;
|
|
3731
|
+
// MAIN-only action: include= is rejected at the schema layer (not in
|
|
3732
|
+
// SAPWRITE_INCLUDE_AWARE_ACTIONS). Defensive guard for direct CLI calls.
|
|
3733
|
+
if (includeProvided) {
|
|
3734
|
+
return errorResult('add_method targets the global class DEFINITION (/source/main). For local-class (CCDEF) method additions, use edit_class_definition with include=definitions.');
|
|
3735
|
+
}
|
|
3736
|
+
await enforcePackageForExistingObject();
|
|
3737
|
+
const { structure, main } = await fetchClassStructureAndMain(name);
|
|
3738
|
+
// Refuse if method already exists (would silently duplicate).
|
|
3739
|
+
if (structure.methods.some((m) => m.name === methodName)) {
|
|
3740
|
+
return errorResult(`Method "${methodName}" already exists in CLAS ${name}. Use SAPWrite(action="edit_method_signature", method="${methodName}", source="<new METHODS clause>") to change its signature.`);
|
|
3741
|
+
}
|
|
3742
|
+
// A concrete (non-abstract) method needs an IMPLEMENTATION block to receive
|
|
3743
|
+
// its METHOD…ENDMETHOD stub. A purely-abstract class has no IMPLEMENTATION
|
|
3744
|
+
// half, so inserting a concrete declaration there would leave it unimplemented.
|
|
3745
|
+
if (!isAbstract && !structure.classImplementationBlock) {
|
|
3746
|
+
return errorResult(`CLAS ${name} has no IMPLEMENTATION block (purely abstract class). Pass abstract=true to add an abstract method, or add the IMPLEMENTATION half first via edit_class_definition.`);
|
|
3747
|
+
}
|
|
3748
|
+
// Refuse with hint if the target visibility section header is missing.
|
|
3749
|
+
const anchor = findSectionAnchor(main, structure, visibility);
|
|
3750
|
+
if (!anchor) {
|
|
3751
|
+
return errorResult(`No ${visibility.toUpperCase()} SECTION exists in CLAS ${name}. Use SAPWrite(action="edit_class_definition") to add the section header first, then re-run add_method.`);
|
|
3752
|
+
}
|
|
3753
|
+
const spliced = insertMethodPair(main, structure, {
|
|
3754
|
+
decl: clause,
|
|
3755
|
+
visibility,
|
|
3756
|
+
methodName,
|
|
3757
|
+
isAbstract,
|
|
3758
|
+
});
|
|
3759
|
+
const lintWarnings = runPreWriteLint(spliced, type, name, config, lintOverride);
|
|
3760
|
+
if (lintWarnings.blocked)
|
|
3761
|
+
return lintWarnings.result;
|
|
3762
|
+
await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, spliced, transport, cachedFeatures?.abapRelease);
|
|
3763
|
+
invalidateWrittenObject(type, name);
|
|
3764
|
+
const stubNote = isAbstract ? ' (abstract — no IMPL stub inserted)' : '';
|
|
3765
|
+
return textResult(`Successfully added method "${methodName}" (${visibility}) to ${type} ${name}${stubNote}. Active version unchanged until activation; SAPActivate next.`);
|
|
3766
|
+
}
|
|
3767
|
+
case 'delete_method': {
|
|
3768
|
+
if (type !== 'CLAS')
|
|
3769
|
+
return errorResult('delete_method is only supported for type=CLAS.');
|
|
3770
|
+
const methodSpecifier = String(args.method ?? '').trim();
|
|
3771
|
+
if (!methodSpecifier) {
|
|
3772
|
+
return errorResult('"method" (the method NAME to delete) is required for delete_method.');
|
|
3773
|
+
}
|
|
3774
|
+
// MAIN-only action: include= is rejected at the schema layer (not in
|
|
3775
|
+
// SAPWRITE_INCLUDE_AWARE_ACTIONS). Defensive guard for direct CLI calls.
|
|
3776
|
+
if (includeProvided) {
|
|
3777
|
+
return errorResult('delete_method targets the global class DEFINITION (/source/main). For local-class (CCDEF/CCIMP) method removal, use edit_class_definition with include=...');
|
|
3778
|
+
}
|
|
3779
|
+
await enforcePackageForExistingObject();
|
|
3780
|
+
const { structure, main } = await fetchClassStructureAndMain(name);
|
|
3781
|
+
const upperName = methodSpecifier.toUpperCase();
|
|
3782
|
+
const method = structure.methods.find((m) => m.name === upperName);
|
|
3783
|
+
if (!method) {
|
|
3784
|
+
const available = structure.methods.map((m) => m.name).join(', ');
|
|
3785
|
+
const hint = methodSpecifier.includes('~')
|
|
3786
|
+
? ' Interface-qualified names (e.g. "zif_x~m") are not addressable here; objectstructure lists methods under their bare names.'
|
|
3787
|
+
: '';
|
|
3788
|
+
return errorResult(`Method "${methodSpecifier}" not found in CLAS ${name}. Available methods: ${available || '(none)'}.${hint}`);
|
|
3789
|
+
}
|
|
3790
|
+
const spliced = removeMethodPair(main, method);
|
|
3791
|
+
const lintWarnings = runPreWriteLint(spliced, type, name, config, lintOverride);
|
|
3792
|
+
if (lintWarnings.blocked)
|
|
3793
|
+
return lintWarnings.result;
|
|
3794
|
+
await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, spliced, transport, cachedFeatures?.abapRelease);
|
|
3795
|
+
invalidateWrittenObject(type, name);
|
|
3796
|
+
const where = method.implementation ? ' (DEFINITION + IMPLEMENTATION)' : ' (DEFINITION only — was ABSTRACT)';
|
|
3797
|
+
return textResult(`Successfully deleted method "${method.name}" from ${type} ${name}${where}. Active version unchanged until activation; SAPActivate next.`);
|
|
3798
|
+
}
|
|
3799
|
+
case 'change_method_visibility': {
|
|
3800
|
+
// Body-preserving visibility move (issue #303 follow-up). Moves the METHODS
|
|
3801
|
+
// clause from its current section to the target section; the IMPLEMENTATION
|
|
3802
|
+
// block is never touched, so the method body survives. This is the safe
|
|
3803
|
+
// alternative to delete_method + add_method (which discards the body).
|
|
3804
|
+
if (type !== 'CLAS')
|
|
3805
|
+
return errorResult('change_method_visibility is only supported for type=CLAS.');
|
|
3806
|
+
const methodSpecifier = String(args.method ?? '').trim();
|
|
3807
|
+
if (!methodSpecifier) {
|
|
3808
|
+
return errorResult('"method" (the method NAME to move) is required for change_method_visibility.');
|
|
3809
|
+
}
|
|
3810
|
+
const target = args.visibility;
|
|
3811
|
+
if (!target) {
|
|
3812
|
+
return errorResult('"visibility" (target section: public, protected, or private) is required for change_method_visibility.');
|
|
3813
|
+
}
|
|
3814
|
+
// MAIN-only action: include= is rejected at the schema layer (not in
|
|
3815
|
+
// SAPWRITE_INCLUDE_AWARE_ACTIONS). Defensive guard for direct CLI calls.
|
|
3816
|
+
if (includeProvided) {
|
|
3817
|
+
return errorResult('change_method_visibility targets the global class DEFINITION (/source/main). For local-class (CCDEF) methods, use edit_class_definition with include=definitions.');
|
|
3818
|
+
}
|
|
3819
|
+
await enforcePackageForExistingObject();
|
|
3820
|
+
const { structure, main } = await fetchClassStructureAndMain(name);
|
|
3821
|
+
const upperName = methodSpecifier.toUpperCase();
|
|
3822
|
+
const method = structure.methods.find((m) => m.name === upperName);
|
|
3823
|
+
if (!method) {
|
|
3824
|
+
const available = structure.methods.map((m) => m.name).join(', ');
|
|
3825
|
+
const hint = methodSpecifier.includes('~')
|
|
3826
|
+
? ' Interface-qualified names (e.g. "zif_x~m") are not addressable here; objectstructure lists methods under their bare names.'
|
|
3827
|
+
: '';
|
|
3828
|
+
return errorResult(`Method "${methodSpecifier}" not found in CLAS ${name}. Available methods: ${available || '(none)'}.${hint}`);
|
|
3829
|
+
}
|
|
3830
|
+
// Idempotent: already in the requested section → no write.
|
|
3831
|
+
if (method.visibility === target) {
|
|
3832
|
+
return textResult(`Method "${method.name}" is already in the ${target.toUpperCase()} SECTION of ${type} ${name}. No change made.`);
|
|
3833
|
+
}
|
|
3834
|
+
// The target section header must already exist (same constraint as add_method).
|
|
3835
|
+
const anchor = findSectionAnchor(main, structure, target);
|
|
3836
|
+
if (!anchor) {
|
|
3837
|
+
return errorResult(`No ${target.toUpperCase()} SECTION exists in CLAS ${name}. Use SAPWrite(action="edit_class_definition") to add the section header first, then re-run change_method_visibility.`);
|
|
3838
|
+
}
|
|
3839
|
+
// DEFINITION-only move — IMPLEMENTATION (the method body) is preserved verbatim.
|
|
3840
|
+
const spliced = moveMethodDefinition(main, method, anchor.afterLine);
|
|
3841
|
+
const lintWarnings = runPreWriteLint(spliced, type, name, config, lintOverride);
|
|
3842
|
+
if (lintWarnings.blocked)
|
|
3843
|
+
return lintWarnings.result;
|
|
3844
|
+
await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, spliced, transport, cachedFeatures?.abapRelease);
|
|
3845
|
+
invalidateWrittenObject(type, name);
|
|
3846
|
+
return textResult(`Successfully moved method "${method.name}" from ${method.visibility.toUpperCase()} to ${target.toUpperCase()} SECTION of ${type} ${name} (IMPLEMENTATION preserved). Active version unchanged until activation; SAPActivate next.`);
|
|
3847
|
+
}
|
|
3341
3848
|
case 'scaffold_rap_handlers': {
|
|
3342
3849
|
// What this action does:
|
|
3343
3850
|
// Given a behavior-pool class (ZBP_*) and its interface BDEF, inspect
|
|
@@ -3581,7 +4088,9 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
3581
4088
|
});
|
|
3582
4089
|
}
|
|
3583
4090
|
catch (err) {
|
|
3584
|
-
if (err instanceof AdtApiError &&
|
|
4091
|
+
if (err instanceof AdtApiError &&
|
|
4092
|
+
CDS_DEPENDENCY_SENSITIVE_TYPES.has(canonicalTablType(type)) &&
|
|
4093
|
+
isDeleteDependencyError(err)) {
|
|
3585
4094
|
const hint = await buildCdsDeleteDependencyHint(client, type, name, objectUrl);
|
|
3586
4095
|
if (hint) {
|
|
3587
4096
|
// Attach via extraHint so the LLM-facing formatter renders it after
|
|
@@ -3608,15 +4117,20 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
3608
4117
|
const activateAtEnd = args.activateAtEnd === true || String(args.activateAtEnd) === 'true';
|
|
3609
4118
|
const defaultPackage = normalizePackageOverride(args.package, '$TMP');
|
|
3610
4119
|
const batchPlan = objects.map((obj) => {
|
|
3611
|
-
const objType =
|
|
4120
|
+
const objType = normalizeWriteObjectType(String(obj.type ?? ''));
|
|
3612
4121
|
const objName = String(obj.name ?? '');
|
|
3613
4122
|
const objPackage = normalizePackageOverride(obj.package, defaultPackage);
|
|
3614
4123
|
const explicitTransport = normalizeTransportOverride(obj.transport) ?? transport;
|
|
3615
4124
|
return { obj, type: objType, name: objName, packageName: objPackage, explicitTransport };
|
|
3616
4125
|
});
|
|
3617
4126
|
// Check every target package before starting any creates.
|
|
3618
|
-
|
|
3619
|
-
|
|
4127
|
+
// Resolver is shared across the loop so subtree BFS happens once even when
|
|
4128
|
+
// many objects target descendants of the same `ZFOO/**` root.
|
|
4129
|
+
{
|
|
4130
|
+
const resolver = client.getPackageHierarchyResolver();
|
|
4131
|
+
for (const pkg of new Set(batchPlan.map((item) => item.packageName))) {
|
|
4132
|
+
await checkPackage(client.safety, pkg, resolver);
|
|
4133
|
+
}
|
|
3620
4134
|
}
|
|
3621
4135
|
// Pre-flight transport check for batch_create (same logic as single create),
|
|
3622
4136
|
// but keyed by each effective package because objects can override package.
|
|
@@ -3749,13 +4263,24 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
3749
4263
|
break;
|
|
3750
4264
|
}
|
|
3751
4265
|
}
|
|
3752
|
-
// Step 1: Create the object
|
|
4266
|
+
// Step 1: Create the object (per-entry transparent-table discovery gate;
|
|
4267
|
+
// mirrors the single-create site above. TABL/DS skips it — /structures/ always exists.)
|
|
4268
|
+
if ((objType === 'TABL' || objType === 'TABL/DT') && isTablesEndpointAvailable() === false) {
|
|
4269
|
+
results.push({
|
|
4270
|
+
type: objType,
|
|
4271
|
+
name: objName,
|
|
4272
|
+
packageName: objPackage,
|
|
4273
|
+
status: 'failed',
|
|
4274
|
+
error: TABL_DT_WRITE_UNAVAILABLE_HINT,
|
|
4275
|
+
});
|
|
4276
|
+
break;
|
|
4277
|
+
}
|
|
3753
4278
|
const objUrl = objectUrlForType(objType, objName);
|
|
3754
4279
|
const createUrl = objUrl.replace(/\/[^/]+$/, '');
|
|
3755
4280
|
const objMetadataProps = getMetadataWriteProperties(obj);
|
|
3756
4281
|
const body = buildCreateXml(objType, objName, objPackage, objDescription, objMetadataProps);
|
|
3757
4282
|
const contentType = createContentTypeForType(objType);
|
|
3758
|
-
const needsPackageParam = objType === 'BDEF' || objType === 'TABL';
|
|
4283
|
+
const needsPackageParam = objType === 'BDEF' || objType === 'TABL' || objType === 'TABL/DT' || objType === 'TABL/DS';
|
|
3759
4284
|
try {
|
|
3760
4285
|
await createObject(client.http, client.safety, createUrl, body, contentType, objTransport, needsPackageParam ? objPackage : undefined, cachedFeatures?.abapRelease);
|
|
3761
4286
|
}
|
|
@@ -3879,11 +4404,11 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
3879
4404
|
const batchStatuses = buildBatchActivationStatuses(writtenObjects, activationOutcome);
|
|
3880
4405
|
const statusDetails = formatBatchActivationStatuses(batchStatuses);
|
|
3881
4406
|
terminalActivationFailure = statusDetails;
|
|
3882
|
-
const statusByName = new Map(batchStatuses.map((s) => [`${s.type}
|
|
4407
|
+
const statusByName = new Map(batchStatuses.map((s) => [`${s.type}\x00${s.name}`, s]));
|
|
3883
4408
|
for (const result of results) {
|
|
3884
4409
|
if (result.status !== 'success')
|
|
3885
4410
|
continue;
|
|
3886
|
-
const key = `${result.type}
|
|
4411
|
+
const key = `${result.type}\x00${result.name}`;
|
|
3887
4412
|
const matched = statusByName.get(key);
|
|
3888
4413
|
if (!matched)
|
|
3889
4414
|
continue;
|
|
@@ -3942,7 +4467,8 @@ function runRapPreflightValidation(source, type, name, features, configSystemTyp
|
|
|
3942
4467
|
return { blocked: false };
|
|
3943
4468
|
}
|
|
3944
4469
|
const systemType = features?.systemType ?? (configSystemType !== 'auto' ? configSystemType : undefined);
|
|
3945
|
-
|
|
4470
|
+
// Canonicalize so validateRapSource's 'TABL' case matches TABL/DT and TABL/DS.
|
|
4471
|
+
const result = validateRapSource(canonicalTablType(type), source, {
|
|
3946
4472
|
systemType,
|
|
3947
4473
|
abapRelease: features?.abapRelease,
|
|
3948
4474
|
});
|
|
@@ -4196,7 +4722,12 @@ async function handleSAPActivate(client, args, cachingLayer) {
|
|
|
4196
4722
|
const objName = String(o.name ?? '');
|
|
4197
4723
|
let url;
|
|
4198
4724
|
if (objType === 'TABL') {
|
|
4199
|
-
|
|
4725
|
+
// Use the write-path resolver: refuses TABL/DT activation on systems
|
|
4726
|
+
// that don't expose /sap/bc/adt/ddic/tables/ (NW 7.50/7.51), where
|
|
4727
|
+
// activate would hit the wrong endpoint. See issue #285.
|
|
4728
|
+
url = await client.resolveTablObjectUrlForWrite(objName, {
|
|
4729
|
+
tablesEndpointAvailable: isTablesEndpointAvailable(),
|
|
4730
|
+
});
|
|
4200
4731
|
}
|
|
4201
4732
|
else if (objType === 'FUNC') {
|
|
4202
4733
|
let group = String(o.group ?? args.group ?? '').trim();
|
|
@@ -4238,15 +4769,26 @@ async function handleSAPActivate(client, args, cachingLayer) {
|
|
|
4238
4769
|
.join('');
|
|
4239
4770
|
return errorResult(`Batch activation failed for: ${names}.${statusDetails}\n${formatActivationMessages(result)}${combinedDiag}`);
|
|
4240
4771
|
}
|
|
4241
|
-
// Single activation (existing behavior). For TABL we
|
|
4242
|
-
//
|
|
4243
|
-
//
|
|
4772
|
+
// Single activation (existing behavior). For TABL we use the write-path
|
|
4773
|
+
// resolver so transparent-table activations on NW 7.50/7.51 are refused
|
|
4774
|
+
// with the SE11 hint instead of silently activating against /structures/
|
|
4775
|
+
// (which would not even be the right object). See issue #285.
|
|
4244
4776
|
// For FUNC the URL needs the parent function group baked into the path
|
|
4245
4777
|
// (issue #250) — `objectBasePath('FUNC')` deliberately throws so generic
|
|
4246
4778
|
// builders fail loudly. Auto-resolve the group when omitted.
|
|
4247
4779
|
let objectUrl;
|
|
4248
4780
|
if (type === 'TABL') {
|
|
4249
|
-
|
|
4781
|
+
try {
|
|
4782
|
+
objectUrl = await client.resolveTablObjectUrlForWrite(name, {
|
|
4783
|
+
tablesEndpointAvailable: isTablesEndpointAvailable(),
|
|
4784
|
+
});
|
|
4785
|
+
}
|
|
4786
|
+
catch (resolveErr) {
|
|
4787
|
+
if (resolveErr instanceof AdtSafetyError) {
|
|
4788
|
+
return errorResult(resolveErr.message);
|
|
4789
|
+
}
|
|
4790
|
+
throw resolveErr;
|
|
4791
|
+
}
|
|
4250
4792
|
}
|
|
4251
4793
|
else if (type === 'FUNC') {
|
|
4252
4794
|
let group = String(args.group ?? '').trim();
|
|
@@ -4906,7 +5448,7 @@ async function handleSAPGit(client, args, _authInfo) {
|
|
|
4906
5448
|
password,
|
|
4907
5449
|
token,
|
|
4908
5450
|
};
|
|
4909
|
-
result = await gctsCloneRepo(client.http, client.safety, params);
|
|
5451
|
+
result = await gctsCloneRepo(client.http, client.safety, params, client.getPackageHierarchyResolver());
|
|
4910
5452
|
}
|
|
4911
5453
|
else {
|
|
4912
5454
|
if (!packageName)
|
|
@@ -4918,7 +5460,7 @@ async function handleSAPGit(client, args, _authInfo) {
|
|
|
4918
5460
|
transportRequest: String(args.transport ?? '').trim() || undefined,
|
|
4919
5461
|
user,
|
|
4920
5462
|
password,
|
|
4921
|
-
});
|
|
5463
|
+
}, client.getPackageHierarchyResolver());
|
|
4922
5464
|
}
|
|
4923
5465
|
break;
|
|
4924
5466
|
case 'pull':
|
|
@@ -4976,7 +5518,7 @@ async function handleSAPGit(client, args, _authInfo) {
|
|
|
4976
5518
|
result = await gctsCreateBranch(client.http, client.safety, repoId, {
|
|
4977
5519
|
branch,
|
|
4978
5520
|
...(packageName ? { package: packageName } : {}),
|
|
4979
|
-
});
|
|
5521
|
+
}, client.getPackageHierarchyResolver());
|
|
4980
5522
|
}
|
|
4981
5523
|
else {
|
|
4982
5524
|
await abapGitCreateBranch(client.http, client.safety, repoId, branch);
|
|
@@ -5477,10 +6019,19 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
|
|
|
5477
6019
|
if (!description)
|
|
5478
6020
|
return errorResult('"description" is required for create_package action.');
|
|
5479
6021
|
checkOperation(client.safety, OperationType.Create, 'CreatePackage');
|
|
5480
|
-
// Package allowlist
|
|
5481
|
-
//
|
|
6022
|
+
// Package allowlist gate:
|
|
6023
|
+
// - When `superPackage` is set, gate the parent. This enables creating
|
|
6024
|
+
// children in allowed parents like $TMP. With subtree (`X/**`) rules,
|
|
6025
|
+
// the new child will automatically be inside its parent's subtree.
|
|
6026
|
+
// - When `superPackage` is omitted, the new package is created at the
|
|
6027
|
+
// root and IS the gateable name itself — otherwise an admin's
|
|
6028
|
+
// allowedPackages restriction would be bypassed by simply omitting
|
|
6029
|
+
// the parent. Gate the new name in that case.
|
|
5482
6030
|
if (superPackage) {
|
|
5483
|
-
checkPackage(client.safety, superPackage);
|
|
6031
|
+
await checkPackage(client.safety, superPackage, client.getPackageHierarchyResolver());
|
|
6032
|
+
}
|
|
6033
|
+
else {
|
|
6034
|
+
await checkPackage(client.safety, name, client.getPackageHierarchyResolver());
|
|
5484
6035
|
}
|
|
5485
6036
|
let effectiveTransport = transport || undefined;
|
|
5486
6037
|
const packageUrl = `/sap/bc/adt/packages/${encodeURIComponent(name)}`;
|
|
@@ -5523,6 +6074,9 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
|
|
|
5523
6074
|
packageType,
|
|
5524
6075
|
});
|
|
5525
6076
|
await createObject(client.http, client.safety, '/sap/bc/adt/packages', xml, 'application/*', effectiveTransport, undefined, cachedFeatures?.abapRelease);
|
|
6077
|
+
// Hierarchy changed: invalidate any cached subtree that could contain
|
|
6078
|
+
// the new package. Conservative: clear all (cheap; per-call cost is one BFS).
|
|
6079
|
+
client.invalidatePackageHierarchy();
|
|
5526
6080
|
return textResult(`Created package ${name}.`);
|
|
5527
6081
|
}
|
|
5528
6082
|
case 'delete_package': {
|
|
@@ -5531,6 +6085,9 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
|
|
|
5531
6085
|
if (!name)
|
|
5532
6086
|
return errorResult('"name" is required for delete_package action.');
|
|
5533
6087
|
checkOperation(client.safety, OperationType.Delete, 'DeletePackage');
|
|
6088
|
+
// Gate by allowedPackages: deletion targets the package itself, so the
|
|
6089
|
+
// package name must be in the allowed set (or in an allowed subtree).
|
|
6090
|
+
await checkPackage(client.safety, name, client.getPackageHierarchyResolver());
|
|
5534
6091
|
const packageUrl = `/sap/bc/adt/packages/${encodeURIComponent(name)}`;
|
|
5535
6092
|
await client.http.withStatefulSession(async (session) => {
|
|
5536
6093
|
const lock = await lockObject(session, client.safety, packageUrl, 'MODIFY', cachedFeatures?.abapRelease);
|
|
@@ -5547,6 +6104,8 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
|
|
|
5547
6104
|
}
|
|
5548
6105
|
}
|
|
5549
6106
|
});
|
|
6107
|
+
// Hierarchy changed: invalidate cached subtrees.
|
|
6108
|
+
client.invalidatePackageHierarchy();
|
|
5550
6109
|
return textResult(`Deleted package ${name}.`);
|
|
5551
6110
|
}
|
|
5552
6111
|
case 'change_package': {
|
|
@@ -5565,8 +6124,11 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
|
|
|
5565
6124
|
if (!newPackage)
|
|
5566
6125
|
return errorResult('"newPackage" is required for change_package action.');
|
|
5567
6126
|
checkOperation(client.safety, OperationType.Update, 'ChangePackage');
|
|
5568
|
-
|
|
5569
|
-
|
|
6127
|
+
{
|
|
6128
|
+
const resolver = client.getPackageHierarchyResolver();
|
|
6129
|
+
await checkPackage(client.safety, oldPackage, resolver);
|
|
6130
|
+
await checkPackage(client.safety, newPackage, resolver);
|
|
6131
|
+
}
|
|
5570
6132
|
// Resolve object URI via search if not provided
|
|
5571
6133
|
if (!objectUri) {
|
|
5572
6134
|
const searchResp = await client.http.get(`/sap/bc/adt/repository/informationsystem/search?operation=quickSearch&query=${encodeURIComponent(objectName)}&maxResults=10`);
|
|
@@ -5612,6 +6174,8 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
|
|
|
5612
6174
|
newPackage,
|
|
5613
6175
|
transport: effectiveTransport,
|
|
5614
6176
|
});
|
|
6177
|
+
// Hierarchy may have shifted (object moved between packages); invalidate cache.
|
|
6178
|
+
client.invalidatePackageHierarchy();
|
|
5615
6179
|
const transportNote = result.transport ? ` (transport: ${result.transport})` : '';
|
|
5616
6180
|
return textResult(`Moved ${objectName} from package ${oldPackage} to ${newPackage}${transportNote}.`);
|
|
5617
6181
|
}
|