arc-1 0.6.1 → 0.6.3
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 +18 -20
- package/dist/adt/btp.d.ts +19 -12
- package/dist/adt/btp.d.ts.map +1 -1
- package/dist/adt/btp.js +48 -35
- package/dist/adt/btp.js.map +1 -1
- package/dist/adt/client.d.ts +12 -2
- package/dist/adt/client.d.ts.map +1 -1
- package/dist/adt/client.js +27 -2
- package/dist/adt/client.js.map +1 -1
- package/dist/adt/codeintel.d.ts.map +1 -1
- package/dist/adt/codeintel.js +1 -10
- package/dist/adt/codeintel.js.map +1 -1
- package/dist/adt/config.d.ts +1 -0
- package/dist/adt/config.d.ts.map +1 -1
- package/dist/adt/config.js +1 -0
- package/dist/adt/config.js.map +1 -1
- package/dist/adt/crud.d.ts +7 -0
- package/dist/adt/crud.d.ts.map +1 -1
- package/dist/adt/crud.js +33 -2
- package/dist/adt/crud.js.map +1 -1
- package/dist/adt/ddic-xml.d.ts +78 -0
- package/dist/adt/ddic-xml.d.ts.map +1 -0
- package/dist/adt/ddic-xml.js +203 -0
- package/dist/adt/ddic-xml.js.map +1 -0
- package/dist/adt/devtools.d.ts +19 -11
- package/dist/adt/devtools.d.ts.map +1 -1
- package/dist/adt/devtools.js +61 -17
- package/dist/adt/devtools.js.map +1 -1
- package/dist/adt/errors.d.ts +12 -0
- package/dist/adt/errors.d.ts.map +1 -1
- package/dist/adt/errors.js +42 -0
- package/dist/adt/errors.js.map +1 -1
- package/dist/adt/features.d.ts.map +1 -1
- package/dist/adt/features.js +20 -3
- package/dist/adt/features.js.map +1 -1
- package/dist/adt/flp.d.ts +43 -0
- package/dist/adt/flp.d.ts.map +1 -0
- package/dist/adt/flp.js +213 -0
- package/dist/adt/flp.js.map +1 -0
- package/dist/adt/http.d.ts +2 -0
- package/dist/adt/http.d.ts.map +1 -1
- package/dist/adt/http.js +83 -6
- package/dist/adt/http.js.map +1 -1
- package/dist/adt/transport.d.ts +42 -3
- package/dist/adt/transport.d.ts.map +1 -1
- package/dist/adt/transport.js +197 -20
- package/dist/adt/transport.js.map +1 -1
- package/dist/adt/types.d.ts +58 -0
- package/dist/adt/types.d.ts.map +1 -1
- package/dist/adt/xml-parser.d.ts +14 -1
- package/dist/adt/xml-parser.d.ts.map +1 -1
- package/dist/adt/xml-parser.js +70 -1
- package/dist/adt/xml-parser.js.map +1 -1
- package/dist/handlers/intent.d.ts +4 -6
- package/dist/handlers/intent.d.ts.map +1 -1
- package/dist/handlers/intent.js +931 -47
- package/dist/handlers/intent.js.map +1 -1
- package/dist/handlers/schemas.d.ts +225 -0
- package/dist/handlers/schemas.d.ts.map +1 -1
- package/dist/handlers/schemas.js +190 -4
- package/dist/handlers/schemas.js.map +1 -1
- package/dist/handlers/tools.d.ts.map +1 -1
- package/dist/handlers/tools.js +308 -43
- package/dist/handlers/tools.js.map +1 -1
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +14 -3
- package/dist/server/config.js.map +1 -1
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +32 -3
- package/dist/server/http.js.map +1 -1
- package/dist/server/server.d.ts +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +37 -6
- package/dist/server/server.js.map +1 -1
- package/dist/server/types.d.ts +1 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +4 -3
- package/dist/server/types.js.map +1 -1
- package/dist/server/xsuaa.d.ts +13 -0
- package/dist/server/xsuaa.d.ts.map +1 -1
- package/dist/server/xsuaa.js +28 -2
- package/dist/server/xsuaa.js.map +1 -1
- package/package.json +10 -5
package/dist/handlers/intent.js
CHANGED
|
@@ -10,13 +10,15 @@
|
|
|
10
10
|
* leaked to the LLM — only user-friendly error messages.
|
|
11
11
|
*/
|
|
12
12
|
import { findDefinition, findReferences, findWhereUsed, getCompletion, } from '../adt/codeintel.js';
|
|
13
|
-
import { createObject, deleteObject, lockObject, safeUpdateSource, unlockObject } from '../adt/crud.js';
|
|
13
|
+
import { createObject, deleteObject, lockObject, safeUpdateObject, safeUpdateSource, unlockObject, updateObject, } from '../adt/crud.js';
|
|
14
|
+
import { buildDataElementXml, buildDomainXml, buildMessageClassXml, buildPackageXml, buildServiceBindingXml, } from '../adt/ddic-xml.js';
|
|
14
15
|
import { activate, activateBatch, publishServiceBinding, runAtcCheck, runUnitTests, syntaxCheck, unpublishServiceBinding, } from '../adt/devtools.js';
|
|
15
16
|
import { getDump, getTraceDbAccesses, getTraceHitlist, getTraceStatements, listDumps, listTraces, } from '../adt/diagnostics.js';
|
|
16
17
|
import { AdtApiError, AdtNetworkError, AdtSafetyError, isNotFoundError } from '../adt/errors.js';
|
|
17
18
|
import { classifyTextSearchError, mapSapReleaseToAbaplintVersion, probeFeatures } from '../adt/features.js';
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
19
|
+
import { addTileToGroup, createCatalog, createGroup, createTile, deleteCatalog, listCatalogs, listGroups, listTiles, } from '../adt/flp.js';
|
|
20
|
+
import { checkOperation, checkPackage, isOperationAllowed, OperationType } from '../adt/safety.js';
|
|
21
|
+
import { createTransport, deleteTransport, getTransport, getTransportInfo, listTransports, reassignTransport, releaseTransport, releaseTransportRecursive, } from '../adt/transport.js';
|
|
20
22
|
import { getAppInfo } from '../adt/ui5-repository.js';
|
|
21
23
|
import { validateAffHeader } from '../aff/validator.js';
|
|
22
24
|
import { extractCdsElements } from '../context/cds-deps.js';
|
|
@@ -115,34 +117,65 @@ export function looksLikeFieldName(query) {
|
|
|
115
117
|
return false;
|
|
116
118
|
return true;
|
|
117
119
|
}
|
|
118
|
-
/** Classify error type for audit logging */
|
|
119
120
|
/** Format error messages with LLM-friendly remediation hints */
|
|
120
121
|
function formatErrorForLLM(err, message, _tool, args) {
|
|
121
122
|
if (err instanceof AdtApiError) {
|
|
123
|
+
// Append additional SAP messages (line numbers, secondary errors) if available
|
|
124
|
+
const enriched = enrichWithSapDetails(err, message);
|
|
122
125
|
if (err.isNotFound) {
|
|
123
126
|
const name = String(args.name ?? '');
|
|
124
127
|
const type = String(args.type ?? '');
|
|
125
|
-
return `${
|
|
128
|
+
return `${enriched}\n\nHint: Object "${name}" (type ${type}) was not found. Use SAPSearch with query "${name}" to verify the name exists and check the correct type.`;
|
|
126
129
|
}
|
|
127
130
|
if (err.isUnauthorized || err.isForbidden) {
|
|
128
|
-
return `${
|
|
131
|
+
return `${enriched}\n\nHint: Authorization error. Check SAP_CLIENT (default: '100'), SAP_USER, and SAP_PASSWORD. The configured SAP user may lack permissions for this object.`;
|
|
129
132
|
}
|
|
130
133
|
// Transport / corrNr specific hints
|
|
131
134
|
const transportHint = getTransportHint(err);
|
|
132
135
|
if (transportHint) {
|
|
133
|
-
return `${
|
|
136
|
+
return `${enriched}\n\nHint: ${transportHint}`;
|
|
134
137
|
}
|
|
138
|
+
// Server errors (500, 502, 503, etc.)
|
|
139
|
+
if (err.isServerError) {
|
|
140
|
+
return `${enriched}\n\nHint: SAP application server error (${err.statusCode}). This is often transient — wait 10-30 seconds and retry. If the error persists, check SAPDiagnose(action="dumps") for short dumps, or verify the SAP system is responding via SAPRead(type="SYSTEM").`;
|
|
141
|
+
}
|
|
142
|
+
return enriched;
|
|
135
143
|
}
|
|
136
144
|
if (err instanceof AdtNetworkError) {
|
|
137
145
|
return `${message}\n\nHint: Cannot reach the SAP system. This is a connectivity issue, not a usage error.`;
|
|
138
146
|
}
|
|
139
147
|
return message;
|
|
140
148
|
}
|
|
149
|
+
/** Enrich error message with additional SAP XML diagnostic detail (extra messages, properties) */
|
|
150
|
+
function enrichWithSapDetails(err, message) {
|
|
151
|
+
if (!err.responseBody)
|
|
152
|
+
return message;
|
|
153
|
+
const extraMessages = AdtApiError.extractAllMessages(err.responseBody);
|
|
154
|
+
const props = AdtApiError.extractProperties(err.responseBody);
|
|
155
|
+
const parts = [message];
|
|
156
|
+
if (extraMessages.length > 0) {
|
|
157
|
+
parts.push(`\nAdditional detail:\n${extraMessages.map((m) => ` - ${m}`).join('\n')}`);
|
|
158
|
+
}
|
|
159
|
+
// Surface line/column info from properties if present
|
|
160
|
+
const lineInfo = props.LINE || props['T100KEY-NO'];
|
|
161
|
+
if (lineInfo || Object.keys(props).length > 0) {
|
|
162
|
+
const propStr = Object.entries(props)
|
|
163
|
+
.slice(0, 5) // Limit to avoid overwhelming output
|
|
164
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
165
|
+
.join(', ');
|
|
166
|
+
if (propStr)
|
|
167
|
+
parts.push(`Properties: ${propStr}`);
|
|
168
|
+
}
|
|
169
|
+
return parts.join('\n');
|
|
170
|
+
}
|
|
141
171
|
/** Detect transport/corrNr failure signatures and return a remediation hint, or undefined if not transport-related. */
|
|
142
172
|
function getTransportHint(err) {
|
|
143
173
|
const body = (err.responseBody ?? '').toLowerCase();
|
|
144
|
-
|
|
145
|
-
|
|
174
|
+
// Use the clean SAP error message, NOT err.message which includes the URL path.
|
|
175
|
+
// The URL path contains `corrNr=<id>` when a transport IS provided, causing false positives
|
|
176
|
+
// if we check for "corrnr" in the full message string.
|
|
177
|
+
const cleanMsg = AdtApiError.extractCleanMessage(err.responseBody ?? '').toLowerCase();
|
|
178
|
+
const combined = `${cleanMsg} ${body}`;
|
|
146
179
|
// Missing or invalid transport/correction number
|
|
147
180
|
if (combined.includes('correction number') ||
|
|
148
181
|
combined.includes('corrnr') ||
|
|
@@ -468,6 +501,10 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
468
501
|
}
|
|
469
502
|
case 'DDLS': {
|
|
470
503
|
const { source: ddlSource, cacheHit } = await cachedGet('DDLS', name, () => client.getDdls(name));
|
|
504
|
+
if (ddlSource.trim() === '') {
|
|
505
|
+
return textResult(`DDLS ${name} exists in the object directory but has no source code stored. ` +
|
|
506
|
+
`The DDL source may need to be written via SAPWrite(action="create" or "update", type="DDLS", name="${name}", source="...").`);
|
|
507
|
+
}
|
|
471
508
|
if (args.include?.toLowerCase() === 'elements') {
|
|
472
509
|
// Elements extraction is derived from source — no cache indicator
|
|
473
510
|
return textResult(extractCdsElements(ddlSource, name));
|
|
@@ -590,8 +627,16 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
590
627
|
const components = await client.getInstalledComponents();
|
|
591
628
|
return textResult(JSON.stringify(components, null, 2));
|
|
592
629
|
}
|
|
593
|
-
case 'MESSAGES':
|
|
594
|
-
|
|
630
|
+
case 'MESSAGES': {
|
|
631
|
+
try {
|
|
632
|
+
const mcInfo = await client.getMessageClassInfo(name);
|
|
633
|
+
return textResult(JSON.stringify(mcInfo, null, 2));
|
|
634
|
+
}
|
|
635
|
+
catch {
|
|
636
|
+
// Fall back to legacy endpoint if messageclass endpoint unavailable
|
|
637
|
+
return textResult(await client.getMessages(name));
|
|
638
|
+
}
|
|
639
|
+
}
|
|
595
640
|
case 'TEXT_ELEMENTS':
|
|
596
641
|
return textResult(await client.getTextElements(name));
|
|
597
642
|
case 'VARIANTS':
|
|
@@ -631,8 +676,22 @@ async function handleSAPRead(client, args, cachingLayer) {
|
|
|
631
676
|
}
|
|
632
677
|
return textResult(JSON.stringify(info, null, 2));
|
|
633
678
|
}
|
|
679
|
+
case 'INACTIVE_OBJECTS': {
|
|
680
|
+
try {
|
|
681
|
+
const objects = await client.getInactiveObjects();
|
|
682
|
+
return textResult(JSON.stringify({ count: objects.length, objects }, null, 2));
|
|
683
|
+
}
|
|
684
|
+
catch (err) {
|
|
685
|
+
if (isNotFoundError(err)) {
|
|
686
|
+
return textResult('Inactive objects listing is not available on this SAP system ' +
|
|
687
|
+
'(the /sap/bc/adt/activation/inactive endpoint returned 404). ' +
|
|
688
|
+
'Use SAPDiagnose(action="syntax", type="...", name="...") to check specific objects instead.');
|
|
689
|
+
}
|
|
690
|
+
throw err;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
634
693
|
default:
|
|
635
|
-
return errorResult(`Unknown SAPRead type: "${type}". Supported types: PROG, CLAS, INTF, FUNC, FUGR, INCL, DDLS, DDLX, BDEF, SRVD, SRVB, TABL, VIEW, STRU, DOMA, DTEL, TRAN, TABLE_CONTENTS, DEVC, SOBJ, SYSTEM, COMPONENTS, MESSAGES, TEXT_ELEMENTS, VARIANTS, BSP, BSP_DEPLOY, API_STATE. ` +
|
|
694
|
+
return errorResult(`Unknown SAPRead type: "${type}". Supported types: PROG, CLAS, INTF, FUNC, FUGR, INCL, DDLS, DDLX, BDEF, SRVD, SRVB, TABL, VIEW, STRU, DOMA, DTEL, TRAN, TABLE_CONTENTS, DEVC, SOBJ, SYSTEM, COMPONENTS, MESSAGES, TEXT_ELEMENTS, VARIANTS, BSP, BSP_DEPLOY, API_STATE, INACTIVE_OBJECTS. ` +
|
|
636
695
|
'Tip: Map objectType from SAPSearch results by dropping the slash suffix (e.g., DDLS/DF → type="DDLS", CLAS/OC → type="CLAS", PROG/P → type="PROG"). ' +
|
|
637
696
|
'Do not pass a URI — use the "type" and "name" parameters instead.');
|
|
638
697
|
}
|
|
@@ -784,6 +843,187 @@ function buildLintConfigOptions(config, ruleOverrides) {
|
|
|
784
843
|
};
|
|
785
844
|
}
|
|
786
845
|
// ─── Object Creation XML ─────────────────────────────────────────────
|
|
846
|
+
const DOMAIN_V2_CONTENT_TYPE = 'application/vnd.sap.adt.domains.v2+xml; charset=utf-8';
|
|
847
|
+
const DATAELEMENT_V2_CONTENT_TYPE = 'application/vnd.sap.adt.dataelements.v2+xml; charset=utf-8';
|
|
848
|
+
const SERVICEBINDING_V2_CONTENT_TYPE = 'application/vnd.sap.adt.businessservices.servicebinding.v2+xml; charset=utf-8';
|
|
849
|
+
const BDEF_CONTENT_TYPE = 'application/vnd.sap.adt.blues.v1+xml';
|
|
850
|
+
const MESSAGECLASS_CONTENT_TYPE = 'application/vnd.sap.adt.mc.messageclass+xml';
|
|
851
|
+
function isMetadataWriteType(type) {
|
|
852
|
+
return type === 'DOMA' || type === 'DTEL' || type === 'MSAG' || type === 'SRVB';
|
|
853
|
+
}
|
|
854
|
+
/** Types that require a specific vendor content type for creation (not application/*) */
|
|
855
|
+
function needsVendorContentType(type) {
|
|
856
|
+
return type === 'DOMA' || type === 'DTEL' || type === 'BDEF' || type === 'MSAG';
|
|
857
|
+
}
|
|
858
|
+
/** Content type used for create POST */
|
|
859
|
+
function createContentTypeForType(type) {
|
|
860
|
+
// SRVB creation works with wildcard content type; updates use vendor v2 type.
|
|
861
|
+
if (type === 'SRVB')
|
|
862
|
+
return 'application/*';
|
|
863
|
+
return needsVendorContentType(type) ? vendorContentTypeForType(type) : 'application/*';
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Check if a DTEL create has properties that SAP ignores on POST but accepts on PUT.
|
|
867
|
+
* SAP's DTEL POST only stores the shell (name, description, package, typeKind, typeName, dataType, length).
|
|
868
|
+
* Labels, searchHelp, setGetParameter, etc. require a follow-up PUT to take effect.
|
|
869
|
+
*/
|
|
870
|
+
function dtelNeedsPostCreateUpdate(props) {
|
|
871
|
+
return Boolean(props.shortLabel ||
|
|
872
|
+
props.mediumLabel ||
|
|
873
|
+
props.longLabel ||
|
|
874
|
+
props.headingLabel ||
|
|
875
|
+
props.searchHelp ||
|
|
876
|
+
props.searchHelpParameter ||
|
|
877
|
+
props.setGetParameter ||
|
|
878
|
+
props.defaultComponentName ||
|
|
879
|
+
props.changeDocument);
|
|
880
|
+
}
|
|
881
|
+
function vendorContentTypeForType(type) {
|
|
882
|
+
switch (type) {
|
|
883
|
+
case 'DOMA':
|
|
884
|
+
return DOMAIN_V2_CONTENT_TYPE;
|
|
885
|
+
case 'DTEL':
|
|
886
|
+
return DATAELEMENT_V2_CONTENT_TYPE;
|
|
887
|
+
case 'SRVB':
|
|
888
|
+
return SERVICEBINDING_V2_CONTENT_TYPE;
|
|
889
|
+
case 'BDEF':
|
|
890
|
+
return BDEF_CONTENT_TYPE;
|
|
891
|
+
case 'MSAG':
|
|
892
|
+
return MESSAGECLASS_CONTENT_TYPE;
|
|
893
|
+
default:
|
|
894
|
+
// Wildcard lets the SAP server resolve the correct handler.
|
|
895
|
+
// Sending 'application/xml' causes 415 on DDL-based endpoints
|
|
896
|
+
// (DDLS, SRVD, DDLX) whose resource classes reject that literal type.
|
|
897
|
+
return 'application/*';
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
function toBoolean(value) {
|
|
901
|
+
if (typeof value === 'boolean')
|
|
902
|
+
return value;
|
|
903
|
+
if (typeof value === 'number')
|
|
904
|
+
return value !== 0;
|
|
905
|
+
if (typeof value === 'string') {
|
|
906
|
+
const normalized = value.trim().toLowerCase();
|
|
907
|
+
if (normalized === 'true')
|
|
908
|
+
return true;
|
|
909
|
+
if (normalized === 'false')
|
|
910
|
+
return false;
|
|
911
|
+
}
|
|
912
|
+
return undefined;
|
|
913
|
+
}
|
|
914
|
+
function getMetadataWriteProperties(input) {
|
|
915
|
+
const props = {
|
|
916
|
+
dataType: input.dataType,
|
|
917
|
+
length: input.length,
|
|
918
|
+
decimals: input.decimals,
|
|
919
|
+
outputLength: input.outputLength,
|
|
920
|
+
conversionExit: input.conversionExit,
|
|
921
|
+
signExists: input.signExists,
|
|
922
|
+
lowercase: input.lowercase,
|
|
923
|
+
fixedValues: input.fixedValues,
|
|
924
|
+
valueTable: input.valueTable,
|
|
925
|
+
typeKind: input.typeKind,
|
|
926
|
+
typeName: input.typeName,
|
|
927
|
+
domainName: input.domainName,
|
|
928
|
+
shortLabel: input.shortLabel,
|
|
929
|
+
mediumLabel: input.mediumLabel,
|
|
930
|
+
longLabel: input.longLabel,
|
|
931
|
+
headingLabel: input.headingLabel,
|
|
932
|
+
searchHelp: input.searchHelp,
|
|
933
|
+
searchHelpParameter: input.searchHelpParameter,
|
|
934
|
+
setGetParameter: input.setGetParameter,
|
|
935
|
+
defaultComponentName: input.defaultComponentName,
|
|
936
|
+
changeDocument: input.changeDocument,
|
|
937
|
+
messages: input.messages,
|
|
938
|
+
serviceDefinition: input.serviceDefinition,
|
|
939
|
+
bindingType: input.bindingType,
|
|
940
|
+
category: input.category,
|
|
941
|
+
version: input.version,
|
|
942
|
+
};
|
|
943
|
+
return props;
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Fetch existing DDIC metadata and merge with provided properties.
|
|
947
|
+
* This ensures that updating a single field (e.g., shortLabel) doesn't
|
|
948
|
+
* reset other fields (e.g., dataType, typeKind) to defaults, since
|
|
949
|
+
* DDIC updates are full-XML-replace operations.
|
|
950
|
+
*
|
|
951
|
+
* Internal _description and _package fields carry the existing values
|
|
952
|
+
* for the caller to use as fallbacks.
|
|
953
|
+
*/
|
|
954
|
+
function normalizeSrvbCategory(value) {
|
|
955
|
+
if (value === '0' || value === 0 || value === 'UI')
|
|
956
|
+
return '0';
|
|
957
|
+
if (value === '1' || value === 1 || value === 'Web API')
|
|
958
|
+
return '1';
|
|
959
|
+
return undefined;
|
|
960
|
+
}
|
|
961
|
+
async function mergeMetadataWriteProperties(client, type, name, provided) {
|
|
962
|
+
try {
|
|
963
|
+
if (type === 'MSAG') {
|
|
964
|
+
const existing = await client.getMessageClassInfo(name);
|
|
965
|
+
return {
|
|
966
|
+
_description: existing.description,
|
|
967
|
+
_package: existing.package,
|
|
968
|
+
messages: provided.messages ?? existing.messages,
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
if (type === 'DOMA') {
|
|
972
|
+
const existing = await client.getDomain(name);
|
|
973
|
+
return {
|
|
974
|
+
_description: existing.description,
|
|
975
|
+
_package: existing.package,
|
|
976
|
+
dataType: provided.dataType ?? existing.dataType,
|
|
977
|
+
length: provided.length ?? existing.length,
|
|
978
|
+
decimals: provided.decimals ?? existing.decimals,
|
|
979
|
+
outputLength: provided.outputLength ?? existing.outputLength,
|
|
980
|
+
conversionExit: provided.conversionExit ?? existing.conversionExit,
|
|
981
|
+
signExists: provided.signExists ?? existing.signExists,
|
|
982
|
+
lowercase: provided.lowercase ?? existing.lowercase,
|
|
983
|
+
fixedValues: provided.fixedValues ?? existing.fixedValues,
|
|
984
|
+
valueTable: provided.valueTable ?? existing.valueTable,
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
if (type === 'DTEL') {
|
|
988
|
+
const existing = await client.getDataElement(name);
|
|
989
|
+
return {
|
|
990
|
+
_description: existing.description,
|
|
991
|
+
_package: existing.package,
|
|
992
|
+
dataType: provided.dataType ?? existing.dataType,
|
|
993
|
+
length: provided.length ?? existing.length,
|
|
994
|
+
decimals: provided.decimals ?? existing.decimals,
|
|
995
|
+
typeKind: provided.typeKind ?? existing.typeKind,
|
|
996
|
+
typeName: provided.typeName ?? existing.typeName,
|
|
997
|
+
domainName: provided.domainName ?? existing.typeName, // DTEL stores domain in typeName
|
|
998
|
+
shortLabel: provided.shortLabel ?? existing.shortLabel,
|
|
999
|
+
mediumLabel: provided.mediumLabel ?? existing.mediumLabel,
|
|
1000
|
+
longLabel: provided.longLabel ?? existing.longLabel,
|
|
1001
|
+
headingLabel: provided.headingLabel ?? existing.headingLabel,
|
|
1002
|
+
searchHelp: provided.searchHelp ?? existing.searchHelp,
|
|
1003
|
+
searchHelpParameter: provided.searchHelpParameter,
|
|
1004
|
+
setGetParameter: provided.setGetParameter,
|
|
1005
|
+
defaultComponentName: provided.defaultComponentName ?? existing.defaultComponentName,
|
|
1006
|
+
changeDocument: provided.changeDocument,
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
if (type === 'SRVB') {
|
|
1010
|
+
const existingRaw = await client.getSrvb(name);
|
|
1011
|
+
const existing = JSON.parse(existingRaw);
|
|
1012
|
+
return {
|
|
1013
|
+
_description: existing.description,
|
|
1014
|
+
_package: existing.package,
|
|
1015
|
+
serviceDefinition: provided.serviceDefinition ?? existing.serviceDefinition,
|
|
1016
|
+
bindingType: provided.bindingType ?? existing.bindingType,
|
|
1017
|
+
category: provided.category ?? normalizeSrvbCategory(existing.bindingCategory),
|
|
1018
|
+
version: provided.version ?? existing.serviceVersion,
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
catch {
|
|
1023
|
+
// If we can't read existing metadata (e.g., object is new/inactive), fall through
|
|
1024
|
+
}
|
|
1025
|
+
return provided;
|
|
1026
|
+
}
|
|
787
1027
|
/**
|
|
788
1028
|
* Build the type-specific XML body for ADT object creation.
|
|
789
1029
|
*
|
|
@@ -791,7 +1031,101 @@ function buildLintConfigOptions(config, ruleOverrides) {
|
|
|
791
1031
|
* Using a generic body (e.g. adtcore:objectReferences) returns 400:
|
|
792
1032
|
* "System expected the element '{http://www.sap.com/adt/programs/programs}abapProgram'"
|
|
793
1033
|
*/
|
|
794
|
-
|
|
1034
|
+
// ─── CDS Pre-Write Validation ──────────────────────────────────────
|
|
1035
|
+
/** Common CDS reserved/function keywords that cause silent DDL save failures when used as field names */
|
|
1036
|
+
const CDS_RESERVED_KEYWORDS = new Set([
|
|
1037
|
+
'position',
|
|
1038
|
+
'value',
|
|
1039
|
+
'type',
|
|
1040
|
+
'data',
|
|
1041
|
+
'timestamp',
|
|
1042
|
+
'language',
|
|
1043
|
+
'text',
|
|
1044
|
+
'source',
|
|
1045
|
+
'target',
|
|
1046
|
+
'name',
|
|
1047
|
+
'description',
|
|
1048
|
+
'concat',
|
|
1049
|
+
'replace',
|
|
1050
|
+
'substring',
|
|
1051
|
+
'length',
|
|
1052
|
+
'left',
|
|
1053
|
+
'right',
|
|
1054
|
+
'round',
|
|
1055
|
+
'abs',
|
|
1056
|
+
'floor',
|
|
1057
|
+
'ceiling',
|
|
1058
|
+
'division',
|
|
1059
|
+
'mod',
|
|
1060
|
+
'case',
|
|
1061
|
+
'when',
|
|
1062
|
+
'then',
|
|
1063
|
+
'else',
|
|
1064
|
+
'end',
|
|
1065
|
+
'cast',
|
|
1066
|
+
'coalesce',
|
|
1067
|
+
'uuid',
|
|
1068
|
+
]);
|
|
1069
|
+
/**
|
|
1070
|
+
* Guard CDS syntax against known version-dependent features.
|
|
1071
|
+
* Returns an error result if the source uses unsupported syntax, or undefined to proceed.
|
|
1072
|
+
* Best-effort: if cachedFeatures is not available (no probe yet), always proceeds.
|
|
1073
|
+
*/
|
|
1074
|
+
function guardCdsSyntax(type, source, features) {
|
|
1075
|
+
if (type !== 'DDLS' || !source)
|
|
1076
|
+
return undefined;
|
|
1077
|
+
// Guard: "define table entity" requires ABAP Cloud (BTP) or SAP_BASIS >= 757
|
|
1078
|
+
if (/\bdefine\s+table\s+(entity|function)\b/i.test(source)) {
|
|
1079
|
+
const release = features?.abapRelease;
|
|
1080
|
+
const isBtp = features?.systemType === 'btp';
|
|
1081
|
+
if (!isBtp && release) {
|
|
1082
|
+
const releaseNum = Number.parseInt(release.replace(/\D/g, ''), 10);
|
|
1083
|
+
if (releaseNum > 0 && releaseNum < 757) {
|
|
1084
|
+
return errorResult(`"define table entity" syntax requires ABAP Cloud (BTP) or S/4HANA on-premise with SAP_BASIS >= 757. ` +
|
|
1085
|
+
`This system reports SAP_BASIS ${release}. ` +
|
|
1086
|
+
`Use DDIC transparent tables (SAPWrite type="TABL" or SE11) + CDS view entities ("define [root] view entity") instead.`);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
// Advisory: warn about CDS reserved keywords used as field names
|
|
1091
|
+
const keywordWarning = warnCdsReservedKeywords(source);
|
|
1092
|
+
if (keywordWarning) {
|
|
1093
|
+
// Non-blocking — return undefined to proceed, but the warning will be
|
|
1094
|
+
// appended to the success message by the caller if needed.
|
|
1095
|
+
// For now we return it as an advisory error only when the keyword is
|
|
1096
|
+
// highly likely to cause issues (position is the most common).
|
|
1097
|
+
// We don't block the write — just append it as advisory context.
|
|
1098
|
+
}
|
|
1099
|
+
return undefined;
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Detect CDS reserved keywords used as field names in DDL source.
|
|
1103
|
+
* Returns a warning string listing suspicious field names, or undefined if none found.
|
|
1104
|
+
*/
|
|
1105
|
+
export function warnCdsReservedKeywords(source) {
|
|
1106
|
+
// Extract field-name-like tokens: lines inside { } that define fields
|
|
1107
|
+
// Pattern: whitespace + identifier + colon (field definitions)
|
|
1108
|
+
const fieldNames = [];
|
|
1109
|
+
const braceStart = source.indexOf('{');
|
|
1110
|
+
const braceEnd = source.lastIndexOf('}');
|
|
1111
|
+
if (braceStart === -1 || braceEnd === -1)
|
|
1112
|
+
return undefined;
|
|
1113
|
+
const body = source.slice(braceStart + 1, braceEnd);
|
|
1114
|
+
// Match field definitions: leading whitespace, optional "key", then identifier before ":"
|
|
1115
|
+
const fieldPattern = /^\s*(?:key\s+)?(\w+)\s*:/gim;
|
|
1116
|
+
let match;
|
|
1117
|
+
while ((match = fieldPattern.exec(body)) !== null) {
|
|
1118
|
+
const fieldName = match[1]?.toLowerCase();
|
|
1119
|
+
if (fieldName && CDS_RESERVED_KEYWORDS.has(fieldName)) {
|
|
1120
|
+
fieldNames.push(match[1]);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
if (fieldNames.length === 0)
|
|
1124
|
+
return undefined;
|
|
1125
|
+
return (`Warning: field name(s) ${fieldNames.map((f) => `'${f}'`).join(', ')} may be CDS reserved keywords. ` +
|
|
1126
|
+
`If the DDL save fails with a generic syntax error, rename them (e.g., 'position' → 'playing_position', 'type' → 'obj_type').`);
|
|
1127
|
+
}
|
|
1128
|
+
export function buildCreateXml(type, name, pkg, description, properties) {
|
|
795
1129
|
switch (type) {
|
|
796
1130
|
case 'PROG':
|
|
797
1131
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -850,45 +1184,145 @@ export function buildCreateXml(type, name, pkg, description) {
|
|
|
850
1184
|
adtcore:type="DDLS/DF"
|
|
851
1185
|
adtcore:masterLanguage="EN"
|
|
852
1186
|
adtcore:masterSystem="H00"
|
|
853
|
-
|
|
1187
|
+
adtcore:responsible="DEVELOPER">
|
|
854
1188
|
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
855
1189
|
</ddl:ddlSource>`;
|
|
1190
|
+
case 'TABL':
|
|
1191
|
+
// TABL creation also uses SAP's "blue" framework envelope, then source is written via /source/main.
|
|
1192
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
1193
|
+
<blue:blueSource xmlns:blue="http://www.sap.com/wbobj/blue"
|
|
1194
|
+
xmlns:adtcore="http://www.sap.com/adt/core"
|
|
1195
|
+
adtcore:description="${escapeXml(description)}"
|
|
1196
|
+
adtcore:name="${escapeXml(name)}"
|
|
1197
|
+
adtcore:type="TABL/DT"
|
|
1198
|
+
adtcore:masterLanguage="EN"
|
|
1199
|
+
adtcore:masterSystem="H00"
|
|
1200
|
+
adtcore:responsible="DEVELOPER">
|
|
1201
|
+
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
1202
|
+
</blue:blueSource>`;
|
|
856
1203
|
case 'BDEF':
|
|
1204
|
+
// BDEF uses SAP's "blue" framework — blue:blueSource with http://www.sap.com/wbobj/blue namespace.
|
|
1205
|
+
// Confirmed by vibing-steampunk (Go) and fr0ster (TypeScript) reference implementations.
|
|
857
1206
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
858
|
-
<
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
1207
|
+
<blue:blueSource xmlns:blue="http://www.sap.com/wbobj/blue"
|
|
1208
|
+
xmlns:adtcore="http://www.sap.com/adt/core"
|
|
1209
|
+
adtcore:description="${escapeXml(description)}"
|
|
1210
|
+
adtcore:name="${escapeXml(name)}"
|
|
1211
|
+
adtcore:type="BDEF/BDO"
|
|
1212
|
+
adtcore:masterLanguage="EN"
|
|
1213
|
+
adtcore:masterSystem="H00"
|
|
1214
|
+
adtcore:responsible="DEVELOPER">
|
|
866
1215
|
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
867
|
-
</
|
|
1216
|
+
</blue:blueSource>`;
|
|
868
1217
|
case 'SRVD':
|
|
869
1218
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
870
|
-
<srvd:srvdSource xmlns:srvd="http://www.sap.com/adt/ddic/
|
|
1219
|
+
<srvd:srvdSource xmlns:srvd="http://www.sap.com/adt/ddic/srvdsources"
|
|
871
1220
|
xmlns:adtcore="http://www.sap.com/adt/core"
|
|
872
1221
|
adtcore:description="${escapeXml(description)}"
|
|
873
1222
|
adtcore:name="${escapeXml(name)}"
|
|
874
1223
|
adtcore:type="SRVD/SRV"
|
|
875
1224
|
adtcore:masterLanguage="EN"
|
|
876
1225
|
adtcore:masterSystem="H00"
|
|
877
|
-
adtcore:responsible="DEVELOPER"
|
|
1226
|
+
adtcore:responsible="DEVELOPER"
|
|
1227
|
+
srvd:srvdSourceType="S">
|
|
878
1228
|
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
879
1229
|
</srvd:srvdSource>`;
|
|
1230
|
+
case 'SRVB': {
|
|
1231
|
+
const serviceDefinition = String(properties?.serviceDefinition ?? '').trim();
|
|
1232
|
+
if (!serviceDefinition) {
|
|
1233
|
+
throw new Error('SRVB create/update requires "serviceDefinition" (referenced SRVD name).');
|
|
1234
|
+
}
|
|
1235
|
+
const categoryRaw = properties?.category;
|
|
1236
|
+
const category = categoryRaw === '1' || categoryRaw === 1 ? '1' : '0';
|
|
1237
|
+
const params = {
|
|
1238
|
+
name,
|
|
1239
|
+
description,
|
|
1240
|
+
package: pkg,
|
|
1241
|
+
serviceDefinition,
|
|
1242
|
+
bindingType: properties?.bindingType ? String(properties.bindingType) : undefined,
|
|
1243
|
+
category,
|
|
1244
|
+
version: properties?.version ? String(properties.version) : undefined,
|
|
1245
|
+
};
|
|
1246
|
+
return buildServiceBindingXml(params);
|
|
1247
|
+
}
|
|
880
1248
|
case 'DDLX':
|
|
881
1249
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
882
|
-
<ddlx:ddlxSource xmlns:ddlx="http://www.sap.com/adt/ddic/
|
|
1250
|
+
<ddlx:ddlxSource xmlns:ddlx="http://www.sap.com/adt/ddic/ddlxsources"
|
|
883
1251
|
xmlns:adtcore="http://www.sap.com/adt/core"
|
|
884
1252
|
adtcore:description="${escapeXml(description)}"
|
|
885
1253
|
adtcore:name="${escapeXml(name)}"
|
|
886
1254
|
adtcore:type="DDLX/EX"
|
|
887
1255
|
adtcore:masterLanguage="EN"
|
|
888
1256
|
adtcore:masterSystem="H00"
|
|
889
|
-
|
|
1257
|
+
adtcore:responsible="DEVELOPER">
|
|
890
1258
|
<adtcore:packageRef adtcore:name="${escapeXml(pkg)}"/>
|
|
891
1259
|
</ddlx:ddlxSource>`;
|
|
1260
|
+
case 'DOMA': {
|
|
1261
|
+
const fixedValuesRaw = Array.isArray(properties?.fixedValues) ? properties.fixedValues : [];
|
|
1262
|
+
const fixedValues = fixedValuesRaw
|
|
1263
|
+
.filter((value) => typeof value === 'object' && value !== null)
|
|
1264
|
+
.map((value) => ({
|
|
1265
|
+
low: String(value.low ?? ''),
|
|
1266
|
+
high: value.high === undefined ? undefined : String(value.high),
|
|
1267
|
+
description: value.description === undefined ? undefined : String(value.description),
|
|
1268
|
+
}));
|
|
1269
|
+
const params = {
|
|
1270
|
+
name,
|
|
1271
|
+
description,
|
|
1272
|
+
package: pkg,
|
|
1273
|
+
dataType: String(properties?.dataType ?? 'CHAR'),
|
|
1274
|
+
length: properties?.length ?? 0,
|
|
1275
|
+
decimals: properties?.decimals,
|
|
1276
|
+
outputLength: properties?.outputLength,
|
|
1277
|
+
conversionExit: properties?.conversionExit ? String(properties.conversionExit) : undefined,
|
|
1278
|
+
signExists: toBoolean(properties?.signExists),
|
|
1279
|
+
lowercase: toBoolean(properties?.lowercase),
|
|
1280
|
+
fixedValues,
|
|
1281
|
+
valueTable: properties?.valueTable ? String(properties.valueTable) : undefined,
|
|
1282
|
+
};
|
|
1283
|
+
return buildDomainXml(params);
|
|
1284
|
+
}
|
|
1285
|
+
case 'DTEL': {
|
|
1286
|
+
const typeKindRaw = String(properties?.typeKind ?? '');
|
|
1287
|
+
const typeKind = typeKindRaw === 'domain' || typeKindRaw === 'predefinedAbapType' ? typeKindRaw : undefined;
|
|
1288
|
+
const params = {
|
|
1289
|
+
name,
|
|
1290
|
+
description,
|
|
1291
|
+
package: pkg,
|
|
1292
|
+
typeKind,
|
|
1293
|
+
typeName: properties?.typeName ? String(properties.typeName) : undefined,
|
|
1294
|
+
domainName: properties?.domainName ? String(properties.domainName) : undefined,
|
|
1295
|
+
dataType: properties?.dataType ? String(properties.dataType) : undefined,
|
|
1296
|
+
length: properties?.length,
|
|
1297
|
+
decimals: properties?.decimals,
|
|
1298
|
+
shortLabel: properties?.shortLabel ? String(properties.shortLabel) : undefined,
|
|
1299
|
+
mediumLabel: properties?.mediumLabel ? String(properties.mediumLabel) : undefined,
|
|
1300
|
+
longLabel: properties?.longLabel ? String(properties.longLabel) : undefined,
|
|
1301
|
+
headingLabel: properties?.headingLabel ? String(properties.headingLabel) : undefined,
|
|
1302
|
+
searchHelp: properties?.searchHelp ? String(properties.searchHelp) : undefined,
|
|
1303
|
+
searchHelpParameter: properties?.searchHelpParameter ? String(properties.searchHelpParameter) : undefined,
|
|
1304
|
+
setGetParameter: properties?.setGetParameter ? String(properties.setGetParameter) : undefined,
|
|
1305
|
+
defaultComponentName: properties?.defaultComponentName ? String(properties.defaultComponentName) : undefined,
|
|
1306
|
+
changeDocument: toBoolean(properties?.changeDocument),
|
|
1307
|
+
};
|
|
1308
|
+
return buildDataElementXml(params);
|
|
1309
|
+
}
|
|
1310
|
+
case 'MSAG': {
|
|
1311
|
+
const messagesRaw = Array.isArray(properties?.messages) ? properties.messages : [];
|
|
1312
|
+
const messages = messagesRaw
|
|
1313
|
+
.filter((m) => typeof m === 'object' && m !== null)
|
|
1314
|
+
.map((m) => ({
|
|
1315
|
+
number: String(m.number ?? ''),
|
|
1316
|
+
shortText: String(m.shortText ?? ''),
|
|
1317
|
+
}));
|
|
1318
|
+
const params = {
|
|
1319
|
+
name,
|
|
1320
|
+
description,
|
|
1321
|
+
package: pkg,
|
|
1322
|
+
messages: messages.length > 0 ? messages : undefined,
|
|
1323
|
+
};
|
|
1324
|
+
return buildMessageClassXml(params);
|
|
1325
|
+
}
|
|
892
1326
|
default:
|
|
893
1327
|
// Fallback — generic objectReferences using the correct URL for the type
|
|
894
1328
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -940,6 +1374,10 @@ function objectBasePath(type) {
|
|
|
940
1374
|
return '/sap/bc/adt/ddic/domains/';
|
|
941
1375
|
case 'DTEL':
|
|
942
1376
|
return '/sap/bc/adt/ddic/dataelements/';
|
|
1377
|
+
case 'MSAG':
|
|
1378
|
+
return '/sap/bc/adt/messageclass/';
|
|
1379
|
+
case 'DEVC':
|
|
1380
|
+
return '/sap/bc/adt/packages/';
|
|
943
1381
|
case 'TRAN':
|
|
944
1382
|
return '/sap/bc/adt/vit/wb/object_type/trant/object_name/';
|
|
945
1383
|
default:
|
|
@@ -985,8 +1423,36 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
985
1423
|
}
|
|
986
1424
|
const objectUrl = objectUrlForType(type, name);
|
|
987
1425
|
const srcUrl = sourceUrlForType(type, name);
|
|
1426
|
+
// Helper: enforce allowedPackages for existing objects (update/delete/edit_method).
|
|
1427
|
+
// Only fetches metadata when package restrictions are configured — no extra HTTP call otherwise.
|
|
1428
|
+
async function enforcePackageForExistingObject() {
|
|
1429
|
+
if (client.safety.allowedPackages.length === 0)
|
|
1430
|
+
return undefined;
|
|
1431
|
+
const pkg = await client.resolveObjectPackage(objectUrl);
|
|
1432
|
+
if (pkg)
|
|
1433
|
+
checkPackage(client.safety, pkg);
|
|
1434
|
+
return pkg;
|
|
1435
|
+
}
|
|
988
1436
|
switch (action) {
|
|
989
1437
|
case 'update': {
|
|
1438
|
+
const existingPackage = await enforcePackageForExistingObject();
|
|
1439
|
+
if (isMetadataWriteType(type)) {
|
|
1440
|
+
// Metadata updates are full-XML-replace — we must fetch existing metadata
|
|
1441
|
+
// and merge with provided fields so omitted fields keep their current values.
|
|
1442
|
+
// Without this, updating just labels would reset dataType/typeKind to defaults.
|
|
1443
|
+
const metadataProps = getMetadataWriteProperties(args);
|
|
1444
|
+
const mergedProps = await mergeMetadataWriteProperties(client, type, name, metadataProps);
|
|
1445
|
+
const description = String(args.description ?? mergedProps._description ?? name);
|
|
1446
|
+
const pkg = String(args.package ?? existingPackage ?? mergedProps._package ?? '$TMP');
|
|
1447
|
+
const body = buildCreateXml(type, name, pkg, description, mergedProps);
|
|
1448
|
+
await safeUpdateObject(client.http, client.safety, objectUrl, body, vendorContentTypeForType(type), transport);
|
|
1449
|
+
cachingLayer?.invalidate(type, name);
|
|
1450
|
+
return textResult(`Successfully updated ${type} ${name}.`);
|
|
1451
|
+
}
|
|
1452
|
+
// CDS pre-write validation: reject unsupported syntax early
|
|
1453
|
+
const cdsGuardUpdate = guardCdsSyntax(type, source, cachedFeatures);
|
|
1454
|
+
if (cdsGuardUpdate)
|
|
1455
|
+
return cdsGuardUpdate;
|
|
990
1456
|
// Pre-write lint validation
|
|
991
1457
|
const lintWarnings = runPreWriteLint(source, type, name, config);
|
|
992
1458
|
if (lintWarnings.blocked)
|
|
@@ -1000,6 +1466,44 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
1000
1466
|
const pkg = String(args.package ?? '$TMP');
|
|
1001
1467
|
checkPackage(client.safety, pkg);
|
|
1002
1468
|
const description = String(args.description ?? name);
|
|
1469
|
+
// Pre-flight: check transport requirements for non-$TMP packages when no transport provided.
|
|
1470
|
+
// SAP requires a transport number for objects in transportable packages.
|
|
1471
|
+
// Instead of letting SAP return a cryptic error, we detect this early and return
|
|
1472
|
+
// an actionable error message guiding the LLM to use SAPTransport first.
|
|
1473
|
+
let effectiveTransport = transport;
|
|
1474
|
+
if (!transport && pkg.toUpperCase() !== '$TMP') {
|
|
1475
|
+
try {
|
|
1476
|
+
const transportInfo = await getTransportInfo(client.http, client.safety, objectUrl, pkg, 'I');
|
|
1477
|
+
if (transportInfo.lockedTransport) {
|
|
1478
|
+
// Object is already locked in a transport — use it automatically
|
|
1479
|
+
effectiveTransport = transportInfo.lockedTransport;
|
|
1480
|
+
}
|
|
1481
|
+
else if (!transportInfo.isLocal && transportInfo.recording) {
|
|
1482
|
+
// Transport IS required but none provided — return guidance
|
|
1483
|
+
const existingList = transportInfo.existingTransports.length > 0
|
|
1484
|
+
? `\n\nExisting transports for this package:\n${transportInfo.existingTransports
|
|
1485
|
+
.slice(0, 10)
|
|
1486
|
+
.map((t) => ` - ${t.id}: ${t.description} (${t.owner})`)
|
|
1487
|
+
.join('\n')}`
|
|
1488
|
+
: '';
|
|
1489
|
+
return errorResult(`Package "${pkg}" requires a transport number for object creation, but none was provided.\n\n` +
|
|
1490
|
+
`To fix this, either:\n` +
|
|
1491
|
+
`1. Use SAPTransport(action="list") to find an existing modifiable transport\n` +
|
|
1492
|
+
`2. Use SAPTransport(action="create", description="...") to create a new one\n` +
|
|
1493
|
+
`3. Then retry SAPWrite(action="create", ..., transport="<transport_id>")` +
|
|
1494
|
+
existingList);
|
|
1495
|
+
}
|
|
1496
|
+
// isLocal=true or recording=false → no transport needed, proceed without one
|
|
1497
|
+
}
|
|
1498
|
+
catch {
|
|
1499
|
+
// If transportInfo check fails (older system, permissions, etc.), proceed without it.
|
|
1500
|
+
// SAP will return its own error if a transport is actually needed.
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
// CDS pre-write validation: reject unsupported syntax early
|
|
1504
|
+
const cdsGuard = guardCdsSyntax(type, source, cachedFeatures);
|
|
1505
|
+
if (cdsGuard)
|
|
1506
|
+
return cdsGuard;
|
|
1003
1507
|
// AFF header validation (if schema available for this type)
|
|
1004
1508
|
const affResult = validateAffHeader(type, { description, originalLanguage: 'en' });
|
|
1005
1509
|
if (!affResult.valid) {
|
|
@@ -1008,10 +1512,52 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
1008
1512
|
// Build type-specific creation XML body.
|
|
1009
1513
|
// SAP ADT requires the root element to match the object type —
|
|
1010
1514
|
// a generic objectReferences body returns 400 "System expected the element ...".
|
|
1011
|
-
const
|
|
1515
|
+
const metadataProperties = getMetadataWriteProperties(args);
|
|
1516
|
+
const body = buildCreateXml(type, name, pkg, description, metadataProperties);
|
|
1012
1517
|
// Step 1: Create the object (metadata only)
|
|
1013
1518
|
const createUrl = objectUrl.replace(/\/[^/]+$/, ''); // parent collection URL
|
|
1014
|
-
|
|
1519
|
+
// DOMA/DTEL/BDEF require vendor-specific content types; all other types use
|
|
1520
|
+
// 'application/*' — the wildcard lets the SAP server resolve the correct
|
|
1521
|
+
// handler (matching how ADT Eclipse and abap-adt-api send requests).
|
|
1522
|
+
const contentType = createContentTypeForType(type);
|
|
1523
|
+
const result = await createObject(client.http, client.safety, createUrl, body, contentType, effectiveTransport);
|
|
1524
|
+
if (isMetadataWriteType(type)) {
|
|
1525
|
+
// SAP's DTEL POST ignores labels, searchHelp, etc. — they require a follow-up PUT.
|
|
1526
|
+
// Use withStatefulSession directly (not safeUpdateObject) to keep the lock cycle
|
|
1527
|
+
// on the main client's session, avoiding lock contention with subsequent operations.
|
|
1528
|
+
if (type === 'DTEL' && dtelNeedsPostCreateUpdate(metadataProperties)) {
|
|
1529
|
+
const ct = vendorContentTypeForType(type);
|
|
1530
|
+
await client.http.withStatefulSession(async (session) => {
|
|
1531
|
+
const lock = await lockObject(session, client.safety, objectUrl);
|
|
1532
|
+
const lockTransport = effectiveTransport ?? (lock.corrNr || undefined);
|
|
1533
|
+
try {
|
|
1534
|
+
await updateObject(session, client.safety, objectUrl, body, lock.lockHandle, ct, lockTransport);
|
|
1535
|
+
}
|
|
1536
|
+
finally {
|
|
1537
|
+
await unlockObject(session, objectUrl, lock.lockHandle);
|
|
1538
|
+
}
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
// MSAG: POST creates empty container — follow-up PUT to write messages
|
|
1542
|
+
if (type === 'MSAG' && Array.isArray(metadataProperties.messages) && metadataProperties.messages.length > 0) {
|
|
1543
|
+
const ct = vendorContentTypeForType(type);
|
|
1544
|
+
await client.http.withStatefulSession(async (session) => {
|
|
1545
|
+
const lock = await lockObject(session, client.safety, objectUrl);
|
|
1546
|
+
const lockTransport = effectiveTransport ?? (lock.corrNr || undefined);
|
|
1547
|
+
try {
|
|
1548
|
+
await updateObject(session, client.safety, objectUrl, body, lock.lockHandle, ct, lockTransport);
|
|
1549
|
+
}
|
|
1550
|
+
finally {
|
|
1551
|
+
await unlockObject(session, objectUrl, lock.lockHandle);
|
|
1552
|
+
}
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
cachingLayer?.invalidate(type, name);
|
|
1556
|
+
const followUpHint = type === 'SRVB'
|
|
1557
|
+
? `\n\nNext steps:\n1. SAPActivate(type="SRVB", name="${name}")\n2. SAPActivate(action="publish_srvb", name="${name}")`
|
|
1558
|
+
: '';
|
|
1559
|
+
return textResult(`Created ${type} ${name} in package ${pkg}.\n${result}${followUpHint}`);
|
|
1560
|
+
}
|
|
1015
1561
|
// Step 2: Write source code if provided
|
|
1016
1562
|
if (source) {
|
|
1017
1563
|
// Pre-write lint validation
|
|
@@ -1019,7 +1565,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
1019
1565
|
if (lintWarnings.blocked) {
|
|
1020
1566
|
return textResult(`Created ${type} ${name} in package ${pkg}, but source was rejected by lint:\n${lintWarnings.result.content[0].text}`);
|
|
1021
1567
|
}
|
|
1022
|
-
await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, source,
|
|
1568
|
+
await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, source, effectiveTransport);
|
|
1023
1569
|
cachingLayer?.invalidate(type, name);
|
|
1024
1570
|
const msg = `Created ${type} ${name} in package ${pkg} and wrote source code.`;
|
|
1025
1571
|
return lintWarnings.warnings ? textResult(`${msg}\n\n${lintWarnings.warnings}`) : textResult(msg);
|
|
@@ -1034,6 +1580,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
1034
1580
|
return errorResult('"source" (new method body) is required for edit_method action.');
|
|
1035
1581
|
if (type !== 'CLAS')
|
|
1036
1582
|
return errorResult('edit_method is only supported for type=CLAS.');
|
|
1583
|
+
await enforcePackageForExistingObject();
|
|
1037
1584
|
// Fetch current full source (use cache if available)
|
|
1038
1585
|
const currentSource = cachingLayer
|
|
1039
1586
|
? (await cachingLayer.getSource('CLAS', name, () => client.getClass(name))).source
|
|
@@ -1058,6 +1605,7 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
1058
1605
|
return lintWarnings.warnings ? textResult(`${msg}\n\n${lintWarnings.warnings}`) : textResult(msg);
|
|
1059
1606
|
}
|
|
1060
1607
|
case 'delete': {
|
|
1608
|
+
await enforcePackageForExistingObject();
|
|
1061
1609
|
// Lock, delete, unlock pattern — auto-propagate lock corrNr if no explicit transport
|
|
1062
1610
|
await client.http.withStatefulSession(async (session) => {
|
|
1063
1611
|
const lock = await lockObject(session, client.safety, objectUrl);
|
|
@@ -1085,10 +1633,41 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
1085
1633
|
const pkg = String(args.package ?? '$TMP');
|
|
1086
1634
|
// Check package is allowed before starting any creates
|
|
1087
1635
|
checkPackage(client.safety, pkg);
|
|
1636
|
+
// Pre-flight transport check for batch_create (same logic as single create)
|
|
1637
|
+
let batchTransport = transport;
|
|
1638
|
+
if (!transport && pkg.toUpperCase() !== '$TMP') {
|
|
1639
|
+
try {
|
|
1640
|
+
// Use first object's URL for the transport check
|
|
1641
|
+
const firstObj = objects[0];
|
|
1642
|
+
const firstUrl = objectUrlForType(String(firstObj.type ?? ''), String(firstObj.name ?? ''));
|
|
1643
|
+
const transportInfo = await getTransportInfo(client.http, client.safety, firstUrl, pkg, 'I');
|
|
1644
|
+
if (transportInfo.lockedTransport) {
|
|
1645
|
+
batchTransport = transportInfo.lockedTransport;
|
|
1646
|
+
}
|
|
1647
|
+
else if (!transportInfo.isLocal && transportInfo.recording) {
|
|
1648
|
+
const existingList = transportInfo.existingTransports.length > 0
|
|
1649
|
+
? `\n\nExisting transports for this package:\n${transportInfo.existingTransports
|
|
1650
|
+
.slice(0, 10)
|
|
1651
|
+
.map((t) => ` - ${t.id}: ${t.description} (${t.owner})`)
|
|
1652
|
+
.join('\n')}`
|
|
1653
|
+
: '';
|
|
1654
|
+
return errorResult(`Package "${pkg}" requires a transport number for object creation, but none was provided.\n\n` +
|
|
1655
|
+
`To fix this, either:\n` +
|
|
1656
|
+
`1. Use SAPTransport(action="list") to find an existing modifiable transport\n` +
|
|
1657
|
+
`2. Use SAPTransport(action="create", description="...") to create a new one\n` +
|
|
1658
|
+
`3. Then retry SAPWrite(action="batch_create", ..., transport="<transport_id>")` +
|
|
1659
|
+
existingList);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
catch {
|
|
1663
|
+
// If transportInfo check fails, proceed — SAP will return its own error if needed.
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1088
1666
|
const results = [];
|
|
1089
1667
|
for (const obj of objects) {
|
|
1090
1668
|
const objType = String(obj.type ?? '');
|
|
1091
1669
|
const objName = String(obj.name ?? '');
|
|
1670
|
+
const metadataObject = isMetadataWriteType(objType);
|
|
1092
1671
|
const objSource = obj.source ? String(obj.source) : undefined;
|
|
1093
1672
|
const objDescription = String(obj.description ?? objName);
|
|
1094
1673
|
// AFF header validation per object (if schema available)
|
|
@@ -1103,8 +1682,9 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
1103
1682
|
break;
|
|
1104
1683
|
}
|
|
1105
1684
|
try {
|
|
1106
|
-
// Pre-validate source with lint BEFORE creating the object to avoid orphaned objects
|
|
1107
|
-
|
|
1685
|
+
// Pre-validate source with lint BEFORE creating the object to avoid orphaned objects.
|
|
1686
|
+
// Metadata objects (DOMA/DTEL) are XML-only and intentionally skip source lint.
|
|
1687
|
+
if (!metadataObject && objSource) {
|
|
1108
1688
|
const lintWarnings = runPreWriteLint(objSource, objType, objName, config);
|
|
1109
1689
|
if (lintWarnings.blocked) {
|
|
1110
1690
|
results.push({
|
|
@@ -1119,12 +1699,27 @@ async function handleSAPWrite(client, args, config, cachingLayer) {
|
|
|
1119
1699
|
// Step 1: Create the object
|
|
1120
1700
|
const objUrl = objectUrlForType(objType, objName);
|
|
1121
1701
|
const createUrl = objUrl.replace(/\/[^/]+$/, '');
|
|
1122
|
-
const
|
|
1123
|
-
|
|
1702
|
+
const objMetadataProps = getMetadataWriteProperties(obj);
|
|
1703
|
+
const body = buildCreateXml(objType, objName, pkg, objDescription, objMetadataProps);
|
|
1704
|
+
const contentType = createContentTypeForType(objType);
|
|
1705
|
+
await createObject(client.http, client.safety, createUrl, body, contentType, batchTransport);
|
|
1706
|
+
// Step 1b: DTEL POST ignores labels — follow up with PUT on main session
|
|
1707
|
+
if (objType === 'DTEL' && dtelNeedsPostCreateUpdate(objMetadataProps)) {
|
|
1708
|
+
await client.http.withStatefulSession(async (session) => {
|
|
1709
|
+
const lock = await lockObject(session, client.safety, objUrl);
|
|
1710
|
+
const lockTransport = batchTransport ?? (lock.corrNr || undefined);
|
|
1711
|
+
try {
|
|
1712
|
+
await updateObject(session, client.safety, objUrl, body, lock.lockHandle, contentType, lockTransport);
|
|
1713
|
+
}
|
|
1714
|
+
finally {
|
|
1715
|
+
await unlockObject(session, objUrl, lock.lockHandle);
|
|
1716
|
+
}
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1124
1719
|
// Step 2: Write source if provided
|
|
1125
|
-
if (objSource) {
|
|
1720
|
+
if (!metadataObject && objSource) {
|
|
1126
1721
|
const srcUrl = sourceUrlForType(objType, objName);
|
|
1127
|
-
await safeUpdateSource(client.http, client.safety, objUrl, srcUrl, objSource,
|
|
1722
|
+
await safeUpdateSource(client.http, client.safety, objUrl, srcUrl, objSource, batchTransport);
|
|
1128
1723
|
}
|
|
1129
1724
|
// Step 3: Activate the object
|
|
1130
1725
|
const activationResult = await activate(client.http, client.safety, objUrl);
|
|
@@ -1300,26 +1895,56 @@ async function handleSAPActivate(client, args) {
|
|
|
1300
1895
|
}
|
|
1301
1896
|
// Batch activation: multiple objects at once (for RAP stacks etc.)
|
|
1302
1897
|
const type = String(args.type ?? '');
|
|
1898
|
+
const preaudit = args.preaudit !== undefined ? Boolean(args.preaudit) : undefined;
|
|
1899
|
+
const activateOpts = preaudit !== undefined ? { preaudit } : undefined;
|
|
1303
1900
|
if (args.objects && Array.isArray(args.objects)) {
|
|
1304
1901
|
const objects = args.objects.map((o) => {
|
|
1305
1902
|
const objType = String(o.type ?? type);
|
|
1306
1903
|
const objName = String(o.name ?? '');
|
|
1307
1904
|
return { url: objectUrlForType(objType, objName), name: objName };
|
|
1308
1905
|
});
|
|
1309
|
-
const result = await activateBatch(client.http, client.safety, objects);
|
|
1906
|
+
const result = await activateBatch(client.http, client.safety, objects, activateOpts);
|
|
1310
1907
|
const names = objects.map((o) => o.name).join(', ');
|
|
1311
1908
|
if (result.success) {
|
|
1312
|
-
return textResult(`Successfully activated ${objects.length} objects: ${names}.${result
|
|
1909
|
+
return textResult(`Successfully activated ${objects.length} objects: ${names}.${formatActivationMessages(result)}`);
|
|
1313
1910
|
}
|
|
1314
|
-
return errorResult(`Batch activation failed for: ${names}.\
|
|
1911
|
+
return errorResult(`Batch activation failed for: ${names}.\n${formatActivationMessages(result)}`);
|
|
1315
1912
|
}
|
|
1316
1913
|
// Single activation (existing behavior)
|
|
1317
1914
|
const objectUrl = objectUrlForType(type, name);
|
|
1318
|
-
const result = await activate(client.http, client.safety, objectUrl);
|
|
1915
|
+
const result = await activate(client.http, client.safety, objectUrl, activateOpts);
|
|
1319
1916
|
if (result.success) {
|
|
1320
|
-
return textResult(`Successfully activated ${type} ${name}.${result
|
|
1917
|
+
return textResult(`Successfully activated ${type} ${name}.${formatActivationMessages(result)}`);
|
|
1321
1918
|
}
|
|
1322
|
-
return errorResult(`Activation failed for ${type} ${name}.\
|
|
1919
|
+
return errorResult(`Activation failed for ${type} ${name}.\n${formatActivationMessages(result)}`);
|
|
1920
|
+
}
|
|
1921
|
+
/** Format activation result messages with structured detail (line numbers, URIs) when available */
|
|
1922
|
+
function formatActivationMessages(result) {
|
|
1923
|
+
if (result.details.length === 0)
|
|
1924
|
+
return '';
|
|
1925
|
+
const errors = result.details.filter((d) => d.severity === 'error');
|
|
1926
|
+
const warnings = result.details.filter((d) => d.severity === 'warning');
|
|
1927
|
+
const parts = [];
|
|
1928
|
+
if (errors.length > 0) {
|
|
1929
|
+
const formatted = errors.map((e) => {
|
|
1930
|
+
const prefix = e.line ? `[line ${e.line}] ` : '';
|
|
1931
|
+
const suffix = e.uri ? ` (${e.uri})` : '';
|
|
1932
|
+
return `- ${prefix}${e.text}${suffix}`;
|
|
1933
|
+
});
|
|
1934
|
+
parts.push(`Errors:\n${formatted.join('\n')}`);
|
|
1935
|
+
}
|
|
1936
|
+
if (warnings.length > 0) {
|
|
1937
|
+
const formatted = warnings.map((w) => {
|
|
1938
|
+
const prefix = w.line ? `[line ${w.line}] ` : '';
|
|
1939
|
+
return `- ${prefix}${w.text}`;
|
|
1940
|
+
});
|
|
1941
|
+
parts.push(`Warnings:\n${formatted.join('\n')}`);
|
|
1942
|
+
}
|
|
1943
|
+
// Fall back to flat messages if no errors/warnings but info messages exist
|
|
1944
|
+
if (parts.length === 0 && result.messages.length > 0) {
|
|
1945
|
+
return `\nMessages: ${result.messages.join('; ')}`;
|
|
1946
|
+
}
|
|
1947
|
+
return parts.length > 0 ? `\n${parts.join('\n')}` : '';
|
|
1323
1948
|
}
|
|
1324
1949
|
// ─── SAPNavigate Handler ─────────────────────────────────────────────
|
|
1325
1950
|
async function handleSAPNavigate(client, args) {
|
|
@@ -1524,8 +2149,9 @@ async function handleSAPTransport(client, args) {
|
|
|
1524
2149
|
const action = String(args.action ?? '');
|
|
1525
2150
|
switch (action) {
|
|
1526
2151
|
case 'list': {
|
|
1527
|
-
const user = args.user;
|
|
1528
|
-
const
|
|
2152
|
+
const user = args.user || client.username;
|
|
2153
|
+
const status = args.status ?? 'D';
|
|
2154
|
+
const transports = await listTransports(client.http, client.safety, user, status === '*' ? undefined : status);
|
|
1529
2155
|
return textResult(JSON.stringify(transports, null, 2));
|
|
1530
2156
|
}
|
|
1531
2157
|
case 'get': {
|
|
@@ -1541,7 +2167,8 @@ async function handleSAPTransport(client, args) {
|
|
|
1541
2167
|
const description = String(args.description ?? '');
|
|
1542
2168
|
if (!description)
|
|
1543
2169
|
return errorResult('Description is required for "create" action.');
|
|
1544
|
-
const
|
|
2170
|
+
const transportType = String(args.type ?? 'K');
|
|
2171
|
+
const id = await createTransport(client.http, client.safety, description, undefined, transportType);
|
|
1545
2172
|
if (!id)
|
|
1546
2173
|
return errorResult('Transport creation succeeded but no transport ID was returned. Check the SAP system manually.');
|
|
1547
2174
|
return textResult(`Created transport request: ${id}`);
|
|
@@ -1553,8 +2180,61 @@ async function handleSAPTransport(client, args) {
|
|
|
1553
2180
|
await releaseTransport(client.http, client.safety, id);
|
|
1554
2181
|
return textResult(`Released transport request: ${id}`);
|
|
1555
2182
|
}
|
|
2183
|
+
case 'delete': {
|
|
2184
|
+
const id = String(args.id ?? '');
|
|
2185
|
+
if (!id)
|
|
2186
|
+
return errorResult('Transport ID is required for "delete" action.');
|
|
2187
|
+
const recursive = Boolean(args.recursive ?? false);
|
|
2188
|
+
await deleteTransport(client.http, client.safety, id, recursive);
|
|
2189
|
+
return textResult(`Deleted transport request: ${id}${recursive ? ' (recursive)' : ''}`);
|
|
2190
|
+
}
|
|
2191
|
+
case 'reassign': {
|
|
2192
|
+
const id = String(args.id ?? '');
|
|
2193
|
+
if (!id)
|
|
2194
|
+
return errorResult('Transport ID is required for "reassign" action.');
|
|
2195
|
+
const owner = String(args.owner ?? '');
|
|
2196
|
+
if (!owner)
|
|
2197
|
+
return errorResult('Owner is required for "reassign" action.');
|
|
2198
|
+
const recursive = Boolean(args.recursive ?? false);
|
|
2199
|
+
await reassignTransport(client.http, client.safety, id, owner, recursive);
|
|
2200
|
+
return textResult(`Reassigned transport ${id} to ${owner}${recursive ? ' (recursive)' : ''}`);
|
|
2201
|
+
}
|
|
2202
|
+
case 'release_recursive': {
|
|
2203
|
+
const id = String(args.id ?? '');
|
|
2204
|
+
if (!id)
|
|
2205
|
+
return errorResult('Transport ID is required for "release_recursive" action.');
|
|
2206
|
+
const result = await releaseTransportRecursive(client.http, client.safety, id);
|
|
2207
|
+
return textResult(JSON.stringify(result, null, 2));
|
|
2208
|
+
}
|
|
2209
|
+
case 'check': {
|
|
2210
|
+
// Check transport requirements for an object/package combination.
|
|
2211
|
+
// Does NOT require enableTransports — this is a read-only check.
|
|
2212
|
+
const objectType = String(args.type ?? '');
|
|
2213
|
+
const objectName = String(args.name ?? '');
|
|
2214
|
+
const pkg = String(args.package ?? '');
|
|
2215
|
+
if (!objectType || !objectName)
|
|
2216
|
+
return errorResult('"type" and "name" are required for "check" action.');
|
|
2217
|
+
if (!pkg)
|
|
2218
|
+
return errorResult('"package" is required for "check" action.');
|
|
2219
|
+
const objectUrl = objectUrlForType(objectType, objectName);
|
|
2220
|
+
const info = await getTransportInfo(client.http, client.safety, objectUrl, pkg, 'I');
|
|
2221
|
+
const summary = info.isLocal
|
|
2222
|
+
? `Package "${pkg}" is local — no transport required.`
|
|
2223
|
+
: info.recording
|
|
2224
|
+
? `Package "${pkg}" requires a transport for object creation.`
|
|
2225
|
+
: `Package "${pkg}" does not require transport recording.`;
|
|
2226
|
+
return textResult(JSON.stringify({
|
|
2227
|
+
package: pkg,
|
|
2228
|
+
transportRequired: !info.isLocal && info.recording,
|
|
2229
|
+
isLocal: info.isLocal,
|
|
2230
|
+
deliveryUnit: info.deliveryUnit,
|
|
2231
|
+
existingTransports: info.existingTransports,
|
|
2232
|
+
...(info.lockedTransport ? { lockedTransport: info.lockedTransport } : {}),
|
|
2233
|
+
summary,
|
|
2234
|
+
}, null, 2));
|
|
2235
|
+
}
|
|
1556
2236
|
default:
|
|
1557
|
-
return errorResult(`Unknown SAPTransport action: ${action}. Supported: list, get, create, release`);
|
|
2237
|
+
return errorResult(`Unknown SAPTransport action: ${action}. Supported: list, get, create, release, delete, reassign, release_recursive, check`);
|
|
1558
2238
|
}
|
|
1559
2239
|
}
|
|
1560
2240
|
// ─── SAPContext Handler ───────────────────────────────────────────────
|
|
@@ -1669,6 +2349,7 @@ async function handleSAPContext(client, args, cachingLayer) {
|
|
|
1669
2349
|
let cachedFeatures;
|
|
1670
2350
|
async function handleSAPManage(client, config, args, cachingLayer, isPerUserClient) {
|
|
1671
2351
|
const action = String(args.action ?? '');
|
|
2352
|
+
const flpUnavailableMessage = 'FLP customization service (PAGE_BUILDER_CUST) is not available on this system. Check ICF service activation in SICF.';
|
|
1672
2353
|
switch (action) {
|
|
1673
2354
|
case 'features': {
|
|
1674
2355
|
if (!cachedFeatures) {
|
|
@@ -1676,6 +2357,208 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
|
|
|
1676
2357
|
}
|
|
1677
2358
|
return textResult(JSON.stringify(cachedFeatures, null, 2));
|
|
1678
2359
|
}
|
|
2360
|
+
case 'create_package': {
|
|
2361
|
+
const name = String(args.name ?? '').trim();
|
|
2362
|
+
const description = String(args.description ?? '').trim();
|
|
2363
|
+
const superPackage = String(args.superPackage ?? '').trim();
|
|
2364
|
+
const softwareComponent = String(args.softwareComponent ?? '').trim();
|
|
2365
|
+
const transportLayer = String(args.transportLayer ?? '').trim();
|
|
2366
|
+
const transport = String(args.transport ?? '').trim();
|
|
2367
|
+
if (!name)
|
|
2368
|
+
return errorResult('"name" is required for create_package action.');
|
|
2369
|
+
if (!description)
|
|
2370
|
+
return errorResult('"description" is required for create_package action.');
|
|
2371
|
+
checkOperation(client.safety, OperationType.Create, 'CreatePackage');
|
|
2372
|
+
// Package allowlist is enforced on the parent package, not the new package name.
|
|
2373
|
+
// This enables creating children in allowed parents like $TMP.
|
|
2374
|
+
if (superPackage) {
|
|
2375
|
+
checkPackage(client.safety, superPackage);
|
|
2376
|
+
}
|
|
2377
|
+
let effectiveTransport = transport || undefined;
|
|
2378
|
+
const packageUrl = `/sap/bc/adt/packages/${encodeURIComponent(name)}`;
|
|
2379
|
+
// Transport pre-flight for non-local parent packages when no transport is provided.
|
|
2380
|
+
if (!effectiveTransport && superPackage && superPackage.toUpperCase() !== '$TMP') {
|
|
2381
|
+
try {
|
|
2382
|
+
const transportInfo = await getTransportInfo(client.http, client.safety, packageUrl, superPackage, 'I');
|
|
2383
|
+
if (transportInfo.lockedTransport) {
|
|
2384
|
+
effectiveTransport = transportInfo.lockedTransport;
|
|
2385
|
+
}
|
|
2386
|
+
else if (!transportInfo.isLocal && transportInfo.recording) {
|
|
2387
|
+
const existingList = transportInfo.existingTransports.length > 0
|
|
2388
|
+
? `\n\nExisting transports for this package:\n${transportInfo.existingTransports
|
|
2389
|
+
.slice(0, 10)
|
|
2390
|
+
.map((t) => ` - ${t.id}: ${t.description} (${t.owner})`)
|
|
2391
|
+
.join('\n')}`
|
|
2392
|
+
: '';
|
|
2393
|
+
return errorResult(`Package "${superPackage}" requires a transport number for package creation, but none was provided.\n\n` +
|
|
2394
|
+
`To fix this, either:\n` +
|
|
2395
|
+
`1. Use SAPTransport(action="list") to find an existing modifiable transport\n` +
|
|
2396
|
+
`2. Use SAPTransport(action="create", description="...") to create a new one\n` +
|
|
2397
|
+
`3. Then retry SAPManage(action="create_package", ..., transport="<transport_id>")` +
|
|
2398
|
+
existingList);
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
catch {
|
|
2402
|
+
// Graceful fallback: let SAP enforce transport requirements if the pre-check fails.
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
const packageTypeRaw = String(args.packageType ?? '').trim();
|
|
2406
|
+
const packageType = packageTypeRaw === 'development' || packageTypeRaw === 'structure' || packageTypeRaw === 'main'
|
|
2407
|
+
? packageTypeRaw
|
|
2408
|
+
: undefined;
|
|
2409
|
+
const xml = buildPackageXml({
|
|
2410
|
+
name,
|
|
2411
|
+
description,
|
|
2412
|
+
superPackage: superPackage || undefined,
|
|
2413
|
+
softwareComponent: softwareComponent || undefined,
|
|
2414
|
+
transportLayer: transportLayer || undefined,
|
|
2415
|
+
packageType,
|
|
2416
|
+
});
|
|
2417
|
+
await createObject(client.http, client.safety, '/sap/bc/adt/packages', xml, 'application/*', effectiveTransport);
|
|
2418
|
+
return textResult(`Created package ${name}.`);
|
|
2419
|
+
}
|
|
2420
|
+
case 'delete_package': {
|
|
2421
|
+
const name = String(args.name ?? '').trim();
|
|
2422
|
+
const transport = String(args.transport ?? '').trim();
|
|
2423
|
+
if (!name)
|
|
2424
|
+
return errorResult('"name" is required for delete_package action.');
|
|
2425
|
+
checkOperation(client.safety, OperationType.Delete, 'DeletePackage');
|
|
2426
|
+
const packageUrl = `/sap/bc/adt/packages/${encodeURIComponent(name)}`;
|
|
2427
|
+
await client.http.withStatefulSession(async (session) => {
|
|
2428
|
+
const lock = await lockObject(session, client.safety, packageUrl);
|
|
2429
|
+
const effectiveTransport = transport || lock.corrNr || undefined;
|
|
2430
|
+
try {
|
|
2431
|
+
await deleteObject(session, client.safety, packageUrl, lock.lockHandle, effectiveTransport);
|
|
2432
|
+
}
|
|
2433
|
+
finally {
|
|
2434
|
+
try {
|
|
2435
|
+
await unlockObject(session, packageUrl, lock.lockHandle);
|
|
2436
|
+
}
|
|
2437
|
+
catch {
|
|
2438
|
+
// Object may already be deleted — unlock failure is expected.
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
});
|
|
2442
|
+
return textResult(`Deleted package ${name}.`);
|
|
2443
|
+
}
|
|
2444
|
+
case 'flp_list_catalogs': {
|
|
2445
|
+
const catalogs = await listCatalogs(client.http, client.safety);
|
|
2446
|
+
const customCount = catalogs.filter((c) => /^(Z|Y)/i.test(c.domainId)).length;
|
|
2447
|
+
const lines = [
|
|
2448
|
+
`${catalogs.length} catalogs (${customCount} custom Z/Y). Columns: domainId | title | type | scope | chips`,
|
|
2449
|
+
...catalogs.map((c) => `${c.domainId} | ${c.title || '(no title)'} | ${c.type || '-'} | ${c.scope || '-'} | ${c.chipCount}`),
|
|
2450
|
+
];
|
|
2451
|
+
return textResult(lines.join('\n'));
|
|
2452
|
+
}
|
|
2453
|
+
case 'flp_list_groups': {
|
|
2454
|
+
const groups = await listGroups(client.http, client.safety);
|
|
2455
|
+
const lines = [
|
|
2456
|
+
`${groups.length} groups. Columns: id | title`,
|
|
2457
|
+
...groups.map((g) => `${g.id} | ${g.title || '(no title)'}`),
|
|
2458
|
+
];
|
|
2459
|
+
return textResult(lines.join('\n'));
|
|
2460
|
+
}
|
|
2461
|
+
case 'flp_list_tiles': {
|
|
2462
|
+
const catalogId = String(args.catalogId ?? '');
|
|
2463
|
+
if (!catalogId)
|
|
2464
|
+
return errorResult('"catalogId" is required for flp_list_tiles action.');
|
|
2465
|
+
const result = await listTiles(client.http, client.safety, catalogId);
|
|
2466
|
+
if (result.backendError) {
|
|
2467
|
+
return textResult(`⚠ Backend error for catalog "${catalogId}": ${result.backendError}\n\nReturned 0 tiles.`);
|
|
2468
|
+
}
|
|
2469
|
+
const lines = [
|
|
2470
|
+
`${result.tiles.length} tiles in catalog "${catalogId}". Columns: instanceId | title | chipId | semanticObject | semanticAction`,
|
|
2471
|
+
...result.tiles.map((t) => {
|
|
2472
|
+
const so = t.configuration?.semantic_object ?? '';
|
|
2473
|
+
const sa = t.configuration?.semantic_action ?? '';
|
|
2474
|
+
return `${t.instanceId} | ${t.title || '(no title)'} | ${t.chipId} | ${so} | ${sa}`;
|
|
2475
|
+
}),
|
|
2476
|
+
];
|
|
2477
|
+
return textResult(lines.join('\n'));
|
|
2478
|
+
}
|
|
2479
|
+
case 'flp_create_catalog': {
|
|
2480
|
+
if (cachedFeatures?.flp && !cachedFeatures.flp.available) {
|
|
2481
|
+
return errorResult(flpUnavailableMessage);
|
|
2482
|
+
}
|
|
2483
|
+
const domainId = String(args.domainId ?? '');
|
|
2484
|
+
const title = String(args.title ?? '');
|
|
2485
|
+
if (!domainId)
|
|
2486
|
+
return errorResult('"domainId" is required for flp_create_catalog action.');
|
|
2487
|
+
if (!title)
|
|
2488
|
+
return errorResult('"title" is required for flp_create_catalog action.');
|
|
2489
|
+
const catalog = await createCatalog(client.http, client.safety, domainId, title);
|
|
2490
|
+
return textResult(JSON.stringify(catalog, null, 2));
|
|
2491
|
+
}
|
|
2492
|
+
case 'flp_create_group': {
|
|
2493
|
+
if (cachedFeatures?.flp && !cachedFeatures.flp.available) {
|
|
2494
|
+
return errorResult(flpUnavailableMessage);
|
|
2495
|
+
}
|
|
2496
|
+
const groupId = String(args.groupId ?? '');
|
|
2497
|
+
const title = String(args.title ?? '');
|
|
2498
|
+
if (!groupId)
|
|
2499
|
+
return errorResult('"groupId" is required for flp_create_group action.');
|
|
2500
|
+
if (!title)
|
|
2501
|
+
return errorResult('"title" is required for flp_create_group action.');
|
|
2502
|
+
const group = await createGroup(client.http, client.safety, groupId, title);
|
|
2503
|
+
return textResult(JSON.stringify(group, null, 2));
|
|
2504
|
+
}
|
|
2505
|
+
case 'flp_create_tile': {
|
|
2506
|
+
if (cachedFeatures?.flp && !cachedFeatures.flp.available) {
|
|
2507
|
+
return errorResult(flpUnavailableMessage);
|
|
2508
|
+
}
|
|
2509
|
+
const catalogId = String(args.catalogId ?? '');
|
|
2510
|
+
if (!catalogId)
|
|
2511
|
+
return errorResult('"catalogId" is required for flp_create_tile action.');
|
|
2512
|
+
const rawTile = args.tile;
|
|
2513
|
+
if (!rawTile || typeof rawTile !== 'object' || Array.isArray(rawTile)) {
|
|
2514
|
+
return errorResult('"tile" object is required for flp_create_tile action.');
|
|
2515
|
+
}
|
|
2516
|
+
const tile = rawTile;
|
|
2517
|
+
const id = String(tile.id ?? '');
|
|
2518
|
+
const title = String(tile.title ?? '');
|
|
2519
|
+
const semanticObject = String(tile.semanticObject ?? '');
|
|
2520
|
+
const semanticAction = String(tile.semanticAction ?? '');
|
|
2521
|
+
if (!id || !title || !semanticObject || !semanticAction) {
|
|
2522
|
+
return errorResult('"tile.id", "tile.title", "tile.semanticObject", and "tile.semanticAction" are required for flp_create_tile action.');
|
|
2523
|
+
}
|
|
2524
|
+
const tileInstance = await createTile(client.http, client.safety, catalogId, {
|
|
2525
|
+
id,
|
|
2526
|
+
title,
|
|
2527
|
+
semanticObject,
|
|
2528
|
+
semanticAction,
|
|
2529
|
+
icon: typeof tile.icon === 'string' ? tile.icon : undefined,
|
|
2530
|
+
url: typeof tile.url === 'string' ? tile.url : undefined,
|
|
2531
|
+
subtitle: typeof tile.subtitle === 'string' ? tile.subtitle : undefined,
|
|
2532
|
+
info: typeof tile.info === 'string' ? tile.info : undefined,
|
|
2533
|
+
});
|
|
2534
|
+
return textResult(JSON.stringify(tileInstance, null, 2));
|
|
2535
|
+
}
|
|
2536
|
+
case 'flp_add_tile_to_group': {
|
|
2537
|
+
if (cachedFeatures?.flp && !cachedFeatures.flp.available) {
|
|
2538
|
+
return errorResult(flpUnavailableMessage);
|
|
2539
|
+
}
|
|
2540
|
+
const groupId = String(args.groupId ?? '');
|
|
2541
|
+
const catalogId = String(args.catalogId ?? '');
|
|
2542
|
+
const tileInstanceId = String(args.tileInstanceId ?? '');
|
|
2543
|
+
if (!groupId)
|
|
2544
|
+
return errorResult('"groupId" is required for flp_add_tile_to_group action.');
|
|
2545
|
+
if (!catalogId)
|
|
2546
|
+
return errorResult('"catalogId" is required for flp_add_tile_to_group action.');
|
|
2547
|
+
if (!tileInstanceId)
|
|
2548
|
+
return errorResult('"tileInstanceId" is required for flp_add_tile_to_group action.');
|
|
2549
|
+
const result = await addTileToGroup(client.http, client.safety, groupId, catalogId, tileInstanceId);
|
|
2550
|
+
return textResult(JSON.stringify(result, null, 2));
|
|
2551
|
+
}
|
|
2552
|
+
case 'flp_delete_catalog': {
|
|
2553
|
+
if (cachedFeatures?.flp && !cachedFeatures.flp.available) {
|
|
2554
|
+
return errorResult(flpUnavailableMessage);
|
|
2555
|
+
}
|
|
2556
|
+
const catalogId = String(args.catalogId ?? '');
|
|
2557
|
+
if (!catalogId)
|
|
2558
|
+
return errorResult('"catalogId" is required for flp_delete_catalog action.');
|
|
2559
|
+
await deleteCatalog(client.http, client.safety, catalogId);
|
|
2560
|
+
return textResult(`Deleted FLP catalog: ${catalogId}`);
|
|
2561
|
+
}
|
|
1679
2562
|
case 'cache_stats': {
|
|
1680
2563
|
if (!cachingLayer) {
|
|
1681
2564
|
return textResult(JSON.stringify({ enabled: false, message: 'Object cache is disabled (ARC1_CACHE=none).' }));
|
|
@@ -1698,6 +2581,7 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
|
|
|
1698
2581
|
featureConfig.ui5 = config.featureUi5;
|
|
1699
2582
|
featureConfig.transport = config.featureTransport;
|
|
1700
2583
|
featureConfig.ui5repo = config.featureUi5Repo;
|
|
2584
|
+
featureConfig.flp = config.featureFlp;
|
|
1701
2585
|
const probed = await probeFeatures(client.http, featureConfig, config.systemType);
|
|
1702
2586
|
// In PP mode with a per-user client, auth-sensitive results (401/403 on any
|
|
1703
2587
|
// feature) must not poison the global cache — another user may have different
|
|
@@ -1721,7 +2605,7 @@ async function handleSAPManage(client, config, args, cachingLayer, isPerUserClie
|
|
|
1721
2605
|
return textResult(JSON.stringify(probed, null, 2));
|
|
1722
2606
|
}
|
|
1723
2607
|
default:
|
|
1724
|
-
return errorResult(`Unknown SAPManage action: ${action}. Supported: features, probe, cache_stats`);
|
|
2608
|
+
return errorResult(`Unknown SAPManage action: ${action}. Supported: features, probe, cache_stats, create_package, delete_package, flp_list_catalogs, flp_list_groups, flp_list_tiles, flp_create_catalog, flp_create_group, flp_create_tile, flp_add_tile_to_group, flp_delete_catalog`);
|
|
1725
2609
|
}
|
|
1726
2610
|
}
|
|
1727
2611
|
/** Reset cached features (for testing) */
|