arc-1 0.9.4 → 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 +61 -7
- package/dist/adt/client.d.ts.map +1 -1
- package/dist/adt/client.js +238 -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/rap-handlers.d.ts +41 -0
- package/dist/adt/rap-handlers.d.ts.map +1 -1
- package/dist/adt/rap-handlers.js +31 -8
- package/dist/adt/rap-handlers.js.map +1 -1
- package/dist/adt/types.d.ts +8 -0
- package/dist/adt/types.d.ts.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/authz/policy.d.ts.map +1 -1
- package/dist/authz/policy.js +8 -0
- package/dist/authz/policy.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 +383 -51
- package/dist/handlers/intent.js.map +1 -1
- package/dist/handlers/schemas.d.ts +48 -28
- package/dist/handlers/schemas.d.ts.map +1 -1
- package/dist/handlers/schemas.js +30 -0
- package/dist/handlers/schemas.js.map +1 -1
- package/dist/handlers/tools.d.ts.map +1 -1
- package/dist/handlers/tools.js +16 -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 +56 -8
- package/dist/server/config.js.map +1 -1
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +74 -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/stateless-client-store.d.ts +11 -3
- package/dist/server/stateless-client-store.d.ts.map +1 -1
- package/dist/server/stateless-client-store.js +39 -9
- package/dist/server/stateless-client-store.js.map +1 -1
- package/dist/server/types.d.ts +37 -6
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +3 -1
- package/dist/server/types.js.map +1 -1
- package/dist/server/xsuaa.d.ts +10 -1
- package/dist/server/xsuaa.d.ts.map +1 -1
- package/dist/server/xsuaa.js +38 -5
- package/dist/server/xsuaa.js.map +1 -1
- package/package.json +15 -13
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,17 +764,70 @@ 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.
|
|
811
|
+
// For SAPSearch.tadir_lookup with source='db'|'both', synthesize a sub-action key so the
|
|
812
|
+
// sql-scoped policy entry kicks in (otherwise viewer-only profiles could piggyback on the
|
|
813
|
+
// ADT info-system route to issue freestyle SQL).
|
|
769
814
|
// Runs BEFORE Zod validation so scope errors don't leak schema details to unauthorized callers.
|
|
770
|
-
|
|
815
|
+
let actionOrType = toolName === 'SAPRead'
|
|
771
816
|
? typeof args.type === 'string'
|
|
772
817
|
? args.type
|
|
773
818
|
: undefined
|
|
774
819
|
: typeof args.action === 'string'
|
|
775
820
|
? args.action
|
|
776
821
|
: undefined;
|
|
822
|
+
if (toolName === 'SAPSearch' &&
|
|
823
|
+
typeof args.searchType === 'string' &&
|
|
824
|
+
args.searchType === 'tadir_lookup' &&
|
|
825
|
+
typeof args.source === 'string') {
|
|
826
|
+
const src = args.source.toLowerCase();
|
|
827
|
+
if (src === 'db' || src === 'both') {
|
|
828
|
+
actionOrType = `tadir_lookup_${src}`;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
777
831
|
const policy = getActionPolicy(toolName, actionOrType);
|
|
778
832
|
if (authInfo && policy) {
|
|
779
833
|
if (!hasRequiredScope(authInfo, policy.scope)) {
|
|
@@ -933,6 +987,28 @@ export async function handleToolCall(client, config, toolName, args, authInfo, _
|
|
|
933
987
|
function isBtpSystem() {
|
|
934
988
|
return cachedFeatures?.systemType === 'btp';
|
|
935
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.';
|
|
936
1012
|
/** BTP-specific error messages for unavailable operations */
|
|
937
1013
|
const BTP_HINTS = {
|
|
938
1014
|
PROG: 'Executable programs (reports) are not available on BTP ABAP Environment. Use CLAS with IF_OO_ADT_CLASSRUN for console applications.',
|
|
@@ -1425,16 +1501,92 @@ async function handleSAPSearch(client, args) {
|
|
|
1425
1501
|
return errorResult('SAPSearch(searchType="tadir_lookup") requires names[] or query with at least one name.');
|
|
1426
1502
|
}
|
|
1427
1503
|
const objectTypes = extractLookupObjectTypes(args.objectType, args.objectTypes);
|
|
1428
|
-
const
|
|
1429
|
-
const
|
|
1430
|
-
|
|
1504
|
+
const rawSource = typeof args.source === 'string' ? args.source.toLowerCase() : 'adt';
|
|
1505
|
+
const source = rawSource === 'db' || rawSource === 'both' ? rawSource : 'adt';
|
|
1506
|
+
// Stamp each match with provenance so a merged 'both' result is unambiguous and
|
|
1507
|
+
// viewer tooling can colour-code ghost rows. The DB path already stamps `_origin:'db'`
|
|
1508
|
+
// (see `lookupObjectsViaDb`); we stamp ADT matches here.
|
|
1509
|
+
const tagOrigin = (lookups, origin) => lookups.map((l) => ({
|
|
1510
|
+
...l,
|
|
1511
|
+
matches: l.matches.map((m) => ({ ...m, _origin: m._origin ?? origin })),
|
|
1512
|
+
}));
|
|
1513
|
+
let finalLookups;
|
|
1431
1514
|
const wildcardNames = names.filter((name) => name.includes('*'));
|
|
1432
|
-
const warnings =
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1515
|
+
const warnings = [];
|
|
1516
|
+
let splitBrain = [];
|
|
1517
|
+
if (source === 'adt') {
|
|
1518
|
+
finalLookups = tagOrigin(await client.lookupObjects(names, { maxResults, objectTypes }), 'adt');
|
|
1519
|
+
}
|
|
1520
|
+
else if (source === 'db') {
|
|
1521
|
+
// The 'db' path bypasses ADT info-system entirely; `lookupObjectsViaDb` already
|
|
1522
|
+
// tags matches with `_origin:'db'`. Safety/scope gating runs at handleToolCall
|
|
1523
|
+
// and in client.runQuery (FreeSQL operation), so unauthorized callers never reach here.
|
|
1524
|
+
finalLookups = await client.lookupObjectsViaDb(names, { maxResults, objectTypes });
|
|
1525
|
+
}
|
|
1526
|
+
else {
|
|
1527
|
+
// 'both' — parallel ADT + DB, merge per name with dedupe.
|
|
1528
|
+
const [adtLookups, dbLookups] = await Promise.all([
|
|
1529
|
+
client.lookupObjects(names, { maxResults, objectTypes }).then((r) => tagOrigin(r, 'adt')),
|
|
1530
|
+
client.lookupObjectsViaDb(names, { maxResults, objectTypes }),
|
|
1531
|
+
]);
|
|
1532
|
+
const dbByName = new Map(dbLookups.map((l) => [l.name.toUpperCase(), l]));
|
|
1533
|
+
const adtByName = new Map(adtLookups.map((l) => [l.name.toUpperCase(), l]));
|
|
1534
|
+
finalLookups = names.map((rawName) => {
|
|
1535
|
+
const upper = rawName.toUpperCase();
|
|
1536
|
+
const adt = adtByName.get(upper);
|
|
1537
|
+
const db = dbByName.get(upper);
|
|
1538
|
+
const adtMatches = adt?.matches ?? [];
|
|
1539
|
+
const dbMatches = db?.matches ?? [];
|
|
1540
|
+
// Dedupe by (baseObjectType, objectName) — TADIR stores bare types ('DDLS')
|
|
1541
|
+
// while ADT info-system returns slash-form ('DDLS/DF'). Stripping the suffix
|
|
1542
|
+
// keeps the same logical object from appearing twice in the merged matches.
|
|
1543
|
+
// Preserve the more-specific slash form when both originate from ADT+DB.
|
|
1544
|
+
const seen = new Map();
|
|
1545
|
+
const baseKey = (m) => `${(m.objectType.split('/')[0] || m.objectType).toUpperCase()}${m.objectName.toUpperCase()}`;
|
|
1546
|
+
for (const m of adtMatches)
|
|
1547
|
+
seen.set(baseKey(m), m);
|
|
1548
|
+
for (const m of dbMatches) {
|
|
1549
|
+
const k = baseKey(m);
|
|
1550
|
+
if (!seen.has(k))
|
|
1551
|
+
seen.set(k, m);
|
|
1552
|
+
}
|
|
1553
|
+
const mergedMatches = [...seen.values()];
|
|
1554
|
+
// Split-brain detection: an object is divergent if exactly one source has matches.
|
|
1555
|
+
// (Zero matches on both sides = consistent absence; matches on both = consistent presence.)
|
|
1556
|
+
if (adtMatches.length > 0 !== dbMatches.length > 0) {
|
|
1557
|
+
splitBrain.push(rawName);
|
|
1558
|
+
}
|
|
1559
|
+
return { name: rawName, found: mergedMatches.length > 0, matches: mergedMatches };
|
|
1560
|
+
});
|
|
1561
|
+
// Compose human-friendly warnings per split-brain name. Keep them grounded in
|
|
1562
|
+
// the most common cause (TADIR ghost from aborted create/delete) so LLM clients
|
|
1563
|
+
// can suggest the right cleanup path without inventing a new pointer.
|
|
1564
|
+
for (const name of splitBrain) {
|
|
1565
|
+
const adt = adtByName.get(name.toUpperCase());
|
|
1566
|
+
const db = dbByName.get(name.toUpperCase());
|
|
1567
|
+
const adtHas = (adt?.matches.length ?? 0) > 0;
|
|
1568
|
+
const dbHas = (db?.matches.length ?? 0) > 0;
|
|
1569
|
+
if (dbHas && !adtHas) {
|
|
1570
|
+
warnings.push(`${name} exists in TADIR (DB) but ADT cannot resolve it — likely a TADIR ghost from an aborted create/delete cycle. Consider RS_DD_TADIR_CLEANUP or manual SE03 cleanup.`);
|
|
1571
|
+
}
|
|
1572
|
+
else if (adtHas && !dbHas) {
|
|
1573
|
+
warnings.push(`${name} resolves via ADT but is not present in the TADIR row scan — likely a release-time mismatch or a type filter excluding the row. Re-run with broader objectTypes or no filter to confirm.`);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
// Dedupe split-brain names (defensive; merge loop should already avoid duplicates).
|
|
1578
|
+
splitBrain = [...new Set(splitBrain)];
|
|
1579
|
+
if (wildcardNames.length > 0) {
|
|
1580
|
+
warnings.push(`tadir_lookup performs exact-name lookup; wildcard characters are treated literally for: ${wildcardNames.join(', ')}`);
|
|
1581
|
+
}
|
|
1582
|
+
const missing = finalLookups.filter((l) => !l.found).map((l) => l.name);
|
|
1583
|
+
const matchCount = finalLookups.reduce((count, lookup) => count + lookup.matches.length, 0);
|
|
1584
|
+
const payload = { count: matchCount, lookups: finalLookups, missing };
|
|
1585
|
+
if (splitBrain.length > 0)
|
|
1586
|
+
payload.splitBrain = splitBrain;
|
|
1587
|
+
if (warnings.length > 0)
|
|
1588
|
+
payload.warnings = warnings;
|
|
1589
|
+
return textResult(JSON.stringify(payload, null, 2));
|
|
1438
1590
|
}
|
|
1439
1591
|
if (searchType === 'source_code') {
|
|
1440
1592
|
// Source code search: do NOT transliterate — source can contain umlauts in strings/comments
|
|
@@ -2165,18 +2317,24 @@ export function buildCreateXml(type, name, pkg, description, properties) {
|
|
|
2165
2317
|
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
2166
2318
|
</dcl:dclSource>`;
|
|
2167
2319
|
case 'TABL':
|
|
2168
|
-
|
|
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';
|
|
2169
2326
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2170
2327
|
<blue:blueSource xmlns:blue="http://www.sap.com/wbobj/blue"
|
|
2171
2328
|
xmlns:adtcore="http://www.sap.com/adt/core"
|
|
2172
2329
|
adtcore:description="${escapeXml(description)}"
|
|
2173
2330
|
adtcore:name="${escapeXml(name)}"
|
|
2174
|
-
adtcore:type="
|
|
2331
|
+
adtcore:type="${adtType}"
|
|
2175
2332
|
adtcore:masterLanguage="EN"
|
|
2176
2333
|
adtcore:masterSystem="H00"
|
|
2177
2334
|
adtcore:responsible="DEVELOPER">
|
|
2178
2335
|
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
2179
2336
|
</blue:blueSource>`;
|
|
2337
|
+
}
|
|
2180
2338
|
case 'BDEF':
|
|
2181
2339
|
// BDEF uses SAP's "blue" framework — blue:blueSource with http://www.sap.com/wbobj/blue namespace.
|
|
2182
2340
|
// Confirmed by vibing-steampunk (Go) and fr0ster (TypeScript) reference implementations.
|
|
@@ -2481,6 +2639,37 @@ export function normalizeObjectType(type) {
|
|
|
2481
2639
|
return '';
|
|
2482
2640
|
return SLASH_TYPE_MAP[normalized] ?? normalized;
|
|
2483
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
|
+
}
|
|
2484
2673
|
/** Normalize type fields before schema validation so slash/case aliases are accepted. */
|
|
2485
2674
|
function normalizeTypeArgsForValidation(toolName, args) {
|
|
2486
2675
|
switch (toolName) {
|
|
@@ -2491,14 +2680,15 @@ function normalizeTypeArgsForValidation(toolName, args) {
|
|
|
2491
2680
|
objectType: args.objectType === undefined ? undefined : normalizeObjectType(String(args.objectType ?? '')),
|
|
2492
2681
|
};
|
|
2493
2682
|
case 'SAPWrite':
|
|
2683
|
+
// SAPWrite preserves TABL/DT and TABL/DS so the create path can route by subtype.
|
|
2494
2684
|
return {
|
|
2495
2685
|
...args,
|
|
2496
|
-
type: args.type === undefined ? undefined :
|
|
2686
|
+
type: args.type === undefined ? undefined : normalizeWriteObjectType(String(args.type ?? '')),
|
|
2497
2687
|
objects: Array.isArray(args.objects)
|
|
2498
2688
|
? args.objects.map((obj) => typeof obj === 'object' && obj !== null
|
|
2499
2689
|
? {
|
|
2500
2690
|
...obj,
|
|
2501
|
-
type:
|
|
2691
|
+
type: normalizeWriteObjectType(String(obj.type ?? '')),
|
|
2502
2692
|
}
|
|
2503
2693
|
: obj)
|
|
2504
2694
|
: args.objects,
|
|
@@ -2600,10 +2790,13 @@ export function objectBasePath(type) {
|
|
|
2600
2790
|
case 'SRVB':
|
|
2601
2791
|
return '/sap/bc/adt/businessservices/bindings/';
|
|
2602
2792
|
case 'TABL':
|
|
2603
|
-
|
|
2604
|
-
//
|
|
2605
|
-
// 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.
|
|
2606
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/';
|
|
2607
2800
|
case 'DOMA':
|
|
2608
2801
|
return '/sap/bc/adt/ddic/domains/';
|
|
2609
2802
|
case 'DTEL':
|
|
@@ -2733,7 +2926,7 @@ function stripIncludeHeader(source) {
|
|
|
2733
2926
|
// ─── SAPWrite Handler ────────────────────────────────────────────────
|
|
2734
2927
|
async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
2735
2928
|
const action = String(args.action ?? '');
|
|
2736
|
-
const type =
|
|
2929
|
+
const type = normalizeWriteObjectType(String(args.type ?? ''));
|
|
2737
2930
|
const name = String(args.name ?? '');
|
|
2738
2931
|
const source = String(args.source ?? '');
|
|
2739
2932
|
const hasSource = typeof args.source === 'string';
|
|
@@ -2772,8 +2965,22 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2772
2965
|
// `buildCreateXml('FUNC', …, properties)` finds it.
|
|
2773
2966
|
let objectUrl;
|
|
2774
2967
|
let srcUrl;
|
|
2775
|
-
if (type === 'TABL'
|
|
2776
|
-
|
|
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
|
+
}
|
|
2777
2984
|
srcUrl = `${objectUrl}/source/main`;
|
|
2778
2985
|
}
|
|
2779
2986
|
else if (type === 'FUNC') {
|
|
@@ -2798,11 +3005,20 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
2798
3005
|
args.group = group;
|
|
2799
3006
|
}
|
|
2800
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
|
+
}
|
|
2801
3016
|
objectUrl = objectUrlForType(type, name);
|
|
2802
3017
|
srcUrl = sourceUrlForType(type, name);
|
|
2803
3018
|
}
|
|
2804
3019
|
const invalidateWrittenObject = (objType = type, objName = name) => {
|
|
2805
|
-
|
|
3020
|
+
// Source cache is keyed by canonical type (SAPRead collapses TABL/DT, TABL/DS).
|
|
3021
|
+
cachingLayer?.invalidate(canonicalTablType(objType), objName, 'all');
|
|
2806
3022
|
cachingLayer?.inactiveLists.invalidate(client.username);
|
|
2807
3023
|
};
|
|
2808
3024
|
// Helper: enforce allowedPackages for existing objects (update/delete/edit_method/scaffold_rap_handlers).
|
|
@@ -3047,7 +3263,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
3047
3263
|
// 'application/*' — the wildcard lets the SAP server resolve the correct
|
|
3048
3264
|
// handler (matching how ADT Eclipse and abap-adt-api send requests).
|
|
3049
3265
|
const contentType = createContentTypeForType(type);
|
|
3050
|
-
const needsPackageParam = type === 'BDEF' || type === 'TABL';
|
|
3266
|
+
const needsPackageParam = type === 'BDEF' || type === 'TABL' || type === 'TABL/DT' || type === 'TABL/DS';
|
|
3051
3267
|
let result;
|
|
3052
3268
|
try {
|
|
3053
3269
|
result = await createObject(client.http, client.safety, createUrl, body, contentType, effectiveTransport, needsPackageParam ? pkg : undefined, cachedFeatures?.abapRelease);
|
|
@@ -3493,7 +3709,9 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
3493
3709
|
});
|
|
3494
3710
|
}
|
|
3495
3711
|
catch (err) {
|
|
3496
|
-
if (err instanceof AdtApiError &&
|
|
3712
|
+
if (err instanceof AdtApiError &&
|
|
3713
|
+
CDS_DEPENDENCY_SENSITIVE_TYPES.has(canonicalTablType(type)) &&
|
|
3714
|
+
isDeleteDependencyError(err)) {
|
|
3497
3715
|
const hint = await buildCdsDeleteDependencyHint(client, type, name, objectUrl);
|
|
3498
3716
|
if (hint) {
|
|
3499
3717
|
// Attach via extraHint so the LLM-facing formatter renders it after
|
|
@@ -3513,9 +3731,14 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
3513
3731
|
if (!objects || !Array.isArray(objects) || objects.length === 0) {
|
|
3514
3732
|
return errorResult('"objects" array is required and must be non-empty for batch_create action.');
|
|
3515
3733
|
}
|
|
3734
|
+
// Opt-in deferred-activation: writes every object as an inactive draft first,
|
|
3735
|
+
// then issues a single terminal activateBatch over the written subset. Use case:
|
|
3736
|
+
// composition-linked DDLS / interdependent RAP graphs where per-object inline
|
|
3737
|
+
// activate() can't resolve cross-references to not-yet-active siblings.
|
|
3738
|
+
const activateAtEnd = args.activateAtEnd === true || String(args.activateAtEnd) === 'true';
|
|
3516
3739
|
const defaultPackage = normalizePackageOverride(args.package, '$TMP');
|
|
3517
3740
|
const batchPlan = objects.map((obj) => {
|
|
3518
|
-
const objType =
|
|
3741
|
+
const objType = normalizeWriteObjectType(String(obj.type ?? ''));
|
|
3519
3742
|
const objName = String(obj.name ?? '');
|
|
3520
3743
|
const objPackage = normalizePackageOverride(obj.package, defaultPackage);
|
|
3521
3744
|
const explicitTransport = normalizeTransportOverride(obj.transport) ?? transport;
|
|
@@ -3574,6 +3797,9 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
3574
3797
|
// guard fires for every MSAG entry, but a batch typically shares one transport — cache
|
|
3575
3798
|
// the lookup result to avoid one HTTP roundtrip per object.
|
|
3576
3799
|
const transportLookupCache = new Map();
|
|
3800
|
+
// Accumulated objects whose create + source-write phase succeeded — used by the
|
|
3801
|
+
// terminal activateBatch when activateAtEnd=true. Order matches the input order.
|
|
3802
|
+
const writtenObjects = [];
|
|
3577
3803
|
for (const plan of batchPlan) {
|
|
3578
3804
|
const { obj, type: objType, name: objName, packageName: objPackage } = plan;
|
|
3579
3805
|
const objTransport = plan.explicitTransport ?? autoTransportByPackage.get(objPackage);
|
|
@@ -3653,13 +3879,24 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
3653
3879
|
break;
|
|
3654
3880
|
}
|
|
3655
3881
|
}
|
|
3656
|
-
// 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
|
+
}
|
|
3657
3894
|
const objUrl = objectUrlForType(objType, objName);
|
|
3658
3895
|
const createUrl = objUrl.replace(/\/[^/]+$/, '');
|
|
3659
3896
|
const objMetadataProps = getMetadataWriteProperties(obj);
|
|
3660
3897
|
const body = buildCreateXml(objType, objName, objPackage, objDescription, objMetadataProps);
|
|
3661
3898
|
const contentType = createContentTypeForType(objType);
|
|
3662
|
-
const needsPackageParam = objType === 'BDEF' || objType === 'TABL';
|
|
3899
|
+
const needsPackageParam = objType === 'BDEF' || objType === 'TABL' || objType === 'TABL/DT' || objType === 'TABL/DS';
|
|
3663
3900
|
try {
|
|
3664
3901
|
await createObject(client.http, client.safety, createUrl, body, contentType, objTransport, needsPackageParam ? objPackage : undefined, cachedFeatures?.abapRelease);
|
|
3665
3902
|
}
|
|
@@ -3690,20 +3927,50 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
3690
3927
|
const srcUrl = sourceUrlForType(objType, objName);
|
|
3691
3928
|
await safeUpdateSource(client.http, client.safety, objUrl, srcUrl, objSource, objTransport, cachedFeatures?.abapRelease);
|
|
3692
3929
|
}
|
|
3693
|
-
//
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3930
|
+
// Resolve the activation URL up front so both the inline path and the
|
|
3931
|
+
// deferred terminal-activate path use the same URL. FUNC needs the parent
|
|
3932
|
+
// function-group baked into the path (issue #250); objectUrlForType throws
|
|
3933
|
+
// for FUNC so we mirror the FUNC-aware resolver from handleSAPActivate. For
|
|
3934
|
+
// TABL we keep objUrl (already resolved to /tables/) — DDIC-structure FMs
|
|
3935
|
+
// aren't a real concept and the create path doesn't expose one.
|
|
3936
|
+
let activationUrl = objUrl;
|
|
3937
|
+
if (objType === 'FUNC') {
|
|
3938
|
+
let group = String(obj.group ?? args.group ?? '').trim();
|
|
3939
|
+
if (!group) {
|
|
3940
|
+
const resolved = cachingLayer
|
|
3941
|
+
? await cachingLayer.resolveFuncGroup(client, objName)
|
|
3942
|
+
: await client.resolveFunctionGroup(objName);
|
|
3943
|
+
if (!resolved) {
|
|
3944
|
+
throw new Error(`Cannot resolve function group for FM "${objName}" in batch_create activation step. Provide "group" on the FUNC entry.`);
|
|
3945
|
+
}
|
|
3946
|
+
group = resolved;
|
|
3947
|
+
}
|
|
3948
|
+
const groupLc = encodeURIComponent(group.toLowerCase());
|
|
3949
|
+
activationUrl = `/sap/bc/adt/functions/groups/${groupLc}/fmodules/${encodeURIComponent(objName.toLowerCase())}`;
|
|
3950
|
+
}
|
|
3951
|
+
if (activateAtEnd) {
|
|
3952
|
+
// Step 3 deferred: track this object for the terminal activateBatch call.
|
|
3953
|
+
// Cache invalidation also moves to AFTER the terminal activate succeeds —
|
|
3954
|
+
// invalidating now would let the next read see a draft we couldn't activate.
|
|
3955
|
+
writtenObjects.push({ type: objType, name: objName, url: activationUrl });
|
|
3956
|
+
results.push({ type: objType, name: objName, packageName: objPackage, status: 'success' });
|
|
3957
|
+
}
|
|
3958
|
+
else {
|
|
3959
|
+
// Step 3: Activate the object (inline, default behavior).
|
|
3960
|
+
const activationResult = await activate(client.http, client.safety, activationUrl);
|
|
3961
|
+
if (!activationResult.success) {
|
|
3962
|
+
results.push({
|
|
3963
|
+
type: objType,
|
|
3964
|
+
name: objName,
|
|
3965
|
+
packageName: objPackage,
|
|
3966
|
+
status: 'failed',
|
|
3967
|
+
error: `activation failed: ${activationResult.messages.join('; ')}`,
|
|
3968
|
+
});
|
|
3969
|
+
break;
|
|
3970
|
+
}
|
|
3971
|
+
invalidateWrittenObject(objType, objName);
|
|
3972
|
+
results.push({ type: objType, name: objName, packageName: objPackage, status: 'success' });
|
|
3704
3973
|
}
|
|
3705
|
-
invalidateWrittenObject(objType, objName);
|
|
3706
|
-
results.push({ type: objType, name: objName, packageName: objPackage, status: 'success' });
|
|
3707
3974
|
}
|
|
3708
3975
|
catch (err) {
|
|
3709
3976
|
results.push({
|
|
@@ -3728,6 +3995,52 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
3728
3995
|
error: 'skipped — stopped after previous failure',
|
|
3729
3996
|
});
|
|
3730
3997
|
}
|
|
3998
|
+
// ── Terminal activateBatch (activateAtEnd=true) ─────────────────────
|
|
3999
|
+
// After every write-phase succeeded (or broke off early), issue ONE batch
|
|
4000
|
+
// activate over the already-written subset. This is the killer feature
|
|
4001
|
+
// for composition-linked DDLS and RAP behavior stacks — SAP's activator
|
|
4002
|
+
// sees the whole graph in a single POST and resolves cross-references
|
|
4003
|
+
// internally, so parent → child siblings activate cleanly.
|
|
4004
|
+
let terminalActivationFailure;
|
|
4005
|
+
if (activateAtEnd && writtenObjects.length > 0) {
|
|
4006
|
+
const activationOutcome = await activateBatch(client.http, client.safety, writtenObjects);
|
|
4007
|
+
if (activationOutcome.success) {
|
|
4008
|
+
// Defensive: per-object status was already 'success' from the write phase.
|
|
4009
|
+
// Cache invalidation moves here so a failed terminal activate doesn't strand
|
|
4010
|
+
// a stale 'active' cache entry. Invalidate inactive-lists once for the user.
|
|
4011
|
+
for (const o of writtenObjects) {
|
|
4012
|
+
cachingLayer?.invalidate(o.type, o.name, 'all');
|
|
4013
|
+
}
|
|
4014
|
+
cachingLayer?.inactiveLists.invalidate(client.username);
|
|
4015
|
+
}
|
|
4016
|
+
else {
|
|
4017
|
+
// Flip every written-but-not-yet-activated entry to 'failed', preserving the
|
|
4018
|
+
// "create + source-write succeeded" context. Reuse the existing per-object
|
|
4019
|
+
// diagnostic mapper so callers see the activation messages keyed by object name.
|
|
4020
|
+
const batchStatuses = buildBatchActivationStatuses(writtenObjects, activationOutcome);
|
|
4021
|
+
const statusDetails = formatBatchActivationStatuses(batchStatuses);
|
|
4022
|
+
terminalActivationFailure = statusDetails;
|
|
4023
|
+
const statusByName = new Map(batchStatuses.map((s) => [`${s.type}${s.name}`, s]));
|
|
4024
|
+
for (const result of results) {
|
|
4025
|
+
if (result.status !== 'success')
|
|
4026
|
+
continue;
|
|
4027
|
+
const key = `${result.type}${result.name}`;
|
|
4028
|
+
const matched = statusByName.get(key);
|
|
4029
|
+
if (!matched)
|
|
4030
|
+
continue;
|
|
4031
|
+
// Some entries may still report status 'active' if the activator returned
|
|
4032
|
+
// success: false but had no per-object error details — keep them as 'success'.
|
|
4033
|
+
if (matched.status === 'active')
|
|
4034
|
+
continue;
|
|
4035
|
+
result.status = 'failed';
|
|
4036
|
+
const detail = matched.messages.length > 0 ? ` — ${matched.messages.join('; ')}` : '';
|
|
4037
|
+
// Preserve the "create + source-write succeeded" context so the user sees that
|
|
4038
|
+
// the failure was specifically the activation step, not the write step.
|
|
4039
|
+
result.error = `${writtenObjects.length}/${writtenObjects.length} written, batch activation failed${detail}`;
|
|
4040
|
+
}
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
// ────────────────────────────────────────────────────────────────────
|
|
3731
4044
|
const summary = results
|
|
3732
4045
|
.map((r) => r.status === 'success'
|
|
3733
4046
|
? `${r.name} (${r.type}) ✓ [${r.packageName}]`
|
|
@@ -3736,19 +4049,21 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
3736
4049
|
const successCount = results.filter((r) => r.status === 'success').length;
|
|
3737
4050
|
const hasFailure = results.some((r) => r.status === 'failed');
|
|
3738
4051
|
const warningSuffix = batchWarnings.length > 0 ? `\n\nRAP preflight warnings:\n- ${batchWarnings.join('\n- ')}` : '';
|
|
4052
|
+
const activateAtEndSuffix = terminalActivationFailure !== undefined ? `\n\nBatch activation diagnostics:${terminalActivationFailure}` : '';
|
|
3739
4053
|
const packageNames = [...new Set(batchPlan.map((item) => item.packageName))];
|
|
3740
4054
|
const packageSummary = packageNames.length === 1
|
|
3741
4055
|
? `in package ${packageNames[0]}`
|
|
3742
4056
|
: packageNames.length <= 3
|
|
3743
4057
|
? `across packages [${packageNames.join(', ')}]`
|
|
3744
4058
|
: `across ${packageNames.length} packages`;
|
|
4059
|
+
const activateAtEndPrefix = activateAtEnd ? '; activated as a single batch' : '';
|
|
3745
4060
|
if (hasFailure) {
|
|
3746
4061
|
const cleanupHint = successCount > 0
|
|
3747
4062
|
? ` Note: ${successCount} already-created object(s) remain on the SAP system and may need manual cleanup.`
|
|
3748
4063
|
: '';
|
|
3749
|
-
return errorResult(`Batch created ${successCount}/${objects.length} objects ${packageSummary}: ${summary}${cleanupHint}${warningSuffix}`);
|
|
4064
|
+
return errorResult(`Batch created ${successCount}/${objects.length} objects ${packageSummary}${activateAtEndPrefix}: ${summary}${cleanupHint}${warningSuffix}${activateAtEndSuffix}`);
|
|
3750
4065
|
}
|
|
3751
|
-
return textResult(`Batch created ${successCount} objects ${packageSummary}: ${summary}${warningSuffix}`);
|
|
4066
|
+
return textResult(`Batch created ${successCount} objects ${packageSummary}${activateAtEndPrefix}: ${summary}${warningSuffix}${activateAtEndSuffix}`);
|
|
3752
4067
|
}
|
|
3753
4068
|
default:
|
|
3754
4069
|
return errorResult(`Unknown SAPWrite action: ${action}. Supported: create, update, delete, edit_method, batch_create, scaffold_rap_handlers, generate_behavior_implementation`);
|
|
@@ -3768,7 +4083,8 @@ function runRapPreflightValidation(source, type, name, features, configSystemTyp
|
|
|
3768
4083
|
return { blocked: false };
|
|
3769
4084
|
}
|
|
3770
4085
|
const systemType = features?.systemType ?? (configSystemType !== 'auto' ? configSystemType : undefined);
|
|
3771
|
-
|
|
4086
|
+
// Canonicalize so validateRapSource's 'TABL' case matches TABL/DT and TABL/DS.
|
|
4087
|
+
const result = validateRapSource(canonicalTablType(type), source, {
|
|
3772
4088
|
systemType,
|
|
3773
4089
|
abapRelease: features?.abapRelease,
|
|
3774
4090
|
});
|
|
@@ -4022,7 +4338,12 @@ async function handleSAPActivate(client, args, cachingLayer) {
|
|
|
4022
4338
|
const objName = String(o.name ?? '');
|
|
4023
4339
|
let url;
|
|
4024
4340
|
if (objType === 'TABL') {
|
|
4025
|
-
|
|
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
|
+
});
|
|
4026
4347
|
}
|
|
4027
4348
|
else if (objType === 'FUNC') {
|
|
4028
4349
|
let group = String(o.group ?? args.group ?? '').trim();
|
|
@@ -4064,15 +4385,26 @@ async function handleSAPActivate(client, args, cachingLayer) {
|
|
|
4064
4385
|
.join('');
|
|
4065
4386
|
return errorResult(`Batch activation failed for: ${names}.${statusDetails}\n${formatActivationMessages(result)}${combinedDiag}`);
|
|
4066
4387
|
}
|
|
4067
|
-
// Single activation (existing behavior). For TABL we
|
|
4068
|
-
//
|
|
4069
|
-
//
|
|
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.
|
|
4070
4392
|
// For FUNC the URL needs the parent function group baked into the path
|
|
4071
4393
|
// (issue #250) — `objectBasePath('FUNC')` deliberately throws so generic
|
|
4072
4394
|
// builders fail loudly. Auto-resolve the group when omitted.
|
|
4073
4395
|
let objectUrl;
|
|
4074
4396
|
if (type === 'TABL') {
|
|
4075
|
-
|
|
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
|
+
}
|
|
4076
4408
|
}
|
|
4077
4409
|
else if (type === 'FUNC') {
|
|
4078
4410
|
let group = String(args.group ?? '').trim();
|