@mcp-abap-adt/adt-backup 1.1.0 → 1.3.0

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.
Files changed (45) hide show
  1. package/README.md +6 -0
  2. package/dist/lib/backup/readBasicMetadata.d.ts.map +1 -1
  3. package/dist/lib/backup/readBasicMetadata.js +7 -0
  4. package/dist/lib/backup/readMetadataXmlForType.d.ts.map +1 -1
  5. package/dist/lib/backup/readMetadataXmlForType.js +37 -17
  6. package/dist/lib/backup/readSourceText.d.ts.map +1 -1
  7. package/dist/lib/backup/readSourceText.js +6 -0
  8. package/dist/lib/constants/typeOrder.d.ts.map +1 -1
  9. package/dist/lib/constants/typeOrder.js +1 -0
  10. package/dist/lib/dependencies/collectTreeDependencies.d.ts.map +1 -1
  11. package/dist/lib/dependencies/collectTreeDependencies.js +1 -0
  12. package/dist/lib/restore/analyzeDependencies.d.ts +6 -0
  13. package/dist/lib/restore/analyzeDependencies.d.ts.map +1 -1
  14. package/dist/lib/restore/analyzeDependencies.js +127 -28
  15. package/dist/lib/restore/restoreObject.d.ts.map +1 -1
  16. package/dist/lib/restore/restoreObject.js +20 -0
  17. package/dist/lib/restore/restoreObjects.d.ts.map +1 -1
  18. package/dist/lib/restore/restoreObjects.js +1 -0
  19. package/dist/lib/restore/restoreTreeBackup.d.ts +2 -2
  20. package/dist/lib/restore/restoreTreeBackup.d.ts.map +1 -1
  21. package/dist/lib/restore/restoreTreeBackup.js +229 -97
  22. package/dist/lib/restore/restoreTreeNode.d.ts.map +1 -1
  23. package/dist/lib/restore/restoreTreeNode.js +17 -0
  24. package/dist/lib/run.d.ts.map +1 -1
  25. package/dist/lib/run.js +103 -44
  26. package/dist/lib/tree/isRestoreImplemented.d.ts.map +1 -1
  27. package/dist/lib/tree/isRestoreImplemented.js +1 -0
  28. package/dist/lib/tree/mapAdtTypeToSupported.d.ts.map +1 -1
  29. package/dist/lib/tree/mapAdtTypeToSupported.js +4 -0
  30. package/dist/lib/tree/readPayloadForType.d.ts.map +1 -1
  31. package/dist/lib/tree/readPayloadForType.js +1 -0
  32. package/dist/lib/types.d.ts +1 -1
  33. package/dist/lib/types.d.ts.map +1 -1
  34. package/dist/lib/utils/applyConfigName.d.ts.map +1 -1
  35. package/dist/lib/utils/applyConfigName.js +3 -0
  36. package/dist/lib/utils/detectTransformationType.d.ts +3 -0
  37. package/dist/lib/utils/detectTransformationType.d.ts.map +1 -0
  38. package/dist/lib/utils/detectTransformationType.js +9 -0
  39. package/dist/lib/utils/normalizeType.d.ts.map +1 -1
  40. package/dist/lib/utils/normalizeType.js +4 -0
  41. package/dist/lib/verify/findOtherType.d.ts.map +1 -1
  42. package/dist/lib/verify/findOtherType.js +1 -0
  43. package/dist/lib/xml/parseServiceBindingConfig.d.ts.map +1 -1
  44. package/dist/lib/xml/parseServiceBindingConfig.js +24 -5
  45. package/package.json +10 -10
@@ -1,11 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.restoreTreeBackup = restoreTreeBackup;
4
+ const fast_xml_parser_1 = require("fast-xml-parser");
4
5
  const logVerbose_1 = require("../cli/logVerbose");
5
6
  const flattenTree_1 = require("../tree/flattenTree");
6
7
  const getNodeObjectId_1 = require("../tree/getNodeObjectId");
7
8
  const analyzeDependencies_1 = require("./analyzeDependencies");
8
9
  const restoreTreeNode_1 = require("./restoreTreeNode");
