arc-1 0.9.13 → 0.9.15
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 +17 -4
- package/dist/adt/class-structure.d.ts +1 -1
- package/dist/adt/class-structure.js +1 -1
- package/dist/adt/client.d.ts +16 -3
- package/dist/adt/client.d.ts.map +1 -1
- package/dist/adt/client.js +40 -35
- package/dist/adt/client.js.map +1 -1
- package/dist/adt/ddic-xml.d.ts +9 -0
- package/dist/adt/ddic-xml.d.ts.map +1 -1
- package/dist/adt/ddic-xml.js +49 -53
- package/dist/adt/ddic-xml.js.map +1 -1
- package/dist/adt/devtools.d.ts.map +1 -1
- package/dist/adt/devtools.js +46 -15
- package/dist/adt/devtools.js.map +1 -1
- package/dist/adt/fm-signature.d.ts +1 -1
- package/dist/adt/fm-signature.js +1 -1
- package/dist/adt/rap-generate.js +1 -1
- package/dist/adt/rap-generate.js.map +1 -1
- package/dist/adt/rap-handlers.d.ts +1 -1
- package/dist/adt/rap-handlers.js +1 -1
- package/dist/adt/refactoring.d.ts.map +1 -1
- package/dist/adt/refactoring.js +11 -14
- package/dist/adt/refactoring.js.map +1 -1
- package/dist/adt/server-driven.d.ts +46 -2
- package/dist/adt/server-driven.d.ts.map +1 -1
- package/dist/adt/server-driven.js +13 -3
- package/dist/adt/server-driven.js.map +1 -1
- package/dist/adt/xml-parser.d.ts +8 -1
- package/dist/adt/xml-parser.d.ts.map +1 -1
- package/dist/adt/xml-parser.js +57 -49
- package/dist/adt/xml-parser.js.map +1 -1
- package/dist/authz/policy.d.ts +1 -1
- package/dist/authz/policy.js +1 -1
- package/dist/cache/cache.d.ts +1 -0
- package/dist/cache/cache.d.ts.map +1 -1
- package/dist/cache/cache.js.map +1 -1
- package/dist/cache/inactive-list-cache.d.ts +4 -4
- package/dist/cache/inactive-list-cache.d.ts.map +1 -1
- package/dist/cache/inactive-list-cache.js +19 -12
- package/dist/cache/inactive-list-cache.js.map +1 -1
- package/dist/cache/memory.d.ts +1 -0
- package/dist/cache/memory.d.ts.map +1 -1
- package/dist/cache/memory.js +3 -0
- package/dist/cache/memory.js.map +1 -1
- package/dist/cache/sqlite.d.ts +1 -0
- package/dist/cache/sqlite.d.ts.map +1 -1
- package/dist/cache/sqlite.js +3 -0
- package/dist/cache/sqlite.js.map +1 -1
- package/dist/cache/warmup.d.ts.map +1 -1
- package/dist/cache/warmup.js +85 -38
- package/dist/cache/warmup.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/context/compressor.d.ts.map +1 -1
- package/dist/context/compressor.js +22 -13
- package/dist/context/compressor.js.map +1 -1
- package/dist/context/contract.d.ts.map +1 -1
- package/dist/context/contract.js +4 -3
- package/dist/context/contract.js.map +1 -1
- package/dist/context/deps.d.ts.map +1 -1
- package/dist/context/deps.js +3 -2
- package/dist/context/deps.js.map +1 -1
- package/dist/context/grep.d.ts +3 -1
- package/dist/context/grep.d.ts.map +1 -1
- package/dist/context/grep.js +83 -1
- package/dist/context/grep.js.map +1 -1
- package/dist/context/method-surgery.d.ts.map +1 -1
- package/dist/context/method-surgery.js +3 -2
- package/dist/context/method-surgery.js.map +1 -1
- package/dist/handlers/activate.d.ts +25 -0
- package/dist/handlers/activate.d.ts.map +1 -0
- package/dist/handlers/activate.js +334 -0
- package/dist/handlers/activate.js.map +1 -0
- package/dist/handlers/cache-security.d.ts +22 -0
- package/dist/handlers/cache-security.d.ts.map +1 -0
- package/dist/handlers/cache-security.js +51 -0
- package/dist/handlers/cache-security.js.map +1 -0
- package/dist/handlers/cds-hints.d.ts +26 -0
- package/dist/handlers/cds-hints.d.ts.map +1 -0
- package/dist/handlers/cds-hints.js +380 -0
- package/dist/handlers/cds-hints.js.map +1 -0
- package/dist/handlers/context.d.ts +10 -0
- package/dist/handlers/context.d.ts.map +1 -0
- package/dist/handlers/context.js +344 -0
- package/dist/handlers/context.js.map +1 -0
- package/dist/handlers/diagnose.d.ts +8 -0
- package/dist/handlers/diagnose.d.ts.map +1 -0
- package/dist/handlers/diagnose.js +274 -0
- package/dist/handlers/diagnose.js.map +1 -0
- package/dist/handlers/dispatch.d.ts +39 -0
- package/dist/handlers/dispatch.d.ts.map +1 -0
- package/dist/handlers/dispatch.js +640 -0
- package/dist/handlers/dispatch.js.map +1 -0
- package/dist/handlers/feature-cache.d.ts +26 -0
- package/dist/handlers/feature-cache.d.ts.map +1 -0
- package/dist/handlers/feature-cache.js +45 -0
- package/dist/handlers/feature-cache.js.map +1 -0
- package/dist/handlers/git.d.ts +9 -0
- package/dist/handlers/git.d.ts.map +1 -0
- package/dist/handlers/git.js +227 -0
- package/dist/handlers/git.js.map +1 -0
- package/dist/handlers/lint.d.ts +9 -0
- package/dist/handlers/lint.d.ts.map +1 -0
- package/dist/handlers/lint.js +82 -0
- package/dist/handlers/lint.js.map +1 -0
- package/dist/handlers/manage.d.ts +10 -0
- package/dist/handlers/manage.d.ts.map +1 -0
- package/dist/handlers/manage.js +375 -0
- package/dist/handlers/manage.js.map +1 -0
- package/dist/handlers/navigate.d.ts +8 -0
- package/dist/handlers/navigate.d.ts.map +1 -0
- package/dist/handlers/navigate.js +188 -0
- package/dist/handlers/navigate.js.map +1 -0
- package/dist/handlers/object-types.d.ts +103 -0
- package/dist/handlers/object-types.d.ts.map +1 -0
- package/dist/handlers/object-types.js +476 -0
- package/dist/handlers/object-types.js.map +1 -0
- package/dist/handlers/query.d.ts +7 -0
- package/dist/handlers/query.d.ts.map +1 -0
- package/dist/handlers/query.js +190 -0
- package/dist/handlers/query.js.map +1 -0
- package/dist/handlers/read.d.ts +18 -0
- package/dist/handlers/read.d.ts.map +1 -0
- package/dist/handlers/read.js +581 -0
- package/dist/handlers/read.js.map +1 -0
- package/dist/handlers/schemas.d.ts +28 -26
- package/dist/handlers/schemas.d.ts.map +1 -1
- package/dist/handlers/schemas.js +17 -153
- package/dist/handlers/schemas.js.map +1 -1
- package/dist/handlers/search.d.ts +24 -0
- package/dist/handlers/search.d.ts.map +1 -0
- package/dist/handlers/search.js +208 -0
- package/dist/handlers/search.js.map +1 -0
- package/dist/handlers/shared.d.ts +19 -0
- package/dist/handlers/shared.d.ts.map +1 -0
- package/dist/handlers/shared.js +23 -0
- package/dist/handlers/shared.js.map +1 -0
- package/dist/handlers/tool-registry.d.ts +44 -0
- package/dist/handlers/tool-registry.d.ts.map +1 -0
- package/dist/handlers/tool-registry.js +152 -0
- package/dist/handlers/tool-registry.js.map +1 -0
- package/dist/handlers/tools.d.ts +9 -0
- package/dist/handlers/tools.d.ts.map +1 -1
- package/dist/handlers/tools.js +40 -141
- package/dist/handlers/tools.js.map +1 -1
- package/dist/handlers/transport.d.ts +8 -0
- package/dist/handlers/transport.d.ts.map +1 -0
- package/dist/handlers/transport.js +281 -0
- package/dist/handlers/transport.js.map +1 -0
- package/dist/handlers/write/class-surgery.d.ts +18 -0
- package/dist/handlers/write/class-surgery.d.ts.map +1 -0
- package/dist/handlers/write/class-surgery.js +366 -0
- package/dist/handlers/write/class-surgery.js.map +1 -0
- package/dist/handlers/write/context.d.ts +43 -0
- package/dist/handlers/write/context.d.ts.map +1 -0
- package/dist/handlers/write/context.js +5 -0
- package/dist/handlers/write/context.js.map +1 -0
- package/dist/handlers/write/create.d.ts +8 -0
- package/dist/handlers/write/create.d.ts.map +1 -0
- package/dist/handlers/write/create.js +603 -0
- package/dist/handlers/write/create.js.map +1 -0
- package/dist/handlers/write/rap.d.ts +8 -0
- package/dist/handlers/write/rap.d.ts.map +1 -0
- package/dist/handlers/write/rap.js +235 -0
- package/dist/handlers/write/rap.js.map +1 -0
- package/dist/handlers/write/update-delete.d.ts +8 -0
- package/dist/handlers/write/update-delete.d.ts.map +1 -0
- package/dist/handlers/write/update-delete.js +182 -0
- package/dist/handlers/write/update-delete.js.map +1 -0
- package/dist/handlers/write-helpers.d.ts +155 -0
- package/dist/handlers/write-helpers.d.ts.map +1 -0
- package/dist/handlers/write-helpers.js +859 -0
- package/dist/handlers/write-helpers.js.map +1 -0
- package/dist/handlers/write.d.ts +16 -0
- package/dist/handlers/write.d.ts.map +1 -0
- package/dist/handlers/write.js +210 -0
- package/dist/handlers/write.js.map +1 -0
- package/dist/handlers/zod-errors.d.ts +1 -1
- package/dist/handlers/zod-errors.js +1 -1
- package/dist/lint/abaplint-config-cache.d.ts +5 -0
- package/dist/lint/abaplint-config-cache.d.ts.map +1 -0
- package/dist/lint/abaplint-config-cache.js +29 -0
- package/dist/lint/abaplint-config-cache.js.map +1 -0
- package/dist/lint/config-builder.d.ts.map +1 -1
- package/dist/lint/config-builder.js +3 -4
- package/dist/lint/config-builder.js.map +1 -1
- package/dist/lint/lint.d.ts +1 -1
- package/dist/lint/lint.d.ts.map +1 -1
- package/dist/lint/lint.js +3 -2
- package/dist/lint/lint.js.map +1 -1
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +22 -8
- package/dist/server/config.js.map +1 -1
- package/dist/server/mcp-rate-limit.d.ts +1 -1
- package/dist/server/mcp-rate-limit.js +1 -1
- package/dist/server/server.d.ts +1 -1
- package/dist/server/server.js +3 -2
- package/dist/server/server.js.map +1 -1
- package/package.json +15 -10
- package/dist/handlers/intent.d.ts +0 -144
- package/dist/handlers/intent.d.ts.map +0 -1
- package/dist/handlers/intent.js +0 -6782
- package/dist/handlers/intent.js.map +0 -1
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SAPWrite actions — create + batch_create.
|
|
3
|
+
*/
|
|
4
|
+
import { createObject, lockObject, safeUpdateObject, safeUpdateSource, unlockObject, updateObject, } from '../../adt/crud.js';
|
|
5
|
+
import { normalizeAdtLanguage, rewriteKtdText } from '../../adt/ddic-xml.js';
|
|
6
|
+
import { activate, activateBatch } from '../../adt/devtools.js';
|
|
7
|
+
import { AdtApiError } from '../../adt/errors.js';
|
|
8
|
+
import { spliceFmSignature } from '../../adt/fm-signature.js';
|
|
9
|
+
import { checkPackage } from '../../adt/safety.js';
|
|
10
|
+
import { getTransport, getTransportInfo } from '../../adt/transport.js';
|
|
11
|
+
import { escapeXmlAttr } from '../../adt/xml-parser.js';
|
|
12
|
+
import { validateAffHeader } from '../../aff/validator.js';
|
|
13
|
+
import { logger } from '../../server/logger.js';
|
|
14
|
+
import { buildBatchActivationStatuses, formatBatchActivationStatuses, } from '../activate.js';
|
|
15
|
+
import { invalidateInactiveList } from '../cache-security.js';
|
|
16
|
+
import { guardCdsSyntax } from '../cds-hints.js';
|
|
17
|
+
import { cachedFeatures, isTablesEndpointAvailable } from '../feature-cache.js';
|
|
18
|
+
import { normalizeObjectType, normalizeWriteObjectType, objectBasePath, objectUrlForType, sourceUrlForType, } from '../object-types.js';
|
|
19
|
+
import { errorResult, textResult } from '../shared.js';
|
|
20
|
+
import { buildCreateXml, createContentTypeForType, dtelNeedsPostCreateUpdate, getMetadataWriteProperties, isMetadataWriteType, mergePreWriteWarnings, runPreWriteLint, runRapPreflightValidation, SKTD_V2_CONTENT_TYPE, stripFmParamCommentBlock, TABL_DT_WRITE_UNAVAILABLE_HINT, tryPostSaveSyntaxCheck, vendorContentTypeForType, } from '../write-helpers.js';
|
|
21
|
+
function normalizePackageOverride(rawPackage, fallback) {
|
|
22
|
+
if (rawPackage === undefined || rawPackage === null) {
|
|
23
|
+
return fallback;
|
|
24
|
+
}
|
|
25
|
+
const value = String(rawPackage).trim();
|
|
26
|
+
return value || fallback;
|
|
27
|
+
}
|
|
28
|
+
function normalizeTransportOverride(rawTransport) {
|
|
29
|
+
if (rawTransport === undefined || rawTransport === null) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
const value = String(rawTransport).trim();
|
|
33
|
+
return value || undefined;
|
|
34
|
+
}
|
|
35
|
+
export async function writeActionCreate(ctx) {
|
|
36
|
+
const { client, args, config, type, name, source, transport, lintOverride, preflightOverride, objectUrl, srcUrl, invalidateWrittenObject, } = ctx;
|
|
37
|
+
const pkg = String(args.package ?? '$TMP');
|
|
38
|
+
await checkPackage(client.safety, pkg, client.getPackageHierarchyResolver());
|
|
39
|
+
const description = String(args.description ?? name);
|
|
40
|
+
// Pre-flight: check transport requirements for non-$TMP packages when no transport provided.
|
|
41
|
+
// SAP requires a transport number for objects in transportable packages.
|
|
42
|
+
// Instead of letting SAP return a cryptic error, we detect this early and return
|
|
43
|
+
// an actionable error message guiding the LLM to use SAPTransport first.
|
|
44
|
+
let effectiveTransport = transport;
|
|
45
|
+
if (!transport && pkg.toUpperCase() !== '$TMP') {
|
|
46
|
+
try {
|
|
47
|
+
const transportInfo = await getTransportInfo(client.http, client.safety, objectUrl, pkg, 'I');
|
|
48
|
+
if (transportInfo.lockedTransport) {
|
|
49
|
+
// Object is already locked in a transport — use it automatically
|
|
50
|
+
effectiveTransport = transportInfo.lockedTransport;
|
|
51
|
+
}
|
|
52
|
+
else if (!transportInfo.isLocal && transportInfo.recording) {
|
|
53
|
+
// Transport IS required but none provided — return guidance
|
|
54
|
+
const existingList = transportInfo.existingTransports.length > 0
|
|
55
|
+
? `\n\nExisting transports for this package:\n${transportInfo.existingTransports
|
|
56
|
+
.slice(0, 10)
|
|
57
|
+
.map((t) => ` - ${t.id}: ${t.description} (${t.owner})`)
|
|
58
|
+
.join('\n')}`
|
|
59
|
+
: '';
|
|
60
|
+
return errorResult(`Package "${pkg}" requires a transport number for object creation, but none was provided.\n\n` +
|
|
61
|
+
`To fix this, either:\n` +
|
|
62
|
+
`1. Use SAPTransport(action="list") to find an existing modifiable transport\n` +
|
|
63
|
+
`2. Use SAPTransport(action="create", description="...") to create a new one\n` +
|
|
64
|
+
`3. Then retry SAPWrite(action="create", ..., transport="<transport_id>")` +
|
|
65
|
+
existingList);
|
|
66
|
+
}
|
|
67
|
+
// isLocal=true or recording=false → no transport needed, proceed without one
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// If transportInfo check fails (older system, permissions, etc.), proceed without it.
|
|
71
|
+
// SAP will return its own error if a transport is actually needed.
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// MSAG transport-vs-task guard. Some SAP releases silently drop message inserts when
|
|
75
|
+
// given a task number as corrNr — CL_ADT_MESSAGE_CLASS_API=>create() passes corrNr to
|
|
76
|
+
// CTS_WBO_API_INSERT_OBJECTS which only accepts request numbers. The TADIR entry is
|
|
77
|
+
// created but T100/T100A are never written, leaving a phantom MSAG. Confirmed on NW 7.50;
|
|
78
|
+
// unclear whether later releases fixed it, so validate everywhere.
|
|
79
|
+
// Cost: one extra HTTP roundtrip per MSAG create (negligible vs. the data loss risk).
|
|
80
|
+
if (type === 'MSAG' && effectiveTransport) {
|
|
81
|
+
const tr = await getTransport(client.http, client.safety, effectiveTransport);
|
|
82
|
+
if (!tr) {
|
|
83
|
+
return errorResult(`Transport "${effectiveTransport}" is not a valid transport request. ` +
|
|
84
|
+
`MSAG creation requires a transport request number, not a task number. ` +
|
|
85
|
+
`Use SAPTransport(action="get", id="<request>") to verify, or SAPTransport(action="list") to find modifiable requests.`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// CDS pre-write validation: reject unsupported syntax early
|
|
89
|
+
const cdsGuard = guardCdsSyntax(type, source, cachedFeatures);
|
|
90
|
+
if (cdsGuard)
|
|
91
|
+
return cdsGuard;
|
|
92
|
+
// RAP deterministic preflight validation (before object creation to avoid stubs)
|
|
93
|
+
const preflightWarnings = runRapPreflightValidation(source, type, name, cachedFeatures, config.systemType, preflightOverride);
|
|
94
|
+
if (preflightWarnings.blocked)
|
|
95
|
+
return preflightWarnings.result;
|
|
96
|
+
// AFF header validation (if schema available for this type)
|
|
97
|
+
const affResult = validateAffHeader(type, { description, originalLanguage: 'en' });
|
|
98
|
+
if (!affResult.valid) {
|
|
99
|
+
return errorResult(`AFF metadata validation failed for ${type} ${name}:\n- ${(affResult.errors ?? []).join('\n- ')}\n\nFix the metadata and retry.`);
|
|
100
|
+
}
|
|
101
|
+
if (type === 'SKTD') {
|
|
102
|
+
// A KTD is not a standalone object — it documents a parent object (e.g., a DDLS view or a CLAS).
|
|
103
|
+
// The create POST goes to the collection URL with a sktd:docu XML body that references the parent.
|
|
104
|
+
const refType = String(args.refObjectType ?? '');
|
|
105
|
+
if (!refType) {
|
|
106
|
+
return errorResult('"refObjectType" is required for SKTD create — the ADT type+subtype of the parent object being documented (e.g., "DDLS/DF", "CLAS/OC", "PROG/P", "INTF/OI", "BDEF/BDO", "SRVD/SRV").');
|
|
107
|
+
}
|
|
108
|
+
const refName = String(args.refObjectName ?? name);
|
|
109
|
+
// SAP rule: a KTD's own name must equal the parent object's name (one KTD per object).
|
|
110
|
+
// Creating a KTD named differently from its parent fails server-side with a cryptic
|
|
111
|
+
// "Check of condition failed" — fail fast with a clear message instead.
|
|
112
|
+
if (refName.toUpperCase() !== name.toUpperCase()) {
|
|
113
|
+
return errorResult(`SKTD name "${name}" must match refObjectName "${refName}" — a Knowledge Transfer Document inherits the name of the ABAP object it documents (one KTD per object). To document "${refName}", call SAPWrite(action="create", type="SKTD", name="${refName}", refObjectType="${refType}", ...).`);
|
|
114
|
+
}
|
|
115
|
+
const refDescription = String(args.refObjectDescription ?? '');
|
|
116
|
+
// Build the parent URI. ADT URIs use lowercase names by convention (matches the Eclipse trace).
|
|
117
|
+
const refParentType = refType.split('/')[0] ?? '';
|
|
118
|
+
const refUri = `${objectBasePath(refParentType)}${encodeURIComponent(refName.toLowerCase())}`;
|
|
119
|
+
const ktdLang = normalizeAdtLanguage(config.language);
|
|
120
|
+
const ktdBody = `<?xml version="1.0" encoding="UTF-8"?>
|
|
121
|
+
<sktd:docu xmlns:sktd="http://www.sap.com/wbobj/texts/sktd" xmlns:adtcore="http://www.sap.com/adt/core" adtcore:language="${ktdLang}" adtcore:name="${escapeXmlAttr(name)}" adtcore:type="SKTD/TYP" adtcore:masterLanguage="${ktdLang}">
|
|
122
|
+
<adtcore:packageRef adtcore:name="${escapeXmlAttr(pkg)}"/>
|
|
123
|
+
<sktd:refObject adtcore:description="${escapeXmlAttr(refDescription)}" adtcore:name="${escapeXmlAttr(refName)}" adtcore:type="${escapeXmlAttr(refType)}" adtcore:uri="${escapeXmlAttr(refUri)}"/>
|
|
124
|
+
</sktd:docu>`;
|
|
125
|
+
const ktdCreateUrl = '/sap/bc/adt/documentation/ktd/documents';
|
|
126
|
+
const ktdResult = await createObject(client.http, client.safety, ktdCreateUrl, ktdBody, SKTD_V2_CONTENT_TYPE, effectiveTransport, undefined, cachedFeatures?.abapRelease);
|
|
127
|
+
// If initial Markdown was provided, follow up with an update PUT to write it.
|
|
128
|
+
// Same envelope contract as the update path: fetch-then-rewrite ensures we
|
|
129
|
+
// PUT back exactly the shape SAP gave us (with all the server-assigned
|
|
130
|
+
// metadata), only swapping <sktd:text>.
|
|
131
|
+
if (source) {
|
|
132
|
+
const { source: currentEnvelope } = await client.getKtd(name);
|
|
133
|
+
const body = rewriteKtdText(currentEnvelope, source);
|
|
134
|
+
await safeUpdateObject(client.http, client.safety, objectUrl, body, SKTD_V2_CONTENT_TYPE, effectiveTransport, cachedFeatures?.abapRelease);
|
|
135
|
+
invalidateWrittenObject(type, name);
|
|
136
|
+
return textResult(`Created SKTD ${name} in package ${pkg} and wrote Markdown content.\nNext step: SAPActivate(type="SKTD", name="${name}").\n${ktdResult}`);
|
|
137
|
+
}
|
|
138
|
+
invalidateWrittenObject();
|
|
139
|
+
return textResult(`Created SKTD ${name} in package ${pkg} (no Markdown content written — pass "source" to write the body).\nNext step: SAPActivate(type="SKTD", name="${name}").\n${ktdResult}`);
|
|
140
|
+
}
|
|
141
|
+
// Build type-specific creation XML body.
|
|
142
|
+
// SAP ADT requires the root element to match the object type —
|
|
143
|
+
// a generic objectReferences body returns 400 "System expected the element ...".
|
|
144
|
+
const metadataProperties = getMetadataWriteProperties(args);
|
|
145
|
+
const body = buildCreateXml(type, name, pkg, description, metadataProperties, config.language, config.username);
|
|
146
|
+
// Step 1: Create the object (metadata only)
|
|
147
|
+
const createUrl = objectUrl.replace(/\/[^/]+$/, ''); // parent collection URL
|
|
148
|
+
// DOMA/DTEL/BDEF require vendor-specific content types; all other types use
|
|
149
|
+
// 'application/*' — the wildcard lets the SAP server resolve the correct
|
|
150
|
+
// handler (matching how ADT Eclipse and abap-adt-api send requests).
|
|
151
|
+
const contentType = createContentTypeForType(type);
|
|
152
|
+
const needsPackageParam = type === 'BDEF' || type === 'TABL' || type === 'TABL/DT' || type === 'TABL/DS';
|
|
153
|
+
let result;
|
|
154
|
+
try {
|
|
155
|
+
result = await createObject(client.http, client.safety, createUrl, body, contentType, effectiveTransport, needsPackageParam ? pkg : undefined, cachedFeatures?.abapRelease);
|
|
156
|
+
}
|
|
157
|
+
catch (createErr) {
|
|
158
|
+
if (createErr instanceof AdtApiError && (createErr.statusCode === 400 || createErr.statusCode === 409)) {
|
|
159
|
+
const syntaxDetail = await tryPostSaveSyntaxCheck(client, type, name);
|
|
160
|
+
if (syntaxDetail) {
|
|
161
|
+
createErr.message += syntaxDetail;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
throw createErr;
|
|
165
|
+
}
|
|
166
|
+
if (isMetadataWriteType(type)) {
|
|
167
|
+
// SAP's DTEL POST ignores labels, searchHelp, etc. — they require a follow-up PUT.
|
|
168
|
+
// Use withStatefulSession directly (not safeUpdateObject) to keep the lock cycle
|
|
169
|
+
// on the main client's session, avoiding lock contention with subsequent operations.
|
|
170
|
+
if (type === 'DTEL' && dtelNeedsPostCreateUpdate(metadataProperties)) {
|
|
171
|
+
const ct = vendorContentTypeForType(type);
|
|
172
|
+
await client.http.withStatefulSession(async (session) => {
|
|
173
|
+
const lock = await lockObject(session, client.safety, objectUrl, 'MODIFY', cachedFeatures?.abapRelease);
|
|
174
|
+
const lockTransport = effectiveTransport ?? (lock.corrNr || undefined);
|
|
175
|
+
try {
|
|
176
|
+
await updateObject(session, client.safety, objectUrl, body, lock.lockHandle, ct, lockTransport);
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
await unlockObject(session, objectUrl, lock.lockHandle);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
// MSAG: POST creates empty container — follow-up PUT to write messages
|
|
184
|
+
if (type === 'MSAG' && Array.isArray(metadataProperties.messages) && metadataProperties.messages.length > 0) {
|
|
185
|
+
const ct = vendorContentTypeForType(type);
|
|
186
|
+
await client.http.withStatefulSession(async (session) => {
|
|
187
|
+
const lock = await lockObject(session, client.safety, objectUrl, 'MODIFY', cachedFeatures?.abapRelease);
|
|
188
|
+
const lockTransport = effectiveTransport ?? (lock.corrNr || undefined);
|
|
189
|
+
try {
|
|
190
|
+
await updateObject(session, client.safety, objectUrl, body, lock.lockHandle, ct, lockTransport);
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
await unlockObject(session, objectUrl, lock.lockHandle);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
invalidateWrittenObject();
|
|
198
|
+
const followUpHint = type === 'SRVB'
|
|
199
|
+
? `\n\nNext steps:\n1. SAPActivate(type="SRVB", name="${name}")\n2. SAPActivate(action="publish_srvb", name="${name}")`
|
|
200
|
+
: '';
|
|
201
|
+
return textResult(`Created ${type} ${name} in package ${pkg}.\n${result}${followUpHint}`);
|
|
202
|
+
}
|
|
203
|
+
// Step 2: Write source code if provided.
|
|
204
|
+
// Issue #252: FUNC create accepts a structured `parameters` array; if
|
|
205
|
+
// provided we must follow up with a source PUT even when `source` is
|
|
206
|
+
// omitted (the array alone synthesizes a minimal FUNCTION/ENDFUNCTION
|
|
207
|
+
// body containing the signature clause).
|
|
208
|
+
const funcParameters = type === 'FUNC' ? args.parameters : undefined;
|
|
209
|
+
const shouldWriteSource = !!source || (funcParameters !== undefined && funcParameters.length > 0);
|
|
210
|
+
if (shouldWriteSource) {
|
|
211
|
+
// FUNC: build/splice the signature, then strip SAPGUI parameter comment
|
|
212
|
+
// blocks as defense-in-depth (see update path for rationale).
|
|
213
|
+
let createSource = source ?? '';
|
|
214
|
+
let fmParamStripWarning;
|
|
215
|
+
let fmParamMergeWarning;
|
|
216
|
+
if (type === 'FUNC') {
|
|
217
|
+
if (funcParameters !== undefined) {
|
|
218
|
+
let baseSource;
|
|
219
|
+
if (!createSource || createSource.trim() === '') {
|
|
220
|
+
baseSource = `FUNCTION ${name}.\nENDFUNCTION.\n`;
|
|
221
|
+
}
|
|
222
|
+
else if (!/^\s*FUNCTION\s+/i.test(createSource)) {
|
|
223
|
+
// Body-only source — wrap so the splicer has a signature region.
|
|
224
|
+
baseSource = `FUNCTION ${name}.\n${createSource}\nENDFUNCTION.\n`;
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
baseSource = createSource;
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
createSource = spliceFmSignature(baseSource, name, funcParameters);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
createSource = baseSource;
|
|
234
|
+
fmParamMergeWarning =
|
|
235
|
+
'Could not splice structured parameters: source did not start with FUNCTION keyword. Used the supplied source verbatim.';
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const stripped = stripFmParamCommentBlock(createSource);
|
|
239
|
+
createSource = stripped.source;
|
|
240
|
+
if (stripped.wasStripped) {
|
|
241
|
+
fmParamStripWarning =
|
|
242
|
+
'Stripped *"…IMPORTING/EXPORTING…*" parameter comment blocks (pass `parameters` as a structured array instead).';
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// Pre-write lint validation
|
|
246
|
+
const lintWarnings = runPreWriteLint(createSource, type, name, config, lintOverride);
|
|
247
|
+
if (lintWarnings.blocked) {
|
|
248
|
+
return textResult(`Created ${type} ${name} in package ${pkg}, but source was rejected by lint:\n${lintWarnings.result.content[0].text}`);
|
|
249
|
+
}
|
|
250
|
+
await safeUpdateSource(client.http, client.safety, objectUrl, srcUrl, createSource, effectiveTransport, cachedFeatures?.abapRelease);
|
|
251
|
+
invalidateWrittenObject(type, name);
|
|
252
|
+
const msg = `Created ${type} ${name} in package ${pkg} and wrote source code.`;
|
|
253
|
+
const warnings = mergePreWriteWarnings(preflightWarnings.warnings, lintWarnings.warnings, fmParamStripWarning, fmParamMergeWarning);
|
|
254
|
+
return warnings ? textResult(`${msg}\n\n${warnings}`) : textResult(msg);
|
|
255
|
+
}
|
|
256
|
+
return textResult(`Created ${type} ${name} in package ${pkg}.\n${result}`);
|
|
257
|
+
}
|
|
258
|
+
export async function writeActionBatchCreate(ctx) {
|
|
259
|
+
const { client, args, config, cachingLayer, cacheSecurity, transport, lintOverride, preflightOverride, invalidateWrittenObject, } = ctx;
|
|
260
|
+
const objects = args.objects;
|
|
261
|
+
if (!objects || !Array.isArray(objects) || objects.length === 0) {
|
|
262
|
+
return errorResult('"objects" array is required and must be non-empty for batch_create action.');
|
|
263
|
+
}
|
|
264
|
+
// Opt-in deferred-activation: writes every object as an inactive draft first,
|
|
265
|
+
// then issues a single terminal activateBatch over the written subset. Use case:
|
|
266
|
+
// composition-linked DDLS / interdependent RAP graphs where per-object inline
|
|
267
|
+
// activate() can't resolve cross-references to not-yet-active siblings.
|
|
268
|
+
const activateAtEnd = args.activateAtEnd === true || String(args.activateAtEnd) === 'true';
|
|
269
|
+
const defaultPackage = normalizePackageOverride(args.package, '$TMP');
|
|
270
|
+
const batchPlan = objects.map((obj) => {
|
|
271
|
+
const objType = normalizeWriteObjectType(String(obj.type ?? ''));
|
|
272
|
+
const objName = String(obj.name ?? '');
|
|
273
|
+
const objPackage = normalizePackageOverride(obj.package, defaultPackage);
|
|
274
|
+
const explicitTransport = normalizeTransportOverride(obj.transport) ?? transport;
|
|
275
|
+
return { obj, type: objType, name: objName, packageName: objPackage, explicitTransport };
|
|
276
|
+
});
|
|
277
|
+
// Check every target package before starting any creates.
|
|
278
|
+
// Resolver is shared across the loop so subtree BFS happens once even when
|
|
279
|
+
// many objects target descendants of the same `ZFOO/**` root.
|
|
280
|
+
{
|
|
281
|
+
const resolver = client.getPackageHierarchyResolver();
|
|
282
|
+
for (const pkg of new Set(batchPlan.map((item) => item.packageName))) {
|
|
283
|
+
await checkPackage(client.safety, pkg, resolver);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// Pre-flight transport check for batch_create (same logic as single create),
|
|
287
|
+
// but keyed by each effective package because objects can override package.
|
|
288
|
+
const autoTransportByPackage = new Map();
|
|
289
|
+
const firstPlanNeedingTransportByPackage = new Map();
|
|
290
|
+
for (const plan of batchPlan) {
|
|
291
|
+
if (!plan.explicitTransport &&
|
|
292
|
+
plan.packageName.toUpperCase() !== '$TMP' &&
|
|
293
|
+
!firstPlanNeedingTransportByPackage.has(plan.packageName)) {
|
|
294
|
+
firstPlanNeedingTransportByPackage.set(plan.packageName, plan);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
for (const [pkg, plan] of firstPlanNeedingTransportByPackage) {
|
|
298
|
+
try {
|
|
299
|
+
const firstUrl = objectUrlForType(plan.type, plan.name);
|
|
300
|
+
const transportInfo = await getTransportInfo(client.http, client.safety, firstUrl, pkg, 'I');
|
|
301
|
+
if (transportInfo.lockedTransport) {
|
|
302
|
+
autoTransportByPackage.set(pkg, transportInfo.lockedTransport);
|
|
303
|
+
}
|
|
304
|
+
else if (!transportInfo.isLocal && transportInfo.recording) {
|
|
305
|
+
const existingList = transportInfo.existingTransports.length > 0
|
|
306
|
+
? `\n\nExisting transports for this package:\n${transportInfo.existingTransports
|
|
307
|
+
.slice(0, 10)
|
|
308
|
+
.map((t) => ` - ${t.id}: ${t.description} (${t.owner})`)
|
|
309
|
+
.join('\n')}`
|
|
310
|
+
: '';
|
|
311
|
+
return errorResult(`Package "${pkg}" requires a transport number for object creation, but none was provided.\n\n` +
|
|
312
|
+
`To fix this, either:\n` +
|
|
313
|
+
`1. Use SAPTransport(action="list") to find an existing modifiable transport\n` +
|
|
314
|
+
`2. Use SAPTransport(action="create", description="...") to create a new one\n` +
|
|
315
|
+
`3. Then retry SAPWrite(action="batch_create", ..., transport="<transport_id>")` +
|
|
316
|
+
existingList);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
catch (err) {
|
|
320
|
+
logger.warn('SAPWrite batch_create transport preflight failed; continuing without auto transport', {
|
|
321
|
+
package: pkg,
|
|
322
|
+
type: plan.type,
|
|
323
|
+
name: plan.name,
|
|
324
|
+
error: err instanceof Error ? err.message : String(err),
|
|
325
|
+
});
|
|
326
|
+
// If transportInfo check fails, proceed — SAP will return its own error if needed.
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const results = [];
|
|
330
|
+
const batchWarnings = [];
|
|
331
|
+
// Per-batch cache for the MSAG transport-vs-task guard. The bug is universal so the
|
|
332
|
+
// guard fires for every MSAG entry, but a batch typically shares one transport — cache
|
|
333
|
+
// the lookup result to avoid one HTTP roundtrip per object.
|
|
334
|
+
const transportLookupCache = new Map();
|
|
335
|
+
// Accumulated objects whose create + source-write phase succeeded — used by the
|
|
336
|
+
// terminal activateBatch when activateAtEnd=true. Order matches the input order.
|
|
337
|
+
const writtenObjects = [];
|
|
338
|
+
for (const plan of batchPlan) {
|
|
339
|
+
const { obj, type: objType, name: objName, packageName: objPackage } = plan;
|
|
340
|
+
const objTransport = plan.explicitTransport ?? autoTransportByPackage.get(objPackage);
|
|
341
|
+
const metadataObject = isMetadataWriteType(objType);
|
|
342
|
+
const objSource = obj.source ? String(obj.source) : undefined;
|
|
343
|
+
const objDescription = String(obj.description ?? objName);
|
|
344
|
+
// Mixed-case object name rejection (matches the create-path check above).
|
|
345
|
+
// Universal SAP convention — TADIR is uppercase on every release.
|
|
346
|
+
// Cheap check first: no HTTP call, fail fast on bad names.
|
|
347
|
+
if (objName && objName !== objName.toUpperCase()) {
|
|
348
|
+
results.push({
|
|
349
|
+
type: objType,
|
|
350
|
+
name: objName,
|
|
351
|
+
packageName: objPackage,
|
|
352
|
+
status: 'failed',
|
|
353
|
+
error: `Object name "${objName}" contains lowercase characters. SAP object names must be uppercase (e.g. "${objName.toUpperCase()}"). Source code inside the object can use mixed case.`,
|
|
354
|
+
});
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
// MSAG transport-vs-task guard (per-batch cache to avoid per-object roundtrip).
|
|
358
|
+
if (objType === 'MSAG' && objTransport) {
|
|
359
|
+
let tr = transportLookupCache.get(objTransport);
|
|
360
|
+
if (tr === undefined) {
|
|
361
|
+
tr = await getTransport(client.http, client.safety, objTransport);
|
|
362
|
+
transportLookupCache.set(objTransport, tr);
|
|
363
|
+
}
|
|
364
|
+
if (!tr) {
|
|
365
|
+
results.push({
|
|
366
|
+
type: objType,
|
|
367
|
+
name: objName,
|
|
368
|
+
packageName: objPackage,
|
|
369
|
+
status: 'failed',
|
|
370
|
+
error: `Transport "${objTransport}" is not a valid transport request. MSAG creation requires a transport request number, not a task number.`,
|
|
371
|
+
});
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
// AFF header validation per object (if schema available)
|
|
376
|
+
const affResult = validateAffHeader(objType, { description: objDescription, originalLanguage: 'en' });
|
|
377
|
+
if (!affResult.valid) {
|
|
378
|
+
results.push({
|
|
379
|
+
type: objType,
|
|
380
|
+
name: objName,
|
|
381
|
+
packageName: objPackage,
|
|
382
|
+
status: 'failed',
|
|
383
|
+
error: `AFF metadata validation failed:\n- ${(affResult.errors ?? []).join('\n- ')}`,
|
|
384
|
+
});
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
// Pre-validate source with lint BEFORE creating the object to avoid orphaned objects.
|
|
389
|
+
// Metadata objects (DOMA/DTEL) are XML-only and intentionally skip source lint.
|
|
390
|
+
if (!metadataObject && objSource) {
|
|
391
|
+
const preflightWarnings = runRapPreflightValidation(objSource, objType, objName, cachedFeatures, config.systemType, preflightOverride);
|
|
392
|
+
if (preflightWarnings.blocked) {
|
|
393
|
+
results.push({
|
|
394
|
+
type: objType,
|
|
395
|
+
name: objName,
|
|
396
|
+
packageName: objPackage,
|
|
397
|
+
status: 'failed',
|
|
398
|
+
error: preflightWarnings.result.content[0].text,
|
|
399
|
+
});
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
if (preflightWarnings.warnings) {
|
|
403
|
+
batchWarnings.push(`${objType} ${objName}: ${preflightWarnings.warnings}`);
|
|
404
|
+
}
|
|
405
|
+
const lintWarnings = runPreWriteLint(objSource, objType, objName, config, lintOverride);
|
|
406
|
+
if (lintWarnings.blocked) {
|
|
407
|
+
results.push({
|
|
408
|
+
type: objType,
|
|
409
|
+
name: objName,
|
|
410
|
+
packageName: objPackage,
|
|
411
|
+
status: 'failed',
|
|
412
|
+
error: `source rejected by lint: ${lintWarnings.result.content[0].text}`,
|
|
413
|
+
});
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
// Step 1: Create the object (per-entry transparent-table discovery gate;
|
|
418
|
+
// mirrors the single-create site above. TABL/DS skips it — /structures/ always exists.)
|
|
419
|
+
if ((objType === 'TABL' || objType === 'TABL/DT') && isTablesEndpointAvailable() === false) {
|
|
420
|
+
results.push({
|
|
421
|
+
type: objType,
|
|
422
|
+
name: objName,
|
|
423
|
+
packageName: objPackage,
|
|
424
|
+
status: 'failed',
|
|
425
|
+
error: TABL_DT_WRITE_UNAVAILABLE_HINT,
|
|
426
|
+
});
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
const objUrl = objectUrlForType(objType, objName);
|
|
430
|
+
const createUrl = objUrl.replace(/\/[^/]+$/, '');
|
|
431
|
+
const objMetadataProps = getMetadataWriteProperties(obj);
|
|
432
|
+
const body = buildCreateXml(objType, objName, objPackage, objDescription, objMetadataProps, config.language, config.username);
|
|
433
|
+
const contentType = createContentTypeForType(objType);
|
|
434
|
+
const needsPackageParam = objType === 'BDEF' || objType === 'TABL' || objType === 'TABL/DT' || objType === 'TABL/DS';
|
|
435
|
+
try {
|
|
436
|
+
await createObject(client.http, client.safety, createUrl, body, contentType, objTransport, needsPackageParam ? objPackage : undefined, cachedFeatures?.abapRelease);
|
|
437
|
+
}
|
|
438
|
+
catch (createErr) {
|
|
439
|
+
if (createErr instanceof AdtApiError && (createErr.statusCode === 400 || createErr.statusCode === 409)) {
|
|
440
|
+
const syntaxDetail = await tryPostSaveSyntaxCheck(client, objType, objName);
|
|
441
|
+
if (syntaxDetail) {
|
|
442
|
+
createErr.message += syntaxDetail;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
throw createErr;
|
|
446
|
+
}
|
|
447
|
+
// Step 1b: DTEL POST ignores labels — follow up with PUT on main session
|
|
448
|
+
if (objType === 'DTEL' && dtelNeedsPostCreateUpdate(objMetadataProps)) {
|
|
449
|
+
await client.http.withStatefulSession(async (session) => {
|
|
450
|
+
const lock = await lockObject(session, client.safety, objUrl, 'MODIFY', cachedFeatures?.abapRelease);
|
|
451
|
+
const lockTransport = objTransport ?? (lock.corrNr || undefined);
|
|
452
|
+
try {
|
|
453
|
+
await updateObject(session, client.safety, objUrl, body, lock.lockHandle, contentType, lockTransport);
|
|
454
|
+
}
|
|
455
|
+
finally {
|
|
456
|
+
await unlockObject(session, objUrl, lock.lockHandle);
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
// Step 2: Write source if provided
|
|
461
|
+
if (!metadataObject && objSource) {
|
|
462
|
+
const srcUrl = sourceUrlForType(objType, objName);
|
|
463
|
+
await safeUpdateSource(client.http, client.safety, objUrl, srcUrl, objSource, objTransport, cachedFeatures?.abapRelease);
|
|
464
|
+
}
|
|
465
|
+
// Resolve the activation URL up front so both the inline path and the
|
|
466
|
+
// deferred terminal-activate path use the same URL. FUNC needs the parent
|
|
467
|
+
// function-group baked into the path (issue #250); objectUrlForType throws
|
|
468
|
+
// for FUNC so we mirror the FUNC-aware resolver from handleSAPActivate. For
|
|
469
|
+
// TABL we keep objUrl (already resolved to /tables/) — DDIC-structure FMs
|
|
470
|
+
// aren't a real concept and the create path doesn't expose one.
|
|
471
|
+
let activationUrl = objUrl;
|
|
472
|
+
if (objType === 'FUNC') {
|
|
473
|
+
let group = String(obj.group ?? args.group ?? '').trim();
|
|
474
|
+
if (!group) {
|
|
475
|
+
const resolved = cachingLayer
|
|
476
|
+
? await cachingLayer.resolveFuncGroup(client, objName)
|
|
477
|
+
: await client.resolveFunctionGroup(objName);
|
|
478
|
+
if (!resolved) {
|
|
479
|
+
throw new Error(`Cannot resolve function group for FM "${objName}" in batch_create activation step. Provide "group" on the FUNC entry.`);
|
|
480
|
+
}
|
|
481
|
+
group = resolved;
|
|
482
|
+
}
|
|
483
|
+
const groupLc = encodeURIComponent(group.toLowerCase());
|
|
484
|
+
activationUrl = `/sap/bc/adt/functions/groups/${groupLc}/fmodules/${encodeURIComponent(objName.toLowerCase())}`;
|
|
485
|
+
}
|
|
486
|
+
if (activateAtEnd) {
|
|
487
|
+
// Step 3 deferred: track this object for the terminal activateBatch call.
|
|
488
|
+
// Cache invalidation also moves to AFTER the terminal activate succeeds —
|
|
489
|
+
// invalidating now would let the next read see a draft we couldn't activate.
|
|
490
|
+
writtenObjects.push({ type: objType, name: objName, url: activationUrl });
|
|
491
|
+
results.push({ type: objType, name: objName, packageName: objPackage, status: 'success' });
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
// Step 3: Activate the object (inline, default behavior).
|
|
495
|
+
const activationResult = await activate(client.http, client.safety, activationUrl);
|
|
496
|
+
if (!activationResult.success) {
|
|
497
|
+
results.push({
|
|
498
|
+
type: objType,
|
|
499
|
+
name: objName,
|
|
500
|
+
packageName: objPackage,
|
|
501
|
+
status: 'failed',
|
|
502
|
+
error: `activation failed: ${activationResult.messages.join('; ')}`,
|
|
503
|
+
});
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
invalidateWrittenObject(objType, objName);
|
|
507
|
+
results.push({ type: objType, name: objName, packageName: objPackage, status: 'success' });
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
catch (err) {
|
|
511
|
+
results.push({
|
|
512
|
+
type: objType,
|
|
513
|
+
name: objName,
|
|
514
|
+
packageName: objPackage,
|
|
515
|
+
status: 'failed',
|
|
516
|
+
error: err instanceof Error ? err.message : String(err),
|
|
517
|
+
});
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// Add 'skipped' entries for objects that were never attempted due to early break
|
|
522
|
+
for (let i = results.length; i < objects.length; i++) {
|
|
523
|
+
const skippedPlan = batchPlan[i];
|
|
524
|
+
const skipped = skippedPlan?.obj ?? objects[i];
|
|
525
|
+
results.push({
|
|
526
|
+
type: skippedPlan?.type ?? normalizeObjectType(String(skipped?.type ?? '')),
|
|
527
|
+
name: skippedPlan?.name ?? String(skipped?.name ?? ''),
|
|
528
|
+
packageName: skippedPlan?.packageName ?? normalizePackageOverride(skipped?.package, defaultPackage),
|
|
529
|
+
status: 'failed',
|
|
530
|
+
error: 'skipped — stopped after previous failure',
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
// ── Terminal activateBatch (activateAtEnd=true) ─────────────────────
|
|
534
|
+
// After every write-phase succeeded (or broke off early), issue ONE batch
|
|
535
|
+
// activate over the already-written subset. This is the killer feature
|
|
536
|
+
// for composition-linked DDLS and RAP behavior stacks — SAP's activator
|
|
537
|
+
// sees the whole graph in a single POST and resolves cross-references
|
|
538
|
+
// internally, so parent → child siblings activate cleanly.
|
|
539
|
+
let terminalActivationFailure;
|
|
540
|
+
if (activateAtEnd && writtenObjects.length > 0) {
|
|
541
|
+
const activationOutcome = await activateBatch(client.http, client.safety, writtenObjects);
|
|
542
|
+
if (activationOutcome.success) {
|
|
543
|
+
// Defensive: per-object status was already 'success' from the write phase.
|
|
544
|
+
// Cache invalidation moves here so a failed terminal activate doesn't strand
|
|
545
|
+
// a stale 'active' cache entry. Invalidate inactive-lists once for the user.
|
|
546
|
+
for (const o of writtenObjects) {
|
|
547
|
+
cachingLayer?.invalidate(o.type, o.name, 'all');
|
|
548
|
+
}
|
|
549
|
+
invalidateInactiveList(cachingLayer, client, cacheSecurity);
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
// Flip every written-but-not-yet-activated entry to 'failed', preserving the
|
|
553
|
+
// "create + source-write succeeded" context. Reuse the existing per-object
|
|
554
|
+
// diagnostic mapper so callers see the activation messages keyed by object name.
|
|
555
|
+
const batchStatuses = buildBatchActivationStatuses(writtenObjects, activationOutcome);
|
|
556
|
+
const statusDetails = formatBatchActivationStatuses(batchStatuses);
|
|
557
|
+
terminalActivationFailure = statusDetails;
|
|
558
|
+
const statusByName = new Map(batchStatuses.map((s) => [`${s.type}\x00${s.name}`, s]));
|
|
559
|
+
for (const result of results) {
|
|
560
|
+
if (result.status !== 'success')
|
|
561
|
+
continue;
|
|
562
|
+
const key = `${result.type}\x00${result.name}`;
|
|
563
|
+
const matched = statusByName.get(key);
|
|
564
|
+
if (!matched)
|
|
565
|
+
continue;
|
|
566
|
+
// Some entries may still report status 'active' if the activator returned
|
|
567
|
+
// success: false but had no per-object error details — keep them as 'success'.
|
|
568
|
+
if (matched.status === 'active')
|
|
569
|
+
continue;
|
|
570
|
+
result.status = 'failed';
|
|
571
|
+
const detail = matched.messages.length > 0 ? ` — ${matched.messages.join('; ')}` : '';
|
|
572
|
+
// Preserve the "create + source-write succeeded" context so the user sees that
|
|
573
|
+
// the failure was specifically the activation step, not the write step.
|
|
574
|
+
result.error = `${writtenObjects.length}/${writtenObjects.length} written, batch activation failed${detail}`;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
// ────────────────────────────────────────────────────────────────────
|
|
579
|
+
const summary = results
|
|
580
|
+
.map((r) => r.status === 'success'
|
|
581
|
+
? `${r.name} (${r.type}) ✓ [${r.packageName}]`
|
|
582
|
+
: `${r.name} (${r.type}) ✗ [${r.packageName}] — ${r.error}`)
|
|
583
|
+
.join(', ');
|
|
584
|
+
const successCount = results.filter((r) => r.status === 'success').length;
|
|
585
|
+
const hasFailure = results.some((r) => r.status === 'failed');
|
|
586
|
+
const warningSuffix = batchWarnings.length > 0 ? `\n\nRAP preflight warnings:\n- ${batchWarnings.join('\n- ')}` : '';
|
|
587
|
+
const activateAtEndSuffix = terminalActivationFailure !== undefined ? `\n\nBatch activation diagnostics:${terminalActivationFailure}` : '';
|
|
588
|
+
const packageNames = [...new Set(batchPlan.map((item) => item.packageName))];
|
|
589
|
+
const packageSummary = packageNames.length === 1
|
|
590
|
+
? `in package ${packageNames[0]}`
|
|
591
|
+
: packageNames.length <= 3
|
|
592
|
+
? `across packages [${packageNames.join(', ')}]`
|
|
593
|
+
: `across ${packageNames.length} packages`;
|
|
594
|
+
const activateAtEndPrefix = activateAtEnd ? '; activated as a single batch' : '';
|
|
595
|
+
if (hasFailure) {
|
|
596
|
+
const cleanupHint = successCount > 0
|
|
597
|
+
? ` Note: ${successCount} already-created object(s) remain on the SAP system and may need manual cleanup.`
|
|
598
|
+
: '';
|
|
599
|
+
return errorResult(`Batch created ${successCount}/${objects.length} objects ${packageSummary}${activateAtEndPrefix}: ${summary}${cleanupHint}${warningSuffix}${activateAtEndSuffix}`);
|
|
600
|
+
}
|
|
601
|
+
return textResult(`Batch created ${successCount} objects ${packageSummary}${activateAtEndPrefix}: ${summary}${warningSuffix}${activateAtEndSuffix}`);
|
|
602
|
+
}
|
|
603
|
+
//# sourceMappingURL=create.js.map
|