@mcp-abap-adt/adt-backup 1.1.0 → 1.2.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.
- package/dist/lib/backup/readMetadataXmlForType.d.ts.map +1 -1
- package/dist/lib/backup/readMetadataXmlForType.js +37 -17
- package/dist/lib/restore/analyzeDependencies.d.ts +6 -0
- package/dist/lib/restore/analyzeDependencies.d.ts.map +1 -1
- package/dist/lib/restore/analyzeDependencies.js +126 -28
- package/dist/lib/restore/restoreTreeBackup.d.ts +2 -2
- package/dist/lib/restore/restoreTreeBackup.d.ts.map +1 -1
- package/dist/lib/restore/restoreTreeBackup.js +224 -97
- package/dist/lib/run.d.ts.map +1 -1
- package/dist/lib/run.js +103 -44
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"readMetadataXmlForType.d.ts","sourceRoot":"","sources":["../../../src/lib/backup/readMetadataXmlForType.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAG9C,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,SAAS,EACjB,IAAI,EAAE,aAAa,EACnB,IAAI,EAAE,MAAM,EACZ,kBAAkB,CAAC,EAAE,MAAM,GAC1B,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,
|
|
1
|
+
{"version":3,"file":"readMetadataXmlForType.d.ts","sourceRoot":"","sources":["../../../src/lib/backup/readMetadataXmlForType.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAG9C,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,SAAS,EACjB,IAAI,EAAE,aAAa,EACnB,IAAI,EAAE,MAAM,EACZ,kBAAkB,CAAC,EAAE,MAAM,GAC1B,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CA0IpC"}
|
|
@@ -4,106 +4,126 @@ exports.readMetadataXmlForType = readMetadataXmlForType;
|
|
|
4
4
|
const responseToText_1 = require("../utils/responseToText");
|
|
5
5
|
async function readMetadataXmlForType(client, type, name, _functionGroupName) {
|
|
6
6
|
try {
|
|
7
|
+
let result;
|
|
7
8
|
switch (type) {
|
|
8
9
|
case 'package': {
|
|
9
10
|
const state = await client
|
|
10
11
|
.getPackage()
|
|
11
12
|
.readMetadata({ packageName: name });
|
|
12
|
-
|
|
13
|
+
result = (0, responseToText_1.responseToText)(state.metadataResult);
|
|
14
|
+
break;
|
|
13
15
|
}
|
|
14
16
|
case 'class': {
|
|
15
17
|
const state = await client.getClass().readMetadata({ className: name });
|
|
16
|
-
|
|
18
|
+
result = (0, responseToText_1.responseToText)(state.metadataResult);
|
|
19
|
+
break;
|
|
17
20
|
}
|
|
18
21
|
case 'interface': {
|
|
19
22
|
const state = await client
|
|
20
23
|
.getInterface()
|
|
21
24
|
.readMetadata({ interfaceName: name });
|
|
22
|
-
|
|
25
|
+
result = (0, responseToText_1.responseToText)(state.metadataResult);
|
|
26
|
+
break;
|
|
23
27
|
}
|
|
24
28
|
case 'domain': {
|
|
25
29
|
const state = await client
|
|
26
30
|
.getDomain()
|
|
27
31
|
.readMetadata({ domainName: name });
|
|
28
|
-
|
|
32
|
+
result = (0, responseToText_1.responseToText)(state.metadataResult);
|
|
33
|
+
break;
|
|
29
34
|
}
|
|
30
35
|
case 'dataElement': {
|
|
31
36
|
const state = await client
|
|
32
37
|
.getDataElement()
|
|
33
38
|
.readMetadata({ dataElementName: name });
|
|
34
|
-
|
|
39
|
+
result = (0, responseToText_1.responseToText)(state.metadataResult);
|
|
40
|
+
break;
|
|
35
41
|
}
|
|
36
42
|
case 'table': {
|
|
37
43
|
const state = await client.getTable().readMetadata({ tableName: name });
|
|
38
|
-
|
|
44
|
+
result = (0, responseToText_1.responseToText)(state.metadataResult);
|
|
45
|
+
break;
|
|
39
46
|
}
|
|
40
47
|
case 'tableType': {
|
|
41
48
|
const state = await client
|
|
42
49
|
.getTableType()
|
|
43
50
|
.readMetadata({ tableTypeName: name });
|
|
44
|
-
|
|
51
|
+
result = (0, responseToText_1.responseToText)(state.metadataResult);
|
|
52
|
+
break;
|
|
45
53
|
}
|
|
46
54
|
case 'structure': {
|
|
47
55
|
const state = await client
|
|
48
56
|
.getStructure()
|
|
49
57
|
.readMetadata({ structureName: name });
|
|
50
|
-
|
|
58
|
+
result = (0, responseToText_1.responseToText)(state.metadataResult);
|
|
59
|
+
break;
|
|
51
60
|
}
|
|
52
61
|
case 'view': {
|
|
53
62
|
const state = await client.getView().readMetadata({ viewName: name });
|
|
54
|
-
|
|
63
|
+
result = (0, responseToText_1.responseToText)(state.metadataResult);
|
|
64
|
+
break;
|
|
55
65
|
}
|
|
56
66
|
case 'behaviorDefinition': {
|
|
57
67
|
const state = await client
|
|
58
68
|
.getBehaviorDefinition()
|
|
59
69
|
.readMetadata({ name });
|
|
60
|
-
|
|
70
|
+
result = (0, responseToText_1.responseToText)(state.metadataResult);
|
|
71
|
+
break;
|
|
61
72
|
}
|
|
62
73
|
case 'behaviorImplementation': {
|
|
63
74
|
const state = await client
|
|
64
75
|
.getBehaviorImplementation()
|
|
65
76
|
.readMetadata({ className: name });
|
|
66
|
-
|
|
77
|
+
result = (0, responseToText_1.responseToText)(state.metadataResult);
|
|
78
|
+
break;
|
|
67
79
|
}
|
|
68
80
|
case 'serviceDefinition': {
|
|
69
81
|
const state = await client
|
|
70
82
|
.getServiceDefinition()
|
|
71
83
|
.readMetadata({ serviceDefinitionName: name });
|
|
72
|
-
|
|
84
|
+
result = (0, responseToText_1.responseToText)(state.metadataResult);
|
|
85
|
+
break;
|
|
73
86
|
}
|
|
74
87
|
case 'serviceBinding': {
|
|
75
88
|
const state = await client
|
|
76
89
|
.getServiceBinding()
|
|
77
90
|
.readMetadata({ bindingName: name });
|
|
78
|
-
|
|
91
|
+
result = (0, responseToText_1.responseToText)(state.metadataResult);
|
|
92
|
+
break;
|
|
79
93
|
}
|
|
80
94
|
case 'metadataExtension': {
|
|
81
95
|
const state = await client
|
|
82
96
|
.getMetadataExtension()
|
|
83
97
|
.readMetadata({ name });
|
|
84
|
-
|
|
98
|
+
result = (0, responseToText_1.responseToText)(state.metadataResult);
|
|
99
|
+
break;
|
|
85
100
|
}
|
|
86
101
|
case 'functionGroup': {
|
|
87
102
|
const state = await client
|
|
88
103
|
.getFunctionGroup()
|
|
89
104
|
.readMetadata({ functionGroupName: name });
|
|
90
|
-
|
|
105
|
+
result = (0, responseToText_1.responseToText)(state.metadataResult);
|
|
106
|
+
break;
|
|
91
107
|
}
|
|
92
108
|
case 'enhancement': {
|
|
93
109
|
const state = await client
|
|
94
110
|
.getEnhancement()
|
|
95
111
|
.readMetadata({ enhancementName: name, enhancementType: 'enhoxh' });
|
|
96
|
-
|
|
112
|
+
result = (0, responseToText_1.responseToText)(state.metadataResult);
|
|
113
|
+
break;
|
|
97
114
|
}
|
|
98
115
|
case 'accessControl': {
|
|
99
116
|
const state = await client
|
|
100
117
|
.getAccessControl()
|
|
101
118
|
.readMetadata({ accessControlName: name });
|
|
102
|
-
|
|
119
|
+
result = (0, responseToText_1.responseToText)(state.metadataResult);
|
|
120
|
+
break;
|
|
103
121
|
}
|
|
104
122
|
default:
|
|
105
123
|
return undefined;
|
|
106
124
|
}
|
|
125
|
+
// ADT clients return undefined on 404 — treat as not found
|
|
126
|
+
return result ?? null;
|
|
107
127
|
}
|
|
108
128
|
catch (error) {
|
|
109
129
|
const status = error.status || error.response?.status;
|
|
@@ -10,4 +10,10 @@ export interface RestoreGroup {
|
|
|
10
10
|
* (e.g. view and behaviorDefinition for the same CDS entity).
|
|
11
11
|
*/
|
|
12
12
|
export declare function analyzeDependencies(nodes: BackupTreeNode[]): RestoreGroup[];
|
|
13
|
+
/**
|
|
14
|
+
* Analyzes dependencies and merges SCCs at the same dependency level
|
|
15
|
+
* into single groups. Level = max(level of dependencies) + 1.
|
|
16
|
+
* Independent SCCs (same level) are merged into one group.
|
|
17
|
+
*/
|
|
18
|
+
export declare function analyzeDependencyLevels(nodes: BackupTreeNode[]): RestoreGroup[];
|
|
13
19
|
//# sourceMappingURL=analyzeDependencies.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyzeDependencies.d.ts","sourceRoot":"","sources":["../../../src/lib/restore/analyzeDependencies.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE/C,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,UAAU,EAAE,OAAO,CAAC;CACrB;
|
|
1
|
+
{"version":3,"file":"analyzeDependencies.d.ts","sourceRoot":"","sources":["../../../src/lib/restore/analyzeDependencies.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE/C,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,UAAU,EAAE,OAAO,CAAC;CACrB;AA+MD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,YAAY,EAAE,CA2B3E;AA2BD;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,cAAc,EAAE,GACtB,YAAY,EAAE,CA8DhB"}
|
|
@@ -1,30 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.analyzeDependencies = analyzeDependencies;
|
|
4
|
+
exports.analyzeDependencyLevels = analyzeDependencyLevels;
|
|
4
5
|
const decodeBase64_1 = require("../crypto/decodeBase64");
|
|
5
6
|
function nodeKey(node) {
|
|
6
7
|
return `${node.type}:${node.name}`.toUpperCase();
|
|
7
8
|
}
|
|
8
9
|
/**
|
|
9
|
-
*
|
|
10
|
-
* source code and XML metadata.
|
|
11
|
-
* Uses composite type:name keys to handle objects that share the same name
|
|
12
|
-
* (e.g. view and behaviorDefinition for the same CDS entity).
|
|
10
|
+
* Build adjacency map (forward dependencies) by scanning source code and config.
|
|
13
11
|
*/
|
|
14
|
-
function
|
|
15
|
-
const idToNode = new Map();
|
|
16
|
-
const nameToIds = new Map();
|
|
17
|
-
const allNames = new Set();
|
|
18
|
-
for (const node of nodes) {
|
|
19
|
-
const id = nodeKey(node);
|
|
20
|
-
const upperName = node.name.toUpperCase();
|
|
21
|
-
idToNode.set(id, node);
|
|
22
|
-
allNames.add(upperName);
|
|
23
|
-
const ids = nameToIds.get(upperName) || [];
|
|
24
|
-
ids.push(id);
|
|
25
|
-
nameToIds.set(upperName, ids);
|
|
26
|
-
}
|
|
27
|
-
const allIds = new Set(idToNode.keys());
|
|
12
|
+
function buildAdjacency(nodes, allNames, nameToIds, allIds) {
|
|
28
13
|
const adj = new Map();
|
|
29
14
|
for (const node of nodes) {
|
|
30
15
|
const deps = new Set();
|
|
@@ -37,13 +22,9 @@ function analyzeDependencies(nodes) {
|
|
|
37
22
|
for (const targetName of allNames) {
|
|
38
23
|
if (targetName === nodeNameUpper)
|
|
39
24
|
continue;
|
|
40
|
-
// Escape name for regex, specifically handling namespaces with /
|
|
41
25
|
const escapedTarget = targetName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
42
|
-
// Match only if surrounded by boundaries that are NOT part of an ABAP name
|
|
43
|
-
// (i.e. not letters, digits, _, or /)
|
|
44
26
|
const regex = new RegExp(`(?<=[^A-Z0-9_/]|^)${escapedTarget}(?=[^A-Z0-9_/]|$)`, 'g');
|
|
45
27
|
if (regex.test(contentUpper)) {
|
|
46
|
-
// Resolve name to all node IDs with that name
|
|
47
28
|
const targetIds = nameToIds.get(targetName) || [];
|
|
48
29
|
for (const tid of targetIds) {
|
|
49
30
|
if (tid !== id)
|
|
@@ -51,9 +32,8 @@ function analyzeDependencies(nodes) {
|
|
|
51
32
|
}
|
|
52
33
|
}
|
|
53
34
|
}
|
|
54
|
-
// 2. Explicit structural dependencies
|
|
55
|
-
//
|
|
56
|
-
// From class source: "FOR BEHAVIOR OF <bdef_name>"
|
|
35
|
+
// 2. Explicit structural dependencies
|
|
36
|
+
// BIML class -> BDEF (bidirectional = same SCC)
|
|
57
37
|
if (node.type === 'class' || node.type === 'behaviorImplementation') {
|
|
58
38
|
const bdefMatch = contentUpper.match(/FOR\s+BEHAVIOR\s+OF\s+([A-Z0-9_/]+)/);
|
|
59
39
|
if (bdefMatch) {
|
|
@@ -62,7 +42,7 @@ function analyzeDependencies(nodes) {
|
|
|
62
42
|
deps.add(bdefId);
|
|
63
43
|
}
|
|
64
44
|
}
|
|
65
|
-
//
|
|
45
|
+
// BDEF -> BIML class
|
|
66
46
|
if (node.type === 'behaviorDefinition') {
|
|
67
47
|
for (const m of contentUpper.matchAll(/IMPLEMENTATION\s+IN\s+CLASS\s+([A-Z0-9_/]+)/g)) {
|
|
68
48
|
const className = m[1];
|
|
@@ -93,7 +73,12 @@ function analyzeDependencies(nodes) {
|
|
|
93
73
|
}
|
|
94
74
|
adj.set(id, deps);
|
|
95
75
|
}
|
|
96
|
-
|
|
76
|
+
return adj;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Tarjan's SCC algorithm.
|
|
80
|
+
*/
|
|
81
|
+
function tarjanSCC(allIds, adj) {
|
|
97
82
|
let index = 0;
|
|
98
83
|
const stack = [];
|
|
99
84
|
const onStack = new Set();
|
|
@@ -140,7 +125,12 @@ function analyzeDependencies(nodes) {
|
|
|
140
125
|
strongConnect(id);
|
|
141
126
|
}
|
|
142
127
|
}
|
|
143
|
-
|
|
128
|
+
return sccs;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Build SCC DAG and topological order from SCCs and adjacency map.
|
|
132
|
+
*/
|
|
133
|
+
function buildSccDag(sccs, adj) {
|
|
144
134
|
const sccAdj = new Map();
|
|
145
135
|
const nodeToSccIndex = new Map();
|
|
146
136
|
sccs.forEach((scc, i) => {
|
|
@@ -177,6 +167,31 @@ function analyzeDependencies(nodes) {
|
|
|
177
167
|
for (let i = 0; i < sccs.length; i++) {
|
|
178
168
|
visit(i);
|
|
179
169
|
}
|
|
170
|
+
return { sccAdj, order };
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Robustly analyzes dependencies between objects by scanning both
|
|
174
|
+
* source code and XML metadata.
|
|
175
|
+
* Uses composite type:name keys to handle objects that share the same name
|
|
176
|
+
* (e.g. view and behaviorDefinition for the same CDS entity).
|
|
177
|
+
*/
|
|
178
|
+
function analyzeDependencies(nodes) {
|
|
179
|
+
const idToNode = new Map();
|
|
180
|
+
const nameToIds = new Map();
|
|
181
|
+
const allNames = new Set();
|
|
182
|
+
for (const node of nodes) {
|
|
183
|
+
const id = nodeKey(node);
|
|
184
|
+
const upperName = node.name.toUpperCase();
|
|
185
|
+
idToNode.set(id, node);
|
|
186
|
+
allNames.add(upperName);
|
|
187
|
+
const ids = nameToIds.get(upperName) || [];
|
|
188
|
+
ids.push(id);
|
|
189
|
+
nameToIds.set(upperName, ids);
|
|
190
|
+
}
|
|
191
|
+
const allIds = new Set(idToNode.keys());
|
|
192
|
+
const adj = buildAdjacency(nodes, allNames, nameToIds, allIds);
|
|
193
|
+
const sccs = tarjanSCC(allIds, adj);
|
|
194
|
+
const { order } = buildSccDag(sccs, adj);
|
|
180
195
|
return order.map((i) => {
|
|
181
196
|
const ids = sccs[i];
|
|
182
197
|
return {
|
|
@@ -185,3 +200,86 @@ function analyzeDependencies(nodes) {
|
|
|
185
200
|
};
|
|
186
201
|
});
|
|
187
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Creation order priority within a group.
|
|
205
|
+
* Lower = created first. Ensures BDEF exists before BIML class, etc.
|
|
206
|
+
*/
|
|
207
|
+
const TYPE_CREATION_ORDER = {
|
|
208
|
+
domain: 0,
|
|
209
|
+
dataElement: 1,
|
|
210
|
+
structure: 2,
|
|
211
|
+
table: 2,
|
|
212
|
+
tableType: 2,
|
|
213
|
+
view: 3,
|
|
214
|
+
behaviorDefinition: 4,
|
|
215
|
+
behaviorImplementation: 5,
|
|
216
|
+
class: 5,
|
|
217
|
+
interface: 5,
|
|
218
|
+
accessControl: 6,
|
|
219
|
+
metadataExtension: 6,
|
|
220
|
+
program: 7,
|
|
221
|
+
functionGroup: 7,
|
|
222
|
+
functionModule: 8,
|
|
223
|
+
serviceDefinition: 9,
|
|
224
|
+
serviceBinding: 10,
|
|
225
|
+
enhancement: 11,
|
|
226
|
+
};
|
|
227
|
+
/**
|
|
228
|
+
* Analyzes dependencies and merges SCCs at the same dependency level
|
|
229
|
+
* into single groups. Level = max(level of dependencies) + 1.
|
|
230
|
+
* Independent SCCs (same level) are merged into one group.
|
|
231
|
+
*/
|
|
232
|
+
function analyzeDependencyLevels(nodes) {
|
|
233
|
+
const idToNode = new Map();
|
|
234
|
+
const nameToIds = new Map();
|
|
235
|
+
const allNames = new Set();
|
|
236
|
+
for (const node of nodes) {
|
|
237
|
+
const id = nodeKey(node);
|
|
238
|
+
const upperName = node.name.toUpperCase();
|
|
239
|
+
idToNode.set(id, node);
|
|
240
|
+
allNames.add(upperName);
|
|
241
|
+
const ids = nameToIds.get(upperName) || [];
|
|
242
|
+
ids.push(id);
|
|
243
|
+
nameToIds.set(upperName, ids);
|
|
244
|
+
}
|
|
245
|
+
const allIds = new Set(idToNode.keys());
|
|
246
|
+
const adj = buildAdjacency(nodes, allNames, nameToIds, allIds);
|
|
247
|
+
const sccs = tarjanSCC(allIds, adj);
|
|
248
|
+
const { sccAdj, order } = buildSccDag(sccs, adj);
|
|
249
|
+
// Compute level for each SCC: level = max(level of deps) + 1
|
|
250
|
+
const sccLevel = new Map();
|
|
251
|
+
for (const i of order) {
|
|
252
|
+
let maxDepLevel = -1;
|
|
253
|
+
const deps = sccAdj.get(i) || new Set();
|
|
254
|
+
for (const d of deps) {
|
|
255
|
+
const depLevel = sccLevel.get(d) ?? 0;
|
|
256
|
+
if (depLevel > maxDepLevel)
|
|
257
|
+
maxDepLevel = depLevel;
|
|
258
|
+
}
|
|
259
|
+
sccLevel.set(i, maxDepLevel + 1);
|
|
260
|
+
}
|
|
261
|
+
// Group SCCs by level, merge into RestoreGroups
|
|
262
|
+
const levelMap = new Map();
|
|
263
|
+
for (const i of order) {
|
|
264
|
+
const level = sccLevel.get(i);
|
|
265
|
+
if (!levelMap.has(level)) {
|
|
266
|
+
levelMap.set(level, { nodes: [], hasCircular: false });
|
|
267
|
+
}
|
|
268
|
+
const entry = levelMap.get(level);
|
|
269
|
+
const sccNodes = sccs[i].map((id) => idToNode.get(id));
|
|
270
|
+
entry.nodes.push(...sccNodes);
|
|
271
|
+
if (sccs[i].length > 1)
|
|
272
|
+
entry.hasCircular = true;
|
|
273
|
+
}
|
|
274
|
+
const sortedLevels = [...levelMap.keys()].sort((a, b) => a - b);
|
|
275
|
+
return sortedLevels.map((level) => {
|
|
276
|
+
const entry = levelMap.get(level);
|
|
277
|
+
// Sort nodes within group by creation order (views before BDEFs before classes)
|
|
278
|
+
entry.nodes.sort((a, b) => (TYPE_CREATION_ORDER[a.type || ''] ?? 99) -
|
|
279
|
+
(TYPE_CREATION_ORDER[b.type || ''] ?? 99));
|
|
280
|
+
return {
|
|
281
|
+
nodes: entry.nodes,
|
|
282
|
+
isCircular: entry.hasCircular || entry.nodes.length > 1,
|
|
283
|
+
};
|
|
284
|
+
});
|
|
285
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { AdtClient } from '@mcp-abap-adt/adt-clients';
|
|
2
|
-
import type { BackupTreeNode, RestoreMode } from '../types';
|
|
3
|
-
export declare function restoreTreeBackup(client: AdtClient, root: BackupTreeNode, mode: RestoreMode, activate: boolean, transportRequest?: string, restoreIds?: Set<string>,
|
|
2
|
+
import type { BackupTreeNode, RestoreMode, RestorePlanGroup } from '../types';
|
|
3
|
+
export declare function restoreTreeBackup(client: AdtClient, root: BackupTreeNode, mode: RestoreMode, activate: boolean, transportRequest?: string, restoreIds?: Set<string>, planGroups?: RestorePlanGroup[], activateOnCreate?: boolean, softwareComponent?: string, superPackageOverride?: string, transportLayer?: string): Promise<void>;
|
|
4
4
|
//# sourceMappingURL=restoreTreeBackup.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"restoreTreeBackup.d.ts","sourceRoot":"","sources":["../../../src/lib/restore/restoreTreeBackup.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAmB,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"restoreTreeBackup.d.ts","sourceRoot":"","sources":["../../../src/lib/restore/restoreTreeBackup.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAmB,MAAM,2BAA2B,CAAC;AAK5E,OAAO,KAAK,EACV,cAAc,EACd,WAAW,EACX,gBAAgB,EAEjB,MAAM,UAAU,CAAC;AA8DlB,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,SAAS,EACjB,IAAI,EAAE,cAAc,EACpB,IAAI,EAAE,WAAW,EACjB,QAAQ,EAAE,OAAO,EACjB,gBAAgB,CAAC,EAAE,MAAM,EACzB,UAAU,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,EACxB,UAAU,CAAC,EAAE,gBAAgB,EAAE,EAC/B,gBAAgB,UAAO,EACvB,iBAAiB,CAAC,EAAE,MAAM,EAC1B,oBAAoB,CAAC,EAAE,MAAM,EAC7B,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC,IAAI,CAAC,CAiZf"}
|
|
@@ -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' },
|
|
@@ -45,7 +51,7 @@ const RESTORE_PHASES = [
|
|
|
45
51
|
{ name: 'Service Bindings', types: ['serviceBinding'], activation: 'bulk' },
|
|
46
52
|
{ name: 'Enhancements', types: ['enhancement'], activation: 'individual' },
|
|
47
53
|
];
|
|
48
|
-
async function restoreTreeBackup(client, root, mode, activate, transportRequest, restoreIds,
|
|
54
|
+
async function restoreTreeBackup(client, root, mode, activate, transportRequest, restoreIds, planGroups, activateOnCreate = true, softwareComponent, superPackageOverride, transportLayer) {
|
|
49
55
|
const allNodes = (0, flattenTree_1.flattenTree)(root).filter((node) => node.type && node.restoreStatus === 'ok');
|
|
50
56
|
const nodes = restoreIds
|
|
51
57
|
? allNodes.filter((node) => {
|
|
@@ -57,68 +63,94 @@ async function restoreTreeBackup(client, root, mode, activate, transportRequest,
|
|
|
57
63
|
const nonPackageNodes = nodes.filter((node) => node.type !== 'package');
|
|
58
64
|
const backupPackageNames = new Set(packageNodes.map((node) => node.name));
|
|
59
65
|
const rootPackageName = root.name;
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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);
|
|
66
|
+
// Build restoreActions map from planGroups (if provided) for mode lookups
|
|
67
|
+
const restoreActions = planGroups
|
|
68
|
+
? new Map(planGroups
|
|
69
|
+
.flatMap((g) => g.actions)
|
|
70
|
+
.map((a) => [a.id, a.action]))
|
|
71
|
+
: undefined;
|
|
72
|
+
// Build nodeMap for quick lookup by objectId (used in plan-driven path)
|
|
73
|
+
const nodeMap = new Map();
|
|
74
|
+
for (const node of allNodes) {
|
|
75
|
+
const id = (0, getNodeObjectId_1.getNodeObjectId)(node);
|
|
76
|
+
if (id)
|
|
77
|
+
nodeMap.set(id, node);
|
|
98
78
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
79
|
+
const failures = [];
|
|
80
|
+
// Helper: check which of our refs are still inactive
|
|
81
|
+
const findInactiveRefs = async (refs) => {
|
|
82
|
+
const result = await client.getUtils().getInactiveObjects();
|
|
83
|
+
const inactiveSet = new Set(result.objects.map((o) => `${o.type}:${o.name}`.toUpperCase()));
|
|
84
|
+
return refs.filter((r) => inactiveSet.has(`${r.type}:${r.name}`.toUpperCase()));
|
|
85
|
+
};
|
|
86
|
+
// Helper to bulk activate a list of refs with verification
|
|
105
87
|
const bulkActivate = async (phaseName, refs) => {
|
|
106
88
|
if (refs.length === 0)
|
|
107
89
|
return;
|
|
108
|
-
|
|
90
|
+
// Check which objects are actually inactive
|
|
91
|
+
let toActivate = await findInactiveRefs(refs);
|
|
92
|
+
if (toActivate.length === 0) {
|
|
93
|
+
(0, logVerbose_1.logVerbose)(2, ` [*] ${phaseName}: all ${refs.length} objects already active`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
(0, logVerbose_1.logVerbose)(2, ` [*] Bulk activating ${phaseName} (${toActivate.length}/${refs.length} inactive)...`);
|
|
97
|
+
let hasErrors = false;
|
|
109
98
|
try {
|
|
110
|
-
await client
|
|
99
|
+
const result = await client
|
|
100
|
+
.getUtils()
|
|
101
|
+
.activateObjectsGroup(toActivate, true);
|
|
102
|
+
if (result?.data) {
|
|
103
|
+
const parsed = xmlParser.parse(typeof result.data === 'string' ? result.data : String(result.data));
|
|
104
|
+
const msgs = parsed?.['chkl:messages']?.msg;
|
|
105
|
+
if (msgs) {
|
|
106
|
+
const msgArray = Array.isArray(msgs) ? msgs : [msgs];
|
|
107
|
+
for (const msg of msgArray) {
|
|
108
|
+
const type = msg['@_type'] || 'info';
|
|
109
|
+
const text = msg?.shortText?.txt || msg?.shortText || String(msg);
|
|
110
|
+
if (type === 'E')
|
|
111
|
+
hasErrors = true;
|
|
112
|
+
(0, logVerbose_1.logVerbose)(2, ` [${type}] ${text}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
111
116
|
}
|
|
112
117
|
catch (error) {
|
|
118
|
+
hasErrors = true;
|
|
113
119
|
const message = error instanceof Error ? error.message : String(error);
|
|
114
|
-
(0, logVerbose_1.logVerbose)(
|
|
115
|
-
|
|
120
|
+
(0, logVerbose_1.logVerbose)(2, ` [*] Activation request completed (${message})`);
|
|
121
|
+
}
|
|
122
|
+
// Verify: poll until our objects are no longer inactive (max 5 retries, 10s apart)
|
|
123
|
+
// Skip polling if activation returned errors
|
|
124
|
+
if (hasErrors) {
|
|
125
|
+
const stillInactive = await findInactiveRefs(refs);
|
|
126
|
+
if (stillInactive.length > 0) {
|
|
127
|
+
(0, logVerbose_1.logVerbose)(1, ` [!] WARNING: ${phaseName}: ${stillInactive.length} object(s) remain inactive:`);
|
|
128
|
+
for (const ref of stillInactive) {
|
|
129
|
+
(0, logVerbose_1.logVerbose)(1, ` - ${ref.type}:${ref.name}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const maxRetries = 5;
|
|
135
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
136
|
+
const stillInactive = await findInactiveRefs(refs);
|
|
137
|
+
if (stillInactive.length === 0) {
|
|
138
|
+
(0, logVerbose_1.logVerbose)(2, ` [*] ${phaseName}: all objects activated successfully`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (attempt < maxRetries) {
|
|
142
|
+
(0, logVerbose_1.logVerbose)(2, ` [*] ${stillInactive.length} object(s) still inactive, waiting... (${attempt}/${maxRetries})`);
|
|
143
|
+
await new Promise((resolve) => setTimeout(resolve, 10000));
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
(0, logVerbose_1.logVerbose)(1, ` [!] WARNING: ${phaseName}: ${stillInactive.length} object(s) remain inactive:`);
|
|
147
|
+
for (const ref of stillInactive) {
|
|
148
|
+
(0, logVerbose_1.logVerbose)(1, ` - ${ref.type}:${ref.name}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
116
151
|
}
|
|
117
152
|
};
|
|
118
|
-
//
|
|
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)
|
|
153
|
+
// Helper: process a single node (create/update)
|
|
122
154
|
const processNode = async (node, activateFlag) => {
|
|
123
155
|
const nodeId = (0, getNodeObjectId_1.getNodeObjectId)(node);
|
|
124
156
|
if (!nodeId)
|
|
@@ -148,64 +180,159 @@ async function restoreTreeBackup(client, root, mode, activate, transportRequest,
|
|
|
148
180
|
}
|
|
149
181
|
return null;
|
|
150
182
|
};
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
if (
|
|
163
|
-
|
|
183
|
+
// Phase 1: Packages (recursive hierarchy) — shared by both paths
|
|
184
|
+
if (packageNodes.length > 0) {
|
|
185
|
+
(0, logVerbose_1.logVerbose)(1, '[PHASE 1] Restoring package hierarchy...');
|
|
186
|
+
const restorePackageRecursive = async (node, parentName) => {
|
|
187
|
+
const nodeId = (0, getNodeObjectId_1.getNodeObjectId)(node);
|
|
188
|
+
if (node.type === 'package' &&
|
|
189
|
+
nodeId &&
|
|
190
|
+
(!restoreIds || restoreIds.has(nodeId))) {
|
|
191
|
+
const isRootNode = node.name === rootPackageName;
|
|
192
|
+
const nodeMode = (restoreActions?.get(nodeId) || mode);
|
|
193
|
+
const effectiveMode = isRootNode ? 'update' : nodeMode;
|
|
194
|
+
if (effectiveMode === 'skip') {
|
|
195
|
+
(0, logVerbose_1.logVerbose)(2, ` [SKIP] package:${node.name}`);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
(0, logVerbose_1.logVerbose)(2, ` [PACKAGE] ${node.name}`);
|
|
199
|
+
try {
|
|
200
|
+
await (0, restoreTreeNode_1.restoreTreeNode)(client, node, effectiveMode, false, transportRequest, softwareComponent, backupPackageNames, parentName || superPackageOverride, transportLayer);
|
|
201
|
+
}
|
|
202
|
+
catch (e) {
|
|
203
|
+
if (isRootNode) {
|
|
204
|
+
(0, logVerbose_1.logVerbose)(1, ` ! Warning: Root package ${node.name} already exists or update skipped.`);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
throw e;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
164
211
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
212
|
+
if (node.children) {
|
|
213
|
+
for (const child of node.children) {
|
|
214
|
+
await restorePackageRecursive(child, node.type === 'package' ? node.name : parentName);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
await restorePackageRecursive(root, undefined);
|
|
219
|
+
}
|
|
220
|
+
const allProcessedRefs = [];
|
|
221
|
+
if (planGroups) {
|
|
222
|
+
// ===== Plan-driven restore: follow plan group order =====
|
|
223
|
+
(0, logVerbose_1.logVerbose)(1, `\n>>> PLAN-DRIVEN RESTORE: ${planGroups.length} groups`);
|
|
224
|
+
for (const group of planGroups) {
|
|
225
|
+
const nonPackageActions = group.actions.filter((a) => a.type !== 'package');
|
|
226
|
+
if (nonPackageActions.length === 0)
|
|
227
|
+
continue;
|
|
228
|
+
(0, logVerbose_1.logVerbose)(1, `[GROUP ${group.id}] ${nonPackageActions.length} object(s)${group.isCircular ? ' (circular)' : ''}`);
|
|
229
|
+
const groupRefs = [];
|
|
230
|
+
for (const action of nonPackageActions) {
|
|
231
|
+
if (action.action === 'skip') {
|
|
232
|
+
(0, logVerbose_1.logVerbose)(2, ` [SKIP] ${action.type}:${action.name}`);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const node = nodeMap.get(action.id);
|
|
236
|
+
if (!node) {
|
|
237
|
+
(0, logVerbose_1.logVerbose)(1, ` [WARN] Node not found for ${action.id}, skipping`);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
170
240
|
const ref = await processNode(node, false);
|
|
171
241
|
if (ref)
|
|
172
|
-
|
|
242
|
+
groupRefs.push(ref);
|
|
243
|
+
}
|
|
244
|
+
if (groupRefs.length > 0) {
|
|
245
|
+
await bulkActivate(`Group ${group.id}${group.isCircular ? ' (circular)' : ''}`, groupRefs);
|
|
173
246
|
}
|
|
174
|
-
|
|
175
|
-
allProcessedRefs.push(...refs);
|
|
247
|
+
allProcessedRefs.push(...groupRefs);
|
|
176
248
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
// ===== Fallback: type-phase restore (no plan) =====
|
|
252
|
+
(0, logVerbose_1.logVerbose)(1, `\n>>> STARTING TYPE-PHASE RESTORE: ${nodes.length} objects`);
|
|
253
|
+
(0, logVerbose_1.logVerbose)(1, `[PHASE 2] Analyzing dependencies for ${nonPackageNodes.length} objects...`);
|
|
254
|
+
const restoreGroups = (0, analyzeDependencies_1.analyzeDependencies)(nonPackageNodes);
|
|
255
|
+
const orderedNodes = restoreGroups.flatMap((g) => g.nodes);
|
|
256
|
+
(0, logVerbose_1.logVerbose)(1, `Dependency analysis complete: ${restoreGroups.length} groups → ${orderedNodes.length} ordered nodes.`);
|
|
257
|
+
const knownTypes = new Set(RESTORE_PHASES.flatMap((p) => p.types));
|
|
258
|
+
const uncategorizedNodes = orderedNodes.filter((n) => n.type && !knownTypes.has(n.type) && n.type !== 'package');
|
|
259
|
+
for (const phase of RESTORE_PHASES) {
|
|
260
|
+
const phaseTypeSet = new Set(phase.types);
|
|
261
|
+
const phaseNodes = orderedNodes.filter((n) => n.type && phaseTypeSet.has(n.type));
|
|
262
|
+
if (phaseNodes.length === 0)
|
|
263
|
+
continue;
|
|
264
|
+
(0, logVerbose_1.logVerbose)(1, `[${phase.name.toUpperCase()}] Processing ${phaseNodes.length} object(s) (${phase.activation})...`);
|
|
265
|
+
if (phase.activation === 'individual') {
|
|
266
|
+
for (const node of phaseNodes) {
|
|
267
|
+
const ref = await processNode(node, true);
|
|
268
|
+
if (ref)
|
|
269
|
+
allProcessedRefs.push(ref);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
else if (phase.activation === 'bulk') {
|
|
273
|
+
const refs = [];
|
|
274
|
+
for (const node of phaseNodes) {
|
|
185
275
|
const ref = await processNode(node, false);
|
|
186
276
|
if (ref)
|
|
187
|
-
|
|
277
|
+
refs.push(ref);
|
|
188
278
|
}
|
|
189
|
-
|
|
190
|
-
|
|
279
|
+
await bulkActivate(phase.name, refs);
|
|
280
|
+
allProcessedRefs.push(...refs);
|
|
281
|
+
}
|
|
282
|
+
else if (phase.activation === 'cluster') {
|
|
283
|
+
const groups = (0, analyzeDependencies_1.analyzeDependencies)(phaseNodes);
|
|
284
|
+
(0, logVerbose_1.logVerbose)(2, ` Dependency clustering: ${groups.length} cluster(s)`);
|
|
285
|
+
for (let gi = 0; gi < groups.length; gi++) {
|
|
286
|
+
const group = groups[gi];
|
|
287
|
+
const clusterRefs = [];
|
|
288
|
+
for (const node of group.nodes) {
|
|
289
|
+
const ref = await processNode(node, false);
|
|
290
|
+
if (ref)
|
|
291
|
+
clusterRefs.push(ref);
|
|
292
|
+
}
|
|
293
|
+
if (clusterRefs.length > 0) {
|
|
294
|
+
await bulkActivate(`${phase.name} cluster ${gi + 1}/${groups.length}${group.isCircular ? ' (circular)' : ''}`, clusterRefs);
|
|
295
|
+
}
|
|
296
|
+
allProcessedRefs.push(...clusterRefs);
|
|
191
297
|
}
|
|
192
|
-
allProcessedRefs.push(...clusterRefs);
|
|
193
298
|
}
|
|
194
299
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
allProcessedRefs.push(ref);
|
|
300
|
+
if (uncategorizedNodes.length > 0) {
|
|
301
|
+
(0, logVerbose_1.logVerbose)(1, `[OTHER] Processing ${uncategorizedNodes.length} uncategorized object(s) (individual)...`);
|
|
302
|
+
for (const node of uncategorizedNodes) {
|
|
303
|
+
const ref = await processNode(node, true);
|
|
304
|
+
if (ref)
|
|
305
|
+
allProcessedRefs.push(ref);
|
|
306
|
+
}
|
|
203
307
|
}
|
|
204
308
|
}
|
|
205
|
-
// Final
|
|
309
|
+
// Final check: find remaining inactive objects and activate them
|
|
206
310
|
if (allProcessedRefs.length > 0) {
|
|
207
|
-
|
|
208
|
-
|
|
311
|
+
const stillInactive = await findInactiveRefs(allProcessedRefs);
|
|
312
|
+
if (stillInactive.length > 0) {
|
|
313
|
+
(0, logVerbose_1.logVerbose)(1, `[FINAL] ${stillInactive.length} object(s) still inactive, activating...`);
|
|
314
|
+
try {
|
|
315
|
+
await client.getUtils().activateObjectsGroup(stillInactive, true);
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
319
|
+
(0, logVerbose_1.logVerbose)(2, ` [*] Final activation request completed (${message})`);
|
|
320
|
+
}
|
|
321
|
+
// Verify final state
|
|
322
|
+
const remaining = await findInactiveRefs(allProcessedRefs);
|
|
323
|
+
if (remaining.length > 0) {
|
|
324
|
+
(0, logVerbose_1.logVerbose)(1, ` [!] ${remaining.length} object(s) remain inactive:`);
|
|
325
|
+
for (const ref of remaining) {
|
|
326
|
+
(0, logVerbose_1.logVerbose)(1, ` - ${ref.type}:${ref.name}`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
(0, logVerbose_1.logVerbose)(1, '[FINAL] All objects activated successfully.');
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
(0, logVerbose_1.logVerbose)(1, '[FINAL] All objects are active.');
|
|
335
|
+
}
|
|
209
336
|
}
|
|
210
337
|
if (failures.length > 0) {
|
|
211
338
|
(0, logVerbose_1.logVerbose)(1, `\n>>> RESTORE COMPLETED WITH ${failures.length} FAILURE(S):`);
|
package/dist/lib/run.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/lib/run.ts"],"names":[],"mappings":"
|
|
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
|
-
|
|
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
|
-
...
|
|
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,
|
|
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
|
-
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
|
|
423
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
|
|
451
|
-
|
|
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
|
-
|
|
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') {
|
package/package.json
CHANGED