10
+ const xmlParser = new fast_xml_parser_1.XMLParser({
11
+ ignoreAttributes: false,
12
+ attributeNamePrefix: '@_',
13
+ parseAttributeValue: false,
14
+ });
9
15
  const RESTORE_PHASES = [
10
16
  { name: 'Domains', types: ['domain'], activation: 'individual' },
11
17
  { name: 'Data Elements', types: ['dataElement'], activation: 'individual' },
@@ -21,6 +27,11 @@ const RESTORE_PHASES = [
21
27
  { name: 'Classes', types: ['class'], activation: 'individual' },
22
28
  { name: 'Interfaces', types: ['interface'], activation: 'individual' },
23
29
  { name: 'Programs', types: ['program'], activation: 'individual' },
30
+ {
31
+ name: 'Transformations',
32
+ types: ['transformation'],
33
+ activation: 'individual',
34
+ },
24
35
  {
25
36
  name: 'Function Groups',
26
37
  types: ['functionGroup'],
@@ -45,7 +56,7 @@ const RESTORE_PHASES = [
45
56
  { name: 'Service Bindings', types: ['serviceBinding'], activation: 'bulk' },
46
57
  { name: 'Enhancements', types: ['enhancement'], activation: 'individual' },
47
58
  ];
48
- async function restoreTreeBackup(client, root, mode, activate, transportRequest, restoreIds, restoreActions, activateOnCreate = true, softwareComponent, superPackageOverride, transportLayer) {
59
+ async function restoreTreeBackup(client, root, mode, activate, transportRequest, restoreIds, planGroups, activateOnCreate = true, softwareComponent, superPackageOverride, transportLayer) {
49
60
  const allNodes = (0, flattenTree_1.flattenTree)(root).filter((node) => node.type && node.restoreStatus === 'ok');
50
61
  const nodes = restoreIds
51
62
  ? allNodes.filter((node) => {
@@ -57,68 +68,94 @@ async function restoreTreeBackup(client, root, mode, activate, transportRequest,
57
68
  const nonPackageNodes = nodes.filter((node) => node.type !== 'package');
58
69
  const backupPackageNames = new Set(packageNodes.map((node) => node.name));
59
70
  const rootPackageName = root.name;
60
- (0, logVerbose_1.logVerbose)(1, `\n>>> STARTING TYPE-PHASE RESTORE: ${nodes.length} objects`);
61
- const failures = [];
62
- // Phase 1: Packages (recursive hierarchy)
63
- if (packageNodes.length > 0) {
64
- (0, logVerbose_1.logVerbose)(1, '[PHASE 1] Restoring package hierarchy...');
65
- const restorePackageRecursive = async (node, parentName) => {
66
- const nodeId = (0, getNodeObjectId_1.getNodeObjectId)(node);
67
- if (node.type === 'package' &&
68
- nodeId &&
69
- (!restoreIds || restoreIds.has(nodeId))) {
70
- const isRootNode = node.name === rootPackageName;
71
- const nodeMode = (restoreActions?.get(nodeId) || mode);
72
- const effectiveMode = isRootNode ? 'update' : nodeMode;
73
- if (effectiveMode === 'skip') {
74
- (0, logVerbose_1.logVerbose)(2, ` [SKIP] package:${node.name}`);
75
- }
76
- else {
77
- (0, logVerbose_1.logVerbose)(2, ` [PACKAGE] ${node.name}`);
78
- try {
79
- await (0, restoreTreeNode_1.restoreTreeNode)(client, node, effectiveMode, false, transportRequest, softwareComponent, backupPackageNames, parentName || superPackageOverride, transportLayer);
80
- }
81
- catch (e) {
82
- if (isRootNode) {
83
- (0, logVerbose_1.logVerbose)(1, ` ! Warning: Root package ${node.name} already exists or update skipped.`);
84
- }
85
- else {
86
- throw e;
87
- }
88
- }
89
- }
90
- }
91
- if (node.children) {
92
- for (const child of node.children) {
93
- await restorePackageRecursive(child, node.type === 'package' ? node.name : parentName);
94
- }
95
- }
96
- };
97
- await restorePackageRecursive(root, undefined);
71
+ // Build restoreActions map from planGroups (if provided) for mode lookups
72
+ const restoreActions = planGroups
73
+ ? new Map(planGroups
74
+ .flatMap((g) => g.actions)
75
+ .map((a) => [a.id, a.action]))
76
+ : undefined;
77
+ // Build nodeMap for quick lookup by objectId (used in plan-driven path)
78
+ const nodeMap = new Map();
79
+ for (const node of allNodes) {
80
+ const id = (0, getNodeObjectId_1.getNodeObjectId)(node);
81
+ if (id)
82
+ nodeMap.set(id, node);
98
83
  }
99
- // Phase 2: Analyze dependencies for processing order
100
- (0, logVerbose_1.logVerbose)(1, `[PHASE 2] Analyzing dependencies for ${nonPackageNodes.length} objects...`);
101
- const restoreGroups = (0, analyzeDependencies_1.analyzeDependencies)(nonPackageNodes);
102
- const orderedNodes = restoreGroups.flatMap((g) => g.nodes);
103
- (0, logVerbose_1.logVerbose)(1, `Dependency analysis complete: ${restoreGroups.length} groups → ${orderedNodes.length} ordered nodes.`);
104
- // Helper to bulk activate a list of refs
84
+ const failures = [];
85
+ // Helper: check which of our refs are still inactive
86
+ const findInactiveRefs = async (refs) => {
87
+ const result = await client.getUtils().getInactiveObjects();
88
+ const inactiveSet = new Set(result.objects.map((o) => `${o.type}:${o.name}`.toUpperCase()));
89
+ return refs.filter((r) => inactiveSet.has(`${r.type}:${r.name}`.toUpperCase()));
90
+ };
91
+ // Helper to bulk activate a list of refs with verification
105
92
  const bulkActivate = async (phaseName, refs) => {
106
93
  if (refs.length === 0)
107
94
  return;
108
- (0, logVerbose_1.logVerbose)(2, ` [*] Bulk activating ${phaseName} (${refs.length} objects)...`);
95
+ // Check which objects are actually inactive
96
+ let toActivate = await findInactiveRefs(refs);
97
+ if (toActivate.length === 0) {
98
+ (0, logVerbose_1.logVerbose)(2, ` [*] ${phaseName}: all ${refs.length} objects already active`);
99
+ return;
100
+ }
101
+ (0, logVerbose_1.logVerbose)(2, ` [*] Bulk activating ${phaseName} (${toActivate.length}/${refs.length} inactive)...`);
102
+ let hasErrors = false;
109
103
  try {
110
- await client.getUtils().activateObjectsGroup(refs, true);
104
+ const result = await client
105
+ .getUtils()
106
+ .activateObjectsGroup(toActivate, true);
107
+ if (result?.data) {
108
+ const parsed = xmlParser.parse(typeof result.data === 'string' ? result.data : String(result.data));
109
+ const msgs = parsed?.['chkl:messages']?.msg;
110
+ if (msgs) {
111
+ const msgArray = Array.isArray(msgs) ? msgs : [msgs];
112
+ for (const msg of msgArray) {
113
+ const type = msg['@_type'] || 'info';
114
+ const text = msg?.shortText?.txt || msg?.shortText || String(msg);
115
+ if (type === 'E')
116
+ hasErrors = true;
117
+ (0, logVerbose_1.logVerbose)(2, ` [${type}] ${text}`);
118
+ }
119
+ }
120
+ }
111
121
  }
112
122
  catch (error) {
123
+ hasErrors = true;
113
124
  const message = error instanceof Error ? error.message : String(error);
114
- (0, logVerbose_1.logVerbose)(1, ` [!] WARNING: ${phaseName} activation failed: ${message}`);
115
- (0, logVerbose_1.logVerbose)(1, ' Objects are created but might be inactive. Continuing...');
125
+ (0, logVerbose_1.logVerbose)(2, ` [*] Activation request completed (${message})`);
126
+ }
127
+ // Verify: poll until our objects are no longer inactive (max 5 retries, 10s apart)
128
+ // Skip polling if activation returned errors
129
+ if (hasErrors) {
130
+ const stillInactive = await findInactiveRefs(refs);
131
+ if (stillInactive.length > 0) {
132
+ (0, logVerbose_1.logVerbose)(1, ` [!] WARNING: ${phaseName}: ${stillInactive.length} object(s) remain inactive:`);
133
+ for (const ref of stillInactive) {
134
+ (0, logVerbose_1.logVerbose)(1, ` - ${ref.type}:${ref.name}`);
135
+ }
136
+ }
137
+ return;
138
+ }
139
+ const maxRetries = 5;
140
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
141
+ const stillInactive = await findInactiveRefs(refs);
142
+ if (stillInactive.length === 0) {
143
+ (0, logVerbose_1.logVerbose)(2, ` [*] ${phaseName}: all objects activated successfully`);
144
+ return;
145
+ }
146
+ if (attempt < maxRetries) {
147
+ (0, logVerbose_1.logVerbose)(2, ` [*] ${stillInactive.length} object(s) still inactive, waiting... (${attempt}/${maxRetries})`);
148
+ await new Promise((resolve) => setTimeout(resolve, 10000));
149
+ }
150
+ else {
151
+ (0, logVerbose_1.logVerbose)(1, ` [!] WARNING: ${phaseName}: ${stillInactive.length} object(s) remain inactive:`);
152
+ for (const ref of stillInactive) {
153
+ (0, logVerbose_1.logVerbose)(1, ` - ${ref.type}:${ref.name}`);
154
+ }
155
+ }
116
156
  }
117
157
  };
118
- // Phase 3: Process by granular type phases with per-type activation strategy
119
- const knownTypes = new Set(RESTORE_PHASES.flatMap((p) => p.types));
120
- const uncategorizedNodes = orderedNodes.filter((n) => n.type && !knownTypes.has(n.type) && n.type !== 'package');
121
- // Helper: process a single node (create/update without activation)
158
+ // Helper: process a single node (create/update)
122
159
  const processNode = async (node, activateFlag) => {
123
160
  const nodeId = (0, getNodeObjectId_1.getNodeObjectId)(node);
124
161
  if (!nodeId)
@@ -148,64 +185,159 @@ async function restoreTreeBackup(client, root, mode, activate, transportRequest,
148
185
  }
149
186
  return null;
150
187
  };
151
- const allProcessedRefs = [];
152
- for (const phase of RESTORE_PHASES) {
153
- const phaseTypeSet = new Set(phase.types);
154
- const phaseNodes = orderedNodes.filter((n) => n.type && phaseTypeSet.has(n.type));
155
- if (phaseNodes.length === 0)
156
- continue;
157
- (0, logVerbose_1.logVerbose)(1, `[${phase.name.toUpperCase()}] Processing ${phaseNodes.length} object(s) (${phase.activation})...`);
158
- if (phase.activation === 'individual') {
159
- // Activate each object at creation/update time
160
- for (const node of phaseNodes) {
161
- const ref = await processNode(node, true);
162
- if (ref)
163
- allProcessedRefs.push(ref);
188
+ // Phase 1: Packages (recursive hierarchy) — shared by both paths
189
+ if (packageNodes.length > 0) {
190
+ (0, logVerbose_1.logVerbose)(1, '[PHASE 1] Restoring package hierarchy...');
191
+ const restorePackageRecursive = async (node, parentName) => {
192
+ const nodeId = (0, getNodeObjectId_1.getNodeObjectId)(node);
193
+ if (node.type === 'package' &&
194
+ nodeId &&
195
+ (!restoreIds || restoreIds.has(nodeId))) {
196
+ const isRootNode = node.name === rootPackageName;
197
+ const nodeMode = (restoreActions?.get(nodeId) || mode);
198
+ const effectiveMode = isRootNode ? 'update' : nodeMode;
199
+ if (effectiveMode === 'skip') {
200
+ (0, logVerbose_1.logVerbose)(2, ` [SKIP] package:${node.name}`);
201
+ }
202
+ else {
203
+ (0, logVerbose_1.logVerbose)(2, ` [PACKAGE] ${node.name}`);
204
+ try {
205
+ await (0, restoreTreeNode_1.restoreTreeNode)(client, node, effectiveMode, false, transportRequest, softwareComponent, backupPackageNames, parentName || superPackageOverride, transportLayer);
206
+ }
207
+ catch (e) {
208
+ if (isRootNode) {
209
+ (0, logVerbose_1.logVerbose)(1, ` ! Warning: Root package ${node.name} already exists or update skipped.`);
210
+ }
211
+ else {
212
+ throw e;
213
+ }
214
+ }
215
+ }
164
216
  }
165
- }
166
- else if (phase.activation === 'bulk') {
167
- // Create/update all without activation, then bulk activate together
168
- const refs = [];
169
- for (const node of phaseNodes) {
217
+ if (node.children) {
218
+ for (const child of node.children) {
219
+ await restorePackageRecursive(child, node.type === 'package' ? node.name : parentName);
220
+ }
221
+ }
222
+ };
223
+ await restorePackageRecursive(root, undefined);
224
+ }
225
+ const allProcessedRefs = [];
226
+ if (planGroups) {
227
+ // ===== Plan-driven restore: follow plan group order =====
228
+ (0, logVerbose_1.logVerbose)(1, `\n>>> PLAN-DRIVEN RESTORE: ${planGroups.length} groups`);
229
+ for (const group of planGroups) {
230
+ const nonPackageActions = group.actions.filter((a) => a.type !== 'package');
231
+ if (nonPackageActions.length === 0)
232
+ continue;
233
+ (0, logVerbose_1.logVerbose)(1, `[GROUP ${group.id}] ${nonPackageActions.length} object(s)${group.isCircular ? ' (circular)' : ''}`);
234
+ const groupRefs = [];
235
+ for (const action of nonPackageActions) {
236
+ if (action.action === 'skip') {
237
+ (0, logVerbose_1.logVerbose)(2, ` [SKIP] ${action.type}:${action.name}`);
238
+ continue;
239
+ }
240
+ const node = nodeMap.get(action.id);
241
+ if (!node) {
242
+ (0, logVerbose_1.logVerbose)(1, ` [WARN] Node not found for ${action.id}, skipping`);
243
+ continue;
244
+ }
170
245
  const ref = await processNode(node, false);
171
246
  if (ref)
172
- refs.push(ref);
247
+ groupRefs.push(ref);
173
248
  }
174
- await bulkActivate(phase.name, refs);
175
- allProcessedRefs.push(...refs);
249
+ if (groupRefs.length > 0) {
250
+ await bulkActivate(`Group ${group.id}${group.isCircular ? ' (circular)' : ''}`, groupRefs);
251
+ }
252
+ allProcessedRefs.push(...groupRefs);
176
253
  }
177
- else if (phase.activation === 'cluster') {
178
- // Cluster by dependencies (SCC groups), bulk activate per cluster
179
- const groups = (0, analyzeDependencies_1.analyzeDependencies)(phaseNodes);
180
- (0, logVerbose_1.logVerbose)(2, ` Dependency clustering: ${groups.length} cluster(s)`);
181
- for (let gi = 0; gi < groups.length; gi++) {
182
- const group = groups[gi];
183
- const clusterRefs = [];
184
- for (const node of group.nodes) {
254
+ }
255
+ else {
256
+ // ===== Fallback: type-phase restore (no plan) =====
257
+ (0, logVerbose_1.logVerbose)(1, `\n>>> STARTING TYPE-PHASE RESTORE: ${nodes.length} objects`);
258
+ (0, logVerbose_1.logVerbose)(1, `[PHASE 2] Analyzing dependencies for ${nonPackageNodes.length} objects...`);
259
+ const restoreGroups = (0, analyzeDependencies_1.analyzeDependencies)(nonPackageNodes);
260
+ const orderedNodes = restoreGroups.flatMap((g) => g.nodes);
261
+ (0, logVerbose_1.logVerbose)(1, `Dependency analysis complete: ${restoreGroups.length} groups → ${orderedNodes.length} ordered nodes.`);
262
+ const knownTypes = new Set(RESTORE_PHASES.flatMap((p) => p.types));
263
+ const uncategorizedNodes = orderedNodes.filter((n) => n.type && !knownTypes.has(n.type) && n.type !== 'package');
264
+ for (const phase of RESTORE_PHASES) {
265
+ const phaseTypeSet = new Set(phase.types);
266
+ const phaseNodes = orderedNodes.filter((n) => n.type && phaseTypeSet.has(n.type));
267
+ if (phaseNodes.length === 0)
268
+ continue;
269
+ (0, logVerbose_1.logVerbose)(1, `[${phase.name.toUpperCase()}] Processing ${phaseNodes.length} object(s) (${phase.activation})...`);
270
+ if (phase.activation === 'individual') {
271
+ for (const node of phaseNodes) {
272
+ const ref = await processNode(node, true);
273
+ if (ref)
274
+ allProcessedRefs.push(ref);
275
+ }
276
+ }
277
+ else if (phase.activation === 'bulk') {
278
+ const refs = [];
279
+ for (const node of phaseNodes) {
185
280
  const ref = await processNode(node, false);
186
281
  if (ref)
187
- clusterRefs.push(ref);
282
+ refs.push(ref);
188
283
  }
189
- if (clusterRefs.length > 0) {
190
- await bulkActivate(`${phase.name} cluster ${gi + 1}/${groups.length}${group.isCircular ? ' (circular)' : ''}`, clusterRefs);
284
+ await bulkActivate(phase.name, refs);
285
+ allProcessedRefs.push(...refs);
286
+ }
287
+ else if (phase.activation === 'cluster') {
288
+ const groups = (0, analyzeDependencies_1.analyzeDependencies)(phaseNodes);
289
+ (0, logVerbose_1.logVerbose)(2, ` Dependency clustering: ${groups.length} cluster(s)`);
290
+ for (let gi = 0; gi < groups.length; gi++) {
291
+ const group = groups[gi];
292
+ const clusterRefs = [];
293
+ for (const node of group.nodes) {
294
+ const ref = await processNode(node, false);
295
+ if (ref)
296
+ clusterRefs.push(ref);
297
+ }
298
+ if (clusterRefs.length > 0) {
299
+ await bulkActivate(`${phase.name} cluster ${gi + 1}/${groups.length}${group.isCircular ? ' (circular)' : ''}`, clusterRefs);
300
+ }
301
+ allProcessedRefs.push(...clusterRefs);
191
302
  }
192
- allProcessedRefs.push(...clusterRefs);
193
303
  }
194
304
  }
195
- }
196
- // Uncategorized types (future types not in RESTORE_PHASES) individual activation
197
- if (uncategorizedNodes.length > 0) {
198
- (0, logVerbose_1.logVerbose)(1, `[OTHER] Processing ${uncategorizedNodes.length} uncategorized object(s) (individual)...`);
199
- for (const node of uncategorizedNodes) {
200
- const ref = await processNode(node, true);
201
- if (ref)
202
- allProcessedRefs.push(ref);
305
+ if (uncategorizedNodes.length > 0) {
306
+ (0, logVerbose_1.logVerbose)(1, `[OTHER] Processing ${uncategorizedNodes.length} uncategorized object(s) (individual)...`);
307
+ for (const node of uncategorizedNodes) {
308
+ const ref = await processNode(node, true);
309
+ if (ref)
310
+ allProcessedRefs.push(ref);
311
+ }
203
312
  }
204
313
  }
205
- // Final activation sweep safety net for objects left inactive
314
+ // Final check: find remaining inactive objects and activate them
206
315
  if (allProcessedRefs.length > 0) {
207
- (0, logVerbose_1.logVerbose)(1, `[FINAL] Activation sweep (${allProcessedRefs.length} objects)...`);
208
- await bulkActivate('Final sweep', allProcessedRefs);
316
+ const stillInactive = await findInactiveRefs(allProcessedRefs);
317
+ if (stillInactive.length > 0) {
318
+ (0, logVerbose_1.logVerbose)(1, `[FINAL] ${stillInactive.length} object(s) still inactive, activating...`);
319
+ try {
320
+ await client.getUtils().activateObjectsGroup(stillInactive, true);
321
+ }
322
+ catch (error) {
323
+ const message = error instanceof Error ? error.message : String(error);
324
+ (0, logVerbose_1.logVerbose)(2, ` [*] Final activation request completed (${message})`);
325
+ }
326
+ // Verify final state
327
+ const remaining = await findInactiveRefs(allProcessedRefs);
328
+ if (remaining.length > 0) {
329
+ (0, logVerbose_1.logVerbose)(1, ` [!] ${remaining.length} object(s) remain inactive:`);
330
+ for (const ref of remaining) {
331
+ (0, logVerbose_1.logVerbose)(1, ` - ${ref.type}:${ref.name}`);
332
+ }
333
+ }
334
+ else {
335
+ (0, logVerbose_1.logVerbose)(1, '[FINAL] All objects activated successfully.');
336
+ }
337
+ }
338
+ else {
339
+ (0, logVerbose_1.logVerbose)(1, '[FINAL] All objects are active.');
340
+ }
209
341
  }
210
342
  if (failures.length > 0) {
211
343
  (0, logVerbose_1.logVerbose)(1, `\n>>> RESTORE COMPLETED WITH ${failures.length} FAILURE(S):`);
@@ -1 +1 @@
1
- {"version":3,"file":"restoreTreeNode.d.ts","sourceRoot":"","sources":["../../../src/lib/restore/restoreTreeNode.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EAoBV,MAAM,2BAA2B,CAAC;AAGnC,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAO5D,wBAAsB,eAAe,CACnC,MAAM,EAAE,SAAS,EACjB,IAAI,EAAE,cAAc,EACpB,IAAI,EAAE,WAAW,EACjB,QAAQ,EAAE,OAAO,EACjB,gBAAgB,CAAC,EAAE,MAAM,EACzB,yBAAyB,CAAC,EAAE,MAAM,EAClC,kBAAkB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,EAChC,oBAAoB,CAAC,EAAE,MAAM,EAC7B,sBAAsB,CAAC,EAAE,MAAM,GAC9B,OAAO,CAAC,IAAI,CAAC,CA8Zf"}
1
+ {"version":3,"file":"restoreTreeNode.d.ts","sourceRoot":"","sources":["../../../src/lib/restore/restoreTreeNode.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EAqBV,MAAM,2BAA2B,CAAC;AAGnC,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAQ5D,wBAAsB,eAAe,CACnC,MAAM,EAAE,SAAS,EACjB,IAAI,EAAE,cAAc,EACpB,IAAI,EAAE,WAAW,EACjB,QAAQ,EAAE,OAAO,EACjB,gBAAgB,CAAC,EAAE,MAAM,EACzB,yBAAyB,CAAC,EAAE,MAAM,EAClC,kBAAkB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,EAChC,oBAAoB,CAAC,EAAE,MAAM,EAC7B,sBAAsB,CAAC,EAAE,MAAM,GAC9B,OAAO,CAAC,IAAI,CAAC,CAibf"}
@@ -4,6 +4,7 @@ exports.restoreTreeNode = restoreTreeNode;
4
4
  const logVerbose_1 = require("../cli/logVerbose");
5
5
  const decodeBase64_1 = require("../crypto/decodeBase64");
6
6
  const asConfig_1 = require("../utils/asConfig");
7
+ const detectTransformationType_1 = require("../utils/detectTransformationType");
7
8
  const ensureDescription_1 = require("../utils/ensureDescription");
8
9
  const parseBdefSource_1 = require("../utils/parseBdefSource");
9
10
  const parsePackageConfig_1 = require("../xml/parsePackageConfig");
@@ -190,6 +191,22 @@ async function restoreTreeNode(client, node, mode, activate, transportRequest, s
190
191
  }
191
192
  return;
192
193
  }
194
+ case 'transformation': {
195
+ const transformationType = (0, detectTransformationType_1.detectTransformationType)(payload);
196
+ const baseTxConfig = { ...config, transformationType };
197
+ if (mode !== 'update') {
198
+ await client
199
+ .getTransformation()
200
+ .create((0, asConfig_1.asConfig)(baseTxConfig), options);
201
+ }
202
+ if (payload) {
203
+ await client.getTransformation().update((0, asConfig_1.asConfig)({
204
+ ...baseTxConfig,
205
+ sourceCode: payload,
206
+ }), options);
207
+ }
208
+ return;
209
+ }
193
210
  case 'functionGroup': {
194
211
  if (mode !== 'update') {
195
212
  await client
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/lib/run.ts"],"names":[],"mappings":"AAiDA,wBAAsB,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAgpBzC"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/lib/run.ts"],"names":[],"mappings":"AAyDA,wBAAsB,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAytBzC"}
package/dist/lib/run.js CHANGED
@@ -41,6 +41,7 @@ const fs = __importStar(require("node:fs"));
41
41
  const path = __importStar(require("node:path"));
42
42
  const adt_clients_1 = require("@mcp-abap-adt/adt-clients");
43
43
  const connection_1 = require("@mcp-abap-adt/connection");
44
+ const fast_xml_parser_1 = require("fast-xml-parser");
44
45
  const yaml_1 = __importDefault(require("yaml"));
45
46
  const getSapConfigFromBroker_1 = require("./auth/getSapConfigFromBroker");
46
47
  const backupObject_1 = require("./backup/backupObject");
@@ -76,6 +77,11 @@ const parseObjectSpec_1 = require("./utils/parseObjectSpec");
76
77
  const formatVerifyResultsText_1 = require("./verify/formatVerifyResultsText");
77
78
  const verifyBackup_1 = require("./verify/verifyBackup");
78
79
  const extractMetadata_1 = require("./xml/extractMetadata");
80
+ const xmlParser = new fast_xml_parser_1.XMLParser({
81
+ ignoreAttributes: false,
82
+ attributeNamePrefix: '@_',
83
+ parseAttributeValue: false,
84
+ });
79
85
  async function run() {
80
86
  const argv = process.argv.slice(2);
81
87
  const args = (0, parseArgs_1.parseArgs)(argv.slice(1));
@@ -272,7 +278,20 @@ async function run() {
272
278
  const allNodes = (0, flattenTree_1.flattenTree)(backup.root).filter((n) => n.type && n.restoreStatus === 'ok');
273
279
  const packageNodes = allNodes.filter((n) => n.type === 'package');
274
280
  const nonPackageNodes = allNodes.filter((n) => n.type !== 'package');
275
- const groups = (0, analyzeDependencies_1.analyzeDependencies)(nonPackageNodes);
281
+ // Dependency-based grouping: SCCs merged by dependency level
282
+ const depGroups = (0, analyzeDependencies_1.analyzeDependencyLevels)(nonPackageNodes);
283
+ const nonPackageGroups = depGroups.map((group, i) => ({
284
+ id: i + 1,
285
+ isCircular: group.isCircular,
286
+ actions: group.nodes.map((node) => ({
287
+ id: (0, getNodeObjectId_1.getNodeObjectId)(node),
288
+ type: node.type,
289
+ name: node.name,
290
+ functionGroupName: node.functionGroupName,
291
+ action: mode,
292
+ adtType: node.adtType,
293
+ })),
294
+ }));
276
295
  const plan = {
277
296
  schemaVersion: 1,
278
297
  generatedAt: new Date().toISOString(),
@@ -290,18 +309,7 @@ async function run() {
290
309
  adtType: node.adtType,
291
310
  })),
292
311
  },
293
- ...groups.map((group, idx) => ({
294
- id: idx + 1,
295
- isCircular: group.isCircular,
296
- actions: group.nodes.map((node) => ({
297
- id: (0, getNodeObjectId_1.getNodeObjectId)(node),
298
- type: node.type,
299
- name: node.name,
300
- functionGroupName: node.functionGroupName,
301
- action: mode,
302
- adtType: node.adtType,
303
- })),
304
- })),
312
+ ...nonPackageGroups,
305
313
  ],
306
314
  };
307
315
  fs.writeFileSync(output, yaml_1.default.stringify(plan, { lineWidth: 0 }), 'utf8');
@@ -382,9 +390,7 @@ async function run() {
382
390
  const transportLayer = typeof args['transport-layer'] === 'string'
383
391
  ? args['transport-layer']
384
392
  : undefined;
385
- await (0, restoreTreeBackup_1.restoreTreeBackup)(client, backup.root, 'upsert', activate, typeof args.transport === 'string' ? args.transport : undefined, undefined, new Map(plan.groups
386
- .flatMap((g) => g.actions)
387
- .map((a) => [a.id, a.action])), activateOnCreate, typeof args['software-component'] === 'string'
393
+ await (0, restoreTreeBackup_1.restoreTreeBackup)(client, backup.root, 'upsert', activate, typeof args.transport === 'string' ? args.transport : undefined, undefined, plan.groups, activateOnCreate, typeof args['software-component'] === 'string'
388
394
  ? args['software-component']
389
395
  : undefined, superPackage, transportLayer);
390
396
  console.log('Restore completed. Running post-restore check on TARGET system...');
@@ -405,54 +411,107 @@ async function run() {
405
411
  throw new Error(`Invalid --filter value: "${filter}". Must be skip, update, or all.`);
406
412
  }
407
413
  const plan = yaml_1.default.parse(fs.readFileSync(planPath, 'utf8'));
408
- const allActions = plan.groups.flatMap((g) => g.actions);
409
- const filtered = allActions.filter((a) => {
410
- if (a.type === 'package')
411
- return false;
412
- if (!a.adtType)
413
- return false;
414
- if (filter === 'all')
415
- return a.action !== 'create';
416
- return a.action === filter;
417
- });
418
- if (filtered.length === 0) {
414
+ // Collect all plan refs (non-package, matching filter)
415
+ const planRefs = [];
416
+ for (const group of plan.groups) {
417
+ for (const action of group.actions) {
418
+ if (action.type === 'package' || !action.adtType)
419
+ continue;
420
+ if (filter !== 'all' && action.action !== filter)
421
+ continue;
422
+ if (filter === 'all' && action.action === 'create')
423
+ continue;
424
+ planRefs.push({ name: action.name, type: action.adtType });
425
+ }
426
+ }
427
+ if (planRefs.length === 0) {
419
428
  console.log('No objects to activate.');
420
429
  return;
421
430
  }
422
- console.log(`Activating ${filtered.length} object(s) (filter: ${filter})...`);
423
- // Group by plan groups to respect dependency order
431
+ // Check which plan objects are actually inactive
432
+ const inactiveResult = await client.getUtils().getInactiveObjects();
433
+ const inactiveSet = new Set(inactiveResult.objects.map((o) => `${o.type}:${o.name}`.toUpperCase()));
434
+ const toActivate = planRefs.filter((r) => inactiveSet.has(`${r.type}:${r.name}`.toUpperCase()));
435
+ const alreadyActive = planRefs.length - toActivate.length;
436
+ if (toActivate.length === 0) {
437
+ console.log(`All ${planRefs.length} object(s) are already active. Nothing to activate.`);
438
+ return;
439
+ }
440
+ console.log(`Found ${toActivate.length} inactive object(s) out of ${planRefs.length} (${alreadyActive} already active). Activating...`);
441
+ // Group inactive objects by plan groups to respect dependency order
424
442
  let activated = 0;
425
- let skipped = 0;
426
443
  let failed = 0;
427
444
  for (const group of plan.groups) {
428
445
  const groupRefs = [];
429
446
  for (const action of group.actions) {
430
447
  if (action.type === 'package' || !action.adtType)
431
448
  continue;
432
- if (filter !== 'all' && action.action !== filter)
433
- continue;
434
- if (filter === 'all' && action.action === 'create')
435
- continue;
436
- groupRefs.push({ name: action.name, type: action.adtType });
449
+ const key = `${action.adtType}:${action.name}`.toUpperCase();
450
+ if (inactiveSet.has(key)) {
451
+ groupRefs.push({ name: action.name, type: action.adtType });
452
+ }
437
453
  }
438
454
  if (groupRefs.length === 0)
439
455
  continue;
440
- (0, logVerbose_1.logVerbose)(2, ` [GROUP ${group.id}] Activating ${groupRefs.length} object(s)...`);
456
+ (0, logVerbose_1.logVerbose)(2, ` [GROUP ${group.id}] Activating ${groupRefs.length} inactive object(s)...`);
457
+ let hasErrors = false;
441
458
  try {
442
- await client.getUtils().activateObjectsGroup(groupRefs, true);
443
- activated += groupRefs.length;
444
- for (const ref of groupRefs) {
445
- (0, logVerbose_1.logVerbose)(2, ` OK ${ref.type} ${ref.name}`);
459
+ const result = await client
460
+ .getUtils()
461
+ .activateObjectsGroup(groupRefs, true);
462
+ // Parse activation result messages
463
+ if (result?.data) {
464
+ const parsed = xmlParser.parse(typeof result.data === 'string' ? result.data : String(result.data));
465
+ const msgs = parsed?.['chkl:messages']?.msg;
466
+ if (msgs) {
467
+ const msgArray = Array.isArray(msgs) ? msgs : [msgs];
468
+ for (const msg of msgArray) {
469
+ const type = msg['@_type'] || 'info';
470
+ const text = msg?.shortText?.txt || msg?.shortText || String(msg);
471
+ if (type === 'E')
472
+ hasErrors = true;
473
+ (0, logVerbose_1.logVerbose)(2, ` [${type}] ${text}`);
474
+ }
475
+ }
446
476
  }
447
477
  }
448
478
  catch (error) {
479
+ hasErrors = true;
449
480
  const message = error instanceof Error ? error.message : String(error);
450
- failed += groupRefs.length;
451
- (0, logVerbose_1.logVerbose)(1, ` [!] Group ${group.id} activation failed: ${message}`);
481
+ (0, logVerbose_1.logVerbose)(2, ` [*] Activation request completed (${message})`);
482
+ }
483
+ // Poll until objects are no longer inactive (max 5 retries, 10s apart)
484
+ // Skip polling if activation returned errors (no point waiting)
485
+ if (!hasErrors) {
486
+ const maxRetries = 5;
487
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
488
+ const postResult = await client.getUtils().getInactiveObjects();
489
+ const postInactiveSet = new Set(postResult.objects.map((o) => `${o.type}:${o.name}`.toUpperCase()));
490
+ const stillInactive = groupRefs.filter((r) => postInactiveSet.has(`${r.type}:${r.name}`.toUpperCase()));
491
+ if (stillInactive.length === 0)
492
+ break;
493
+ if (attempt < maxRetries) {
494
+ (0, logVerbose_1.logVerbose)(2, ` [*] ${stillInactive.length} still inactive, waiting... (${attempt}/${maxRetries})`);
495
+ await new Promise((resolve) => setTimeout(resolve, 10000));
496
+ }
497
+ }
498
+ }
499
+ // Report per-object status
500
+ const finalResult = await client.getUtils().getInactiveObjects();
501
+ const finalInactiveSet = new Set(finalResult.objects.map((o) => `${o.type}:${o.name}`.toUpperCase()));
502
+ for (const ref of groupRefs) {
503
+ const key = `${ref.type}:${ref.name}`.toUpperCase();
504
+ if (finalInactiveSet.has(key)) {
505
+ (0, logVerbose_1.logVerbose)(2, ` INACTIVE ${ref.type} ${ref.name}`);
506
+ failed++;
507
+ }
508
+ else {
509
+ (0, logVerbose_1.logVerbose)(2, ` OK ${ref.type} ${ref.name}`);
510
+ activated++;
511
+ }
452
512
  }
453
513
  }
454
- skipped = allActions.length - activated - failed;
455
- console.log(`Activation complete: ${activated} activated, ${skipped} skipped, ${failed} failed`);
514
+ console.log(`Activation complete: ${activated} activated, ${alreadyActive} already active, ${failed} still inactive`);
456
515
  return;
457
516
  }
458
517
  if (command === 'diff') {