arc-1 0.9.5 → 0.9.6
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 +19 -1
- package/dist/adt/client.d.ts +38 -7
- package/dist/adt/client.d.ts.map +1 -1
- package/dist/adt/client.js +100 -9
- 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/features.d.ts.map +1 -1
- package/dist/adt/features.js +27 -3
- package/dist/adt/features.js.map +1 -1
- package/dist/adt/http.d.ts +23 -0
- package/dist/adt/http.d.ts.map +1 -1
- package/dist/adt/http.js +82 -2
- package/dist/adt/http.js.map +1 -1
- package/dist/adt/xml-parser.d.ts +22 -0
- package/dist/adt/xml-parser.d.ts.map +1 -1
- package/dist/adt/xml-parser.js +32 -0
- package/dist/adt/xml-parser.js.map +1 -1
- package/dist/handlers/intent.d.ts +2 -1
- package/dist/handlers/intent.d.ts.map +1 -1
- package/dist/handlers/intent.js +184 -26
- package/dist/handlers/intent.js.map +1 -1
- package/dist/handlers/schemas.d.ts +10 -2
- package/dist/handlers/schemas.d.ts.map +1 -1
- package/dist/handlers/schemas.js +5 -0
- package/dist/handlers/schemas.js.map +1 -1
- package/dist/handlers/tools.d.ts.map +1 -1
- package/dist/handlers/tools.js +5 -0
- 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 +7 -5
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +43 -18
- 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
|
@@ -37,6 +37,7 @@ import { detectFilename, lintAbapSource, lintAndFix, validateBeforeWrite } from
|
|
|
37
37
|
import { sanitizeArgs } from '../server/audit.js';
|
|
38
38
|
import { generateRequestId, requestContext } from '../server/context.js';
|
|
39
39
|
import { logger } from '../server/logger.js';
|
|
40
|
+
import { resolveRateLimitUserKey } from '../server/mcp-rate-limit.js';
|
|
40
41
|
import { expandHyperfocusedArgs } from './hyperfocused.js';
|
|
41
42
|
import { getToolSchema } from './schemas.js';
|
|
42
43
|
import { formatZodError } from './zod-errors.js';
|
|
@@ -225,7 +226,7 @@ function buildBaseErrorMessage(err, message, tool, args, config) {
|
|
|
225
226
|
if (err instanceof AdtApiError) {
|
|
226
227
|
// Append additional SAP messages (line numbers, secondary errors) if available
|
|
227
228
|
const enriched = enrichWithSapDetails(err, message);
|
|
228
|
-
const argType = String(args.type ?? '').toUpperCase();
|
|
229
|
+
const argType = canonicalTablType(String(args.type ?? '').toUpperCase());
|
|
229
230
|
const classification = classifySapDomainError(err.statusCode, err.responseBody, err.path);
|
|
230
231
|
if (classification) {
|
|
231
232
|
const transactionLine = classification.transaction ? `\nSAP Transaction: ${classification.transaction}` : '';
|
|
@@ -295,7 +296,7 @@ function buildBaseErrorMessage(err, message, tool, args, config) {
|
|
|
295
296
|
return enriched;
|
|
296
297
|
}
|
|
297
298
|
if (err instanceof AdtSafetyError) {
|
|
298
|
-
const argType = String(args.type ?? '').toUpperCase();
|
|
299
|
+
const argType = canonicalTablType(String(args.type ?? '').toUpperCase());
|
|
299
300
|
if (tool === 'SAPRead' && argType === 'TABLE_CONTENTS') {
|
|
300
301
|
return (`${message}\n\nHint: TABLE_CONTENTS is blocked by safety configuration or missing data scope. ` +
|
|
301
302
|
'Set SAP_ALLOW_DATA_PREVIEW=true at the server level and, in authenticated HTTP mode, ' +
|
|
@@ -658,7 +659,7 @@ async function inactiveSyntaxDiagnostic(client, type, name) {
|
|
|
658
659
|
}
|
|
659
660
|
}
|
|
660
661
|
async function tryPostSaveSyntaxCheck(client, type, name) {
|
|
661
|
-
if (!DDIC_POST_SAVE_CHECK_TYPES.has(type.toUpperCase()))
|
|
662
|
+
if (!DDIC_POST_SAVE_CHECK_TYPES.has(canonicalTablType(type.toUpperCase())))
|
|
662
663
|
return '';
|
|
663
664
|
return inactiveSyntaxDiagnostic(client, type, name);
|
|
664
665
|
}
|
|
@@ -746,7 +747,7 @@ function classifyError(err) {
|
|
|
746
747
|
* all tools are allowed (backward compatibility).
|
|
747
748
|
* @param server - MCP Server instance for elicitation support.
|
|
748
749
|
*/
|
|
749
|
-
export async function handleToolCall(client, config, toolName, args, authInfo, _server, cachingLayer, isPerUserClient) {
|
|
750
|
+
export async function handleToolCall(client, config, toolName, args, authInfo, _server, cachingLayer, isPerUserClient, mcpRateLimiter) {
|
|
750
751
|
const reqId = generateRequestId();
|
|
751
752
|
const start = Date.now();
|
|
752
753
|
// Build user context for audit logging
|
|
@@ -763,6 +764,47 @@ export async function handleToolCall(client, config, toolName, args, authInfo, _
|
|
|
763
764
|
tool: toolName,
|
|
764
765
|
args: sanitizeArgs(args),
|
|
765
766
|
});
|
|
767
|
+
// ─── Layer 2: per-user MCP tool-call rate limit ─────────────────────
|
|
768
|
+
// Applied immediately so we don't waste any work on denied calls. Stdio mode
|
|
769
|
+
// (no authInfo) is exempt — there's no user identity to key on. On denial we
|
|
770
|
+
// return an MCP tool error (not HTTP 429) so the LLM client surfaces it as a
|
|
771
|
+
// tool failure and the agent loop backs off via its own retry policy.
|
|
772
|
+
// See docs_page/rate-limiting.md (Layer 2). Cost weighting per tool is deferred
|
|
773
|
+
// to v2 — every consume call counts as one point.
|
|
774
|
+
if (mcpRateLimiter && authInfo) {
|
|
775
|
+
// Walks the most-specific identity claim first (userName → email → sub →
|
|
776
|
+
// preferred_username → clientId) so OIDC users sharing one `azp` clientId
|
|
777
|
+
// don't collapse into a single bucket. See resolveRateLimitUserKey.
|
|
778
|
+
const userKey = resolveRateLimitUserKey(authInfo);
|
|
779
|
+
const decision = await mcpRateLimiter.consume(userKey, toolName);
|
|
780
|
+
if (!decision.allowed) {
|
|
781
|
+
const retryAfter = Math.ceil(decision.retryAfterMs / 1000);
|
|
782
|
+
logger.emitAudit({
|
|
783
|
+
timestamp: new Date().toISOString(),
|
|
784
|
+
level: 'warn',
|
|
785
|
+
event: 'mcp_rate_limited',
|
|
786
|
+
requestId: reqId,
|
|
787
|
+
clientId,
|
|
788
|
+
user: userKey,
|
|
789
|
+
tool: toolName,
|
|
790
|
+
limitPerMinute: decision.limitPerMinute,
|
|
791
|
+
retryAfterMs: decision.retryAfterMs,
|
|
792
|
+
});
|
|
793
|
+
return {
|
|
794
|
+
content: [
|
|
795
|
+
{
|
|
796
|
+
type: 'text',
|
|
797
|
+
text: JSON.stringify({
|
|
798
|
+
error: 'rate_limited',
|
|
799
|
+
retryAfter,
|
|
800
|
+
message: `Rate limit exceeded (${decision.limitPerMinute}/min per user). Retry after ${retryAfter} seconds.`,
|
|
801
|
+
}),
|
|
802
|
+
},
|
|
803
|
+
],
|
|
804
|
+
isError: true,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
}
|
|
766
808
|
// Unified scope enforcement via ACTION_POLICY — routes through action/type-aware lookup.
|
|
767
809
|
// For SAPRead, the policy key is Tool.{type}; for other action-bearing tools, Tool.{action};
|
|
768
810
|
// for tools without an action/type enum (SAPSearch, SAPQuery), the tool-level default applies.
|
|
@@ -945,6 +987,28 @@ export async function handleToolCall(client, config, toolName, args, authInfo, _
|
|
|
945
987
|
function isBtpSystem() {
|
|
946
988
|
return cachedFeatures?.systemType === 'btp';
|
|
947
989
|
}
|
|
990
|
+
/** Return whether the SAP ADT discovery feed advertises the /sap/bc/adt/ddic/tables
|
|
991
|
+
* collection (the transparent-table editor endpoint). Absent on NW 7.50/7.51 —
|
|
992
|
+
* SAP added it in NW 7.52 along with the new database-table editor. When the
|
|
993
|
+
* discovery cache is empty (e.g. probe never ran, tests that bypass SAPManage),
|
|
994
|
+
* returns `undefined` so callers can decide whether to default-allow.
|
|
995
|
+
* See issue #285. */
|
|
996
|
+
function isTablesEndpointAvailable() {
|
|
997
|
+
const map = cachedFeatures?.discoveryMap ?? cachedDiscovery;
|
|
998
|
+
if (!map || map.size === 0)
|
|
999
|
+
return undefined;
|
|
1000
|
+
return map.has('/sap/bc/adt/ddic/tables');
|
|
1001
|
+
}
|
|
1002
|
+
/** Stable hint surfaced when ARC-1 refuses a TABL/DT write because the connected
|
|
1003
|
+
* system does not expose /sap/bc/adt/ddic/tables/. Shared between the
|
|
1004
|
+
* resolver-driven update/delete/activate paths and the discovery-gated create
|
|
1005
|
+
* paths so the LLM always sees the same recovery instructions. */
|
|
1006
|
+
const TABL_DT_WRITE_UNAVAILABLE_HINT = 'Transparent table writes via ADT REST are not available on this system ' +
|
|
1007
|
+
'(/sap/bc/adt/ddic/tables/ is not exposed — NW 7.50/7.51 ship the DDIC ' +
|
|
1008
|
+
'structures endpoint only; the table editor was added in NW 7.52). ' +
|
|
1009
|
+
'Use SE11 in SAPGUI, or connect ARC-1 to an SAP_BASIS ≥ 7.52 system. ' +
|
|
1010
|
+
'Writing the source via /sap/bc/adt/ddic/structures/ would silently flip ' +
|
|
1011
|
+
'DD02L-TABCLASS to INTTAB and corrupt the table.';
|
|
948
1012
|
/** BTP-specific error messages for unavailable operations */
|
|
949
1013
|
const BTP_HINTS = {
|
|
950
1014
|
PROG: 'Executable programs (reports) are not available on BTP ABAP Environment. Use CLAS with IF_OO_ADT_CLASSRUN for console applications.',
|
|
@@ -2253,18 +2317,24 @@ export function buildCreateXml(type, name, pkg, description, properties) {
|
|
|
2253
2317
|
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
2254
2318
|
</dcl:dclSource>`;
|
|
2255
2319
|
case 'TABL':
|
|
2256
|
-
|
|
2320
|
+
case 'TABL/DT':
|
|
2321
|
+
case 'TABL/DS': {
|
|
2322
|
+
// Bare TABL is the legacy alias for TABL/DT (transparent table). The same
|
|
2323
|
+
// <blue:blueSource> envelope works for both subtypes — only adtcore:type
|
|
2324
|
+
// and the POST URL differ. See docs/plans/completed/fix-tabl-ds-create-routing.md.
|
|
2325
|
+
const adtType = type === 'TABL/DS' ? 'TABL/DS' : 'TABL/DT';
|
|
2257
2326
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2258
2327
|
<blue:blueSource xmlns:blue="http://www.sap.com/wbobj/blue"
|
|
2259
2328
|
xmlns:adtcore="http://www.sap.com/adt/core"
|
|
2260
2329
|
adtcore:description="${escapeXml(description)}"
|
|
2261
2330
|
adtcore:name="${escapeXml(name)}"
|
|
2262
|
-
adtcore:type="
|
|
2331
|
+
adtcore:type="${adtType}"
|
|
2263
2332
|
adtcore:masterLanguage="EN"
|
|
2264
2333
|
adtcore:masterSystem="H00"
|
|
2265
2334
|
adtcore:responsible="DEVELOPER">
|
|
2266
2335
|
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
2267
2336
|
</blue:blueSource>`;
|
|
2337
|
+
}
|
|
2268
2338
|
case 'BDEF':
|
|
2269
2339
|
// BDEF uses SAP's "blue" framework — blue:blueSource with http://www.sap.com/wbobj/blue namespace.
|
|
2270
2340
|
// Confirmed by vibing-steampunk (Go) and fr0ster (TypeScript) reference implementations.
|
|
@@ -2569,6 +2639,37 @@ export function normalizeObjectType(type) {
|
|
|
2569
2639
|
return '';
|
|
2570
2640
|
return SLASH_TYPE_MAP[normalized] ?? normalized;
|
|
2571
2641
|
}
|
|
2642
|
+
/** TABL subtypes that SAPWrite preserves (instead of collapsing to bare 'TABL' via
|
|
2643
|
+
* SLASH_TYPE_MAP) so the create path can route TABL/DT → /ddic/tables and
|
|
2644
|
+
* TABL/DS → /ddic/structures. See docs/plans/completed/fix-tabl-ds-create-routing.md. */
|
|
2645
|
+
const TABL_WRITE_SUBTYPES = new Set(['TABL/DT', 'TABL/DS']);
|
|
2646
|
+
/** Legacy slash-form aliases SAPWrite remaps to a canonical subtype before
|
|
2647
|
+
* SLASH_TYPE_MAP runs — otherwise STRU/DS would collapse to bare 'TABL' and
|
|
2648
|
+
* route the structure create to /ddic/tables. */
|
|
2649
|
+
const SAPWRITE_TABL_ALIAS = {
|
|
2650
|
+
'STRU/DS': 'TABL/DS',
|
|
2651
|
+
};
|
|
2652
|
+
/** SAPWrite-only normalizer: preserves TABL/DT and TABL/DS and remaps STRU/DS
|
|
2653
|
+
* to TABL/DS. Every other tool keeps the global collapsing behaviour of
|
|
2654
|
+
* `normalizeObjectType`. */
|
|
2655
|
+
function normalizeWriteObjectType(type) {
|
|
2656
|
+
const normalized = String(type).trim().toUpperCase();
|
|
2657
|
+
if (!normalized)
|
|
2658
|
+
return '';
|
|
2659
|
+
const aliased = SAPWRITE_TABL_ALIAS[normalized];
|
|
2660
|
+
if (aliased)
|
|
2661
|
+
return aliased;
|
|
2662
|
+
if (TABL_WRITE_SUBTYPES.has(normalized))
|
|
2663
|
+
return normalized;
|
|
2664
|
+
return SLASH_TYPE_MAP[normalized] ?? normalized;
|
|
2665
|
+
}
|
|
2666
|
+
/** Collapse TABL/DT and TABL/DS back to bare 'TABL' for downstream Set-membership
|
|
2667
|
+
* checks (DDIC hints, RAP preflight, CDS dependency hints, cache invalidation)
|
|
2668
|
+
* that only know about canonical types. The slash form survives at URL routing
|
|
2669
|
+
* + XML envelope sites. */
|
|
2670
|
+
function canonicalTablType(type) {
|
|
2671
|
+
return type === 'TABL/DT' || type === 'TABL/DS' ? 'TABL' : type;
|
|
2672
|
+
}
|
|
2572
2673
|
/** Normalize type fields before schema validation so slash/case aliases are accepted. */
|
|
2573
2674
|
function normalizeTypeArgsForValidation(toolName, args) {
|
|
2574
2675
|
switch (toolName) {
|
|
@@ -2579,14 +2680,15 @@ function normalizeTypeArgsForValidation(toolName, args) {
|
|
|
2579
2680
|
objectType: args.objectType === undefined ? undefined : normalizeObjectType(String(args.objectType ?? '')),
|
|
2580
2681
|
};
|
|
2581
2682
|
case 'SAPWrite':
|
|
2683
|
+
// SAPWrite preserves TABL/DT and TABL/DS so the create path can route by subtype.
|
|
2582
2684
|
return {
|
|
2583
2685
|
...args,
|
|
2584
|
-
type: args.type === undefined ? undefined :
|
|
2686
|
+
type: args.type === undefined ? undefined : normalizeWriteObjectType(String(args.type ?? '')),
|
|
2585
2687
|
objects: Array.isArray(args.objects)
|
|
2586
2688
|
? args.objects.map((obj) => typeof obj === 'object' && obj !== null
|
|
2587
2689
|
? {
|
|
2588
2690
|
...obj,
|
|
2589
|
-
type:
|
|
2691
|
+
type: normalizeWriteObjectType(String(obj.type ?? '')),
|
|
2590
2692
|
}
|
|
2591
2693
|
: obj)
|
|
2592
2694
|
: args.objects,
|
|
@@ -2688,10 +2790,13 @@ export function objectBasePath(type) {
|
|
|
2688
2790
|
case 'SRVB':
|
|
2689
2791
|
return '/sap/bc/adt/businessservices/bindings/';
|
|
2690
2792
|
case 'TABL':
|
|
2691
|
-
|
|
2692
|
-
//
|
|
2693
|
-
// AdtClient.resolveTablObjectUrl(name) which falls back on 404.
|
|
2793
|
+
case 'TABL/DT':
|
|
2794
|
+
// Bare TABL defaults to transparent table. For reads, callers should use
|
|
2795
|
+
// AdtClient.resolveTablObjectUrl(name) which falls back to /structures/ on 404.
|
|
2694
2796
|
return '/sap/bc/adt/ddic/tables/';
|
|
2797
|
+
case 'TABL/DS':
|
|
2798
|
+
// DDIC structures only route through this collection; see follow-up to #285.
|
|
2799
|
+
return '/sap/bc/adt/ddic/structures/';
|
|
2695
2800
|
case 'DOMA':
|
|
2696
2801
|
return '/sap/bc/adt/ddic/domains/';
|
|
2697
2802
|
case 'DTEL':
|
|
@@ -2821,7 +2926,7 @@ function stripIncludeHeader(source) {
|
|
|
2821
2926
|
// ─── SAPWrite Handler ────────────────────────────────────────────────
|
|
2822
2927
|
async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
2823
2928
|
const action = String(args.action ?? '');
|
|
2824
|
-
const type =
|
|
2929
|
+
const type = normalizeWriteObjectType(String(args.type ?? ''));
|
|
2825
2930
|
const name = String(args.name ?? '');
|
|
2826
2931
|
const source = String(args.source ?? '');
|
|
2827
2932
|
const hasSource = typeof args.source === 'string';
|
|
@@ -2860,8 +2965,22 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2860
2965
|
// `buildCreateXml('FUNC', …, properties)` finds it.
|
|
2861
2966
|
let objectUrl;
|
|
2862
2967
|
let srcUrl;
|
|
2863
|
-
if (type === 'TABL'
|
|
2864
|
-
|
|
2968
|
+
if ((type === 'TABL' || type === 'TABL/DT' || type === 'TABL/DS') &&
|
|
2969
|
+
action !== 'create' &&
|
|
2970
|
+
action !== 'batch_create') {
|
|
2971
|
+
// All TABL forms route through the search-first resolver on update/delete/activate
|
|
2972
|
+
// so the PR #286 SE11-hint refusal applies even when callers pass an explicit slash form.
|
|
2973
|
+
try {
|
|
2974
|
+
objectUrl = await client.resolveTablObjectUrlForWrite(name, {
|
|
2975
|
+
tablesEndpointAvailable: isTablesEndpointAvailable(),
|
|
2976
|
+
});
|
|
2977
|
+
}
|
|
2978
|
+
catch (resolveErr) {
|
|
2979
|
+
if (resolveErr instanceof AdtSafetyError) {
|
|
2980
|
+
return errorResult(resolveErr.message);
|
|
2981
|
+
}
|
|
2982
|
+
throw resolveErr;
|
|
2983
|
+
}
|
|
2865
2984
|
srcUrl = `${objectUrl}/source/main`;
|
|
2866
2985
|
}
|
|
2867
2986
|
else if (type === 'FUNC') {
|
|
@@ -2886,11 +3005,20 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2886
3005
|
args.group = group;
|
|
2887
3006
|
}
|
|
2888
3007
|
else {
|
|
3008
|
+
// Discovery gate: refuse transparent-table creates upfront on systems that
|
|
3009
|
+
// don't expose /ddic/tables/ (NW 7.50/7.51). TABL/DS skips this — /structures/
|
|
3010
|
+
// is always available. See issue #285.
|
|
3011
|
+
if ((type === 'TABL' || type === 'TABL/DT') && (action === 'create' || action === 'batch_create')) {
|
|
3012
|
+
if (isTablesEndpointAvailable() === false) {
|
|
3013
|
+
return errorResult(TABL_DT_WRITE_UNAVAILABLE_HINT);
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
2889
3016
|
objectUrl = objectUrlForType(type, name);
|
|
2890
3017
|
srcUrl = sourceUrlForType(type, name);
|
|
2891
3018
|
}
|
|
2892
3019
|
const invalidateWrittenObject = (objType = type, objName = name) => {
|
|
2893
|
-
|
|
3020
|
+
// Source cache is keyed by canonical type (SAPRead collapses TABL/DT, TABL/DS).
|
|
3021
|
+
cachingLayer?.invalidate(canonicalTablType(objType), objName, 'all');
|
|
2894
3022
|
cachingLayer?.inactiveLists.invalidate(client.username);
|
|
2895
3023
|
};
|
|
2896
3024
|
// Helper: enforce allowedPackages for existing objects (update/delete/edit_method/scaffold_rap_handlers).
|
|
@@ -3135,7 +3263,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
3135
3263
|
// 'application/*' — the wildcard lets the SAP server resolve the correct
|
|
3136
3264
|
// handler (matching how ADT Eclipse and abap-adt-api send requests).
|
|
3137
3265
|
const contentType = createContentTypeForType(type);
|
|
3138
|
-
const needsPackageParam = type === 'BDEF' || type === 'TABL';
|
|
3266
|
+
const needsPackageParam = type === 'BDEF' || type === 'TABL' || type === 'TABL/DT' || type === 'TABL/DS';
|
|
3139
3267
|
let result;
|
|
3140
3268
|
try {
|
|
3141
3269
|
result = await createObject(client.http, client.safety, createUrl, body, contentType, effectiveTransport, needsPackageParam ? pkg : undefined, cachedFeatures?.abapRelease);
|
|
@@ -3581,7 +3709,9 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
3581
3709
|
});
|
|
3582
3710
|
}
|
|
3583
3711
|
catch (err) {
|
|
3584
|
-
if (err instanceof AdtApiError &&
|
|
3712
|
+
if (err instanceof AdtApiError &&
|
|
3713
|
+
CDS_DEPENDENCY_SENSITIVE_TYPES.has(canonicalTablType(type)) &&
|
|
3714
|
+
isDeleteDependencyError(err)) {
|
|
3585
3715
|
const hint = await buildCdsDeleteDependencyHint(client, type, name, objectUrl);
|
|
3586
3716
|
if (hint) {
|
|
3587
3717
|
// Attach via extraHint so the LLM-facing formatter renders it after
|
|
@@ -3608,7 +3738,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
3608
3738
|
const activateAtEnd = args.activateAtEnd === true || String(args.activateAtEnd) === 'true';
|
|
3609
3739
|
const defaultPackage = normalizePackageOverride(args.package, '$TMP');
|
|
3610
3740
|
const batchPlan = objects.map((obj) => {
|
|
3611
|
-
const objType =
|
|
3741
|
+
const objType = normalizeWriteObjectType(String(obj.type ?? ''));
|
|
3612
3742
|
const objName = String(obj.name ?? '');
|
|
3613
3743
|
const objPackage = normalizePackageOverride(obj.package, defaultPackage);
|
|
3614
3744
|
const explicitTransport = normalizeTransportOverride(obj.transport) ?? transport;
|
|
@@ -3749,13 +3879,24 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
3749
3879
|
break;
|
|
3750
3880
|
}
|
|
3751
3881
|
}
|
|
3752
|
-
// Step 1: Create the object
|
|
3882
|
+
// Step 1: Create the object (per-entry transparent-table discovery gate;
|
|
3883
|
+
// mirrors the single-create site above. TABL/DS skips it — /structures/ always exists.)
|
|
3884
|
+
if ((objType === 'TABL' || objType === 'TABL/DT') && isTablesEndpointAvailable() === false) {
|
|
3885
|
+
results.push({
|
|
3886
|
+
type: objType,
|
|
3887
|
+
name: objName,
|
|
3888
|
+
packageName: objPackage,
|
|
3889
|
+
status: 'failed',
|
|
3890
|
+
error: TABL_DT_WRITE_UNAVAILABLE_HINT,
|
|
3891
|
+
});
|
|
3892
|
+
break;
|
|
3893
|
+
}
|
|
3753
3894
|
const objUrl = objectUrlForType(objType, objName);
|
|
3754
3895
|
const createUrl = objUrl.replace(/\/[^/]+$/, '');
|
|
3755
3896
|
const objMetadataProps = getMetadataWriteProperties(obj);
|
|
3756
3897
|
const body = buildCreateXml(objType, objName, objPackage, objDescription, objMetadataProps);
|
|
3757
3898
|
const contentType = createContentTypeForType(objType);
|
|
3758
|
-
const needsPackageParam = objType === 'BDEF' || objType === 'TABL';
|
|
3899
|
+
const needsPackageParam = objType === 'BDEF' || objType === 'TABL' || objType === 'TABL/DT' || objType === 'TABL/DS';
|
|
3759
3900
|
try {
|
|
3760
3901
|
await createObject(client.http, client.safety, createUrl, body, contentType, objTransport, needsPackageParam ? objPackage : undefined, cachedFeatures?.abapRelease);
|
|
3761
3902
|
}
|
|
@@ -3942,7 +4083,8 @@ function runRapPreflightValidation(source, type, name, features, configSystemTyp
|
|
|
3942
4083
|
return { blocked: false };
|
|
3943
4084
|
}
|
|
3944
4085
|
const systemType = features?.systemType ?? (configSystemType !== 'auto' ? configSystemType : undefined);
|
|
3945
|
-
|
|
4086
|
+
// Canonicalize so validateRapSource's 'TABL' case matches TABL/DT and TABL/DS.
|
|
4087
|
+
const result = validateRapSource(canonicalTablType(type), source, {
|
|
3946
4088
|
systemType,
|
|
3947
4089
|
abapRelease: features?.abapRelease,
|
|
3948
4090
|
});
|
|
@@ -4196,7 +4338,12 @@ async function handleSAPActivate(client, args, cachingLayer) {
|
|
|
4196
4338
|
const objName = String(o.name ?? '');
|
|
4197
4339
|
let url;
|
|
4198
4340
|
if (objType === 'TABL') {
|
|
4199
|
-
|
|
4341
|
+
// Use the write-path resolver: refuses TABL/DT activation on systems
|
|
4342
|
+
// that don't expose /sap/bc/adt/ddic/tables/ (NW 7.50/7.51), where
|
|
4343
|
+
// activate would hit the wrong endpoint. See issue #285.
|
|
4344
|
+
url = await client.resolveTablObjectUrlForWrite(objName, {
|
|
4345
|
+
tablesEndpointAvailable: isTablesEndpointAvailable(),
|
|
4346
|
+
});
|
|
4200
4347
|
}
|
|
4201
4348
|
else if (objType === 'FUNC') {
|
|
4202
4349
|
let group = String(o.group ?? args.group ?? '').trim();
|
|
@@ -4238,15 +4385,26 @@ async function handleSAPActivate(client, args, cachingLayer) {
|
|
|
4238
4385
|
.join('');
|
|
4239
4386
|
return errorResult(`Batch activation failed for: ${names}.${statusDetails}\n${formatActivationMessages(result)}${combinedDiag}`);
|
|
4240
4387
|
}
|
|
4241
|
-
// Single activation (existing behavior). For TABL we
|
|
4242
|
-
//
|
|
4243
|
-
//
|
|
4388
|
+
// Single activation (existing behavior). For TABL we use the write-path
|
|
4389
|
+
// resolver so transparent-table activations on NW 7.50/7.51 are refused
|
|
4390
|
+
// with the SE11 hint instead of silently activating against /structures/
|
|
4391
|
+
// (which would not even be the right object). See issue #285.
|
|
4244
4392
|
// For FUNC the URL needs the parent function group baked into the path
|
|
4245
4393
|
// (issue #250) — `objectBasePath('FUNC')` deliberately throws so generic
|
|
4246
4394
|
// builders fail loudly. Auto-resolve the group when omitted.
|
|
4247
4395
|
let objectUrl;
|
|
4248
4396
|
if (type === 'TABL') {
|
|
4249
|
-
|
|
4397
|
+
try {
|
|
4398
|
+
objectUrl = await client.resolveTablObjectUrlForWrite(name, {
|
|
4399
|
+
tablesEndpointAvailable: isTablesEndpointAvailable(),
|
|
4400
|
+
});
|
|
4401
|
+
}
|
|
4402
|
+
catch (resolveErr) {
|
|
4403
|
+
if (resolveErr instanceof AdtSafetyError) {
|
|
4404
|
+
return errorResult(resolveErr.message);
|
|
4405
|
+
}
|
|
4406
|
+
throw resolveErr;
|
|
4407
|
+
}
|
|
4250
4408
|
}
|
|
4251
4409
|
else if (type === 'FUNC') {
|
|
4252
4410
|
let group = String(args.group ?? '').trim();
|