tm1npm 1.5.3 → 2.0.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/CHANGELOG.md +89 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/services/ApplicationService.d.ts +19 -3
- package/lib/services/ApplicationService.d.ts.map +1 -1
- package/lib/services/ApplicationService.js +232 -6
- package/lib/services/AsyncOperationService.d.ts +8 -1
- package/lib/services/AsyncOperationService.d.ts.map +1 -1
- package/lib/services/AsyncOperationService.js +69 -26
- package/lib/services/ElementService.d.ts +67 -1
- package/lib/services/ElementService.d.ts.map +1 -1
- package/lib/services/ElementService.js +214 -0
- package/lib/services/FileService.d.ts.map +1 -1
- package/lib/services/HierarchyService.d.ts +26 -0
- package/lib/services/HierarchyService.d.ts.map +1 -1
- package/lib/services/HierarchyService.js +306 -0
- package/lib/services/ProcessService.d.ts +40 -22
- package/lib/services/ProcessService.d.ts.map +1 -1
- package/lib/services/ProcessService.js +118 -111
- package/lib/services/RestService.d.ts +213 -25
- package/lib/services/RestService.d.ts.map +1 -1
- package/lib/services/RestService.js +841 -263
- package/lib/services/SubsetService.d.ts +2 -0
- package/lib/services/SubsetService.d.ts.map +1 -1
- package/lib/services/SubsetService.js +33 -0
- package/lib/services/TM1Service.d.ts +44 -1
- package/lib/services/TM1Service.d.ts.map +1 -1
- package/lib/services/TM1Service.js +96 -4
- package/lib/services/index.d.ts +1 -1
- package/lib/services/index.d.ts.map +1 -1
- package/lib/tests/100PercentParityCheck.test.js +23 -6
- package/lib/tests/applicationService.issue38.test.d.ts +5 -0
- package/lib/tests/applicationService.issue38.test.d.ts.map +1 -0
- package/lib/tests/applicationService.issue38.test.js +237 -0
- package/lib/tests/asyncOperationService.test.js +51 -45
- package/lib/tests/bugfix28.test.js +12 -4
- package/lib/tests/elementService.issue37.test.d.ts +5 -0
- package/lib/tests/elementService.issue37.test.d.ts.map +1 -0
- package/lib/tests/elementService.issue37.test.js +413 -0
- package/lib/tests/elementService.issue38.test.d.ts +5 -0
- package/lib/tests/elementService.issue38.test.d.ts.map +1 -0
- package/lib/tests/elementService.issue38.test.js +79 -0
- package/lib/tests/hierarchyService.issue38.test.d.ts +5 -0
- package/lib/tests/hierarchyService.issue38.test.d.ts.map +1 -0
- package/lib/tests/hierarchyService.issue38.test.js +460 -0
- package/lib/tests/processService.comprehensive.test.js +9 -9
- package/lib/tests/processService.test.js +234 -0
- package/lib/tests/restService.test.d.ts +0 -4
- package/lib/tests/restService.test.d.ts.map +1 -1
- package/lib/tests/restService.test.js +1558 -143
- package/lib/tests/subsetService.issue38.test.d.ts +5 -0
- package/lib/tests/subsetService.issue38.test.d.ts.map +1 -0
- package/lib/tests/subsetService.issue38.test.js +113 -0
- package/lib/tests/tm1Service.test.js +80 -8
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/services/ApplicationService.ts +282 -10
- package/src/services/AsyncOperationService.ts +76 -29
- package/src/services/ElementService.ts +322 -1
- package/src/services/FileService.ts +3 -3
- package/src/services/HierarchyService.ts +419 -1
- package/src/services/ProcessService.ts +185 -142
- package/src/services/RestService.ts +1021 -267
- package/src/services/SubsetService.ts +48 -0
- package/src/services/TM1Service.ts +127 -6
- package/src/services/index.ts +1 -1
- package/src/tests/100PercentParityCheck.test.ts +29 -8
- package/src/tests/applicationService.issue38.test.ts +293 -0
- package/src/tests/asyncOperationService.test.ts +52 -48
- package/src/tests/bugfix28.test.ts +12 -4
- package/src/tests/elementService.issue37.test.ts +571 -0
- package/src/tests/elementService.issue38.test.ts +103 -0
- package/src/tests/hierarchyService.issue38.test.ts +599 -0
- package/src/tests/processService.comprehensive.test.ts +10 -10
- package/src/tests/processService.test.ts +295 -3
- package/src/tests/restService.test.ts +1844 -139
- package/src/tests/subsetService.issue38.test.ts +182 -0
- package/src/tests/tm1Service.test.ts +95 -11
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HierarchyService Tests — Issue #38 new methods
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { HierarchyService } from '../services/HierarchyService';
|
|
6
|
+
import { RestService } from '../services/RestService';
|
|
7
|
+
import { Hierarchy } from '../objects/Hierarchy';
|
|
8
|
+
import { Element, ElementType } from '../objects/Element';
|
|
9
|
+
import { ElementAttribute } from '../objects/ElementAttribute';
|
|
10
|
+
import { TM1RestException } from '../exceptions/TM1Exception';
|
|
11
|
+
import { DataFrame } from '../utils/DataFrame';
|
|
12
|
+
|
|
13
|
+
const createMockResponse = (data: any, status: number = 200) => ({
|
|
14
|
+
data,
|
|
15
|
+
status,
|
|
16
|
+
statusText: status === 200 ? 'OK' : status === 201 ? 'Created' : status === 204 ? 'No Content' : 'Error',
|
|
17
|
+
headers: {},
|
|
18
|
+
config: {} as any
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('HierarchyService — Issue #38 new methods', () => {
|
|
22
|
+
let hierarchyService: HierarchyService;
|
|
23
|
+
let mockRestService: jest.Mocked<RestService>;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
mockRestService = {
|
|
27
|
+
get: jest.fn(),
|
|
28
|
+
post: jest.fn(),
|
|
29
|
+
patch: jest.fn(),
|
|
30
|
+
delete: jest.fn(),
|
|
31
|
+
put: jest.fn(),
|
|
32
|
+
config: {} as any,
|
|
33
|
+
rest: {} as any,
|
|
34
|
+
buildBaseUrl: jest.fn(),
|
|
35
|
+
extractErrorMessage: jest.fn()
|
|
36
|
+
} as any;
|
|
37
|
+
|
|
38
|
+
hierarchyService = new HierarchyService(mockRestService);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// ===== updateOrCreate =====
|
|
42
|
+
|
|
43
|
+
describe('updateOrCreate', () => {
|
|
44
|
+
test('should call create when hierarchy does not exist', async () => {
|
|
45
|
+
const hierarchy = new Hierarchy('NewHierarchy', 'TestDimension');
|
|
46
|
+
|
|
47
|
+
// exists() does a GET for $select=Name — return list without the hierarchy
|
|
48
|
+
mockRestService.get.mockImplementation(async (url: string) => {
|
|
49
|
+
if (url.includes('$select=Name')) {
|
|
50
|
+
return createMockResponse({ value: [] });
|
|
51
|
+
}
|
|
52
|
+
// getElementAttributes inside create -> updateElementAttributes
|
|
53
|
+
return createMockResponse({ value: [] });
|
|
54
|
+
});
|
|
55
|
+
mockRestService.post.mockResolvedValue(createMockResponse({ Name: 'NewHierarchy' }, 201));
|
|
56
|
+
|
|
57
|
+
const result = await hierarchyService.updateOrCreate(hierarchy);
|
|
58
|
+
|
|
59
|
+
expect(mockRestService.post).toHaveBeenCalledTimes(1);
|
|
60
|
+
const postUrl = mockRestService.post.mock.calls[0][0];
|
|
61
|
+
expect(postUrl).toContain("/Hierarchies");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('should call update when hierarchy exists', async () => {
|
|
65
|
+
const hierarchy = new Hierarchy('ExistingHierarchy', 'TestDimension');
|
|
66
|
+
|
|
67
|
+
// exists() returns the hierarchy in the list
|
|
68
|
+
mockRestService.get.mockImplementation(async (url: string) => {
|
|
69
|
+
if (url.includes('$select=Name')) {
|
|
70
|
+
return createMockResponse({ value: [{ Name: 'ExistingHierarchy' }] });
|
|
71
|
+
}
|
|
72
|
+
// getElementAttributes inside update -> updateElementAttributes
|
|
73
|
+
return createMockResponse({ value: [] });
|
|
74
|
+
});
|
|
75
|
+
mockRestService.patch.mockResolvedValue(createMockResponse({}, 200));
|
|
76
|
+
|
|
77
|
+
const result = await hierarchyService.updateOrCreate(hierarchy);
|
|
78
|
+
|
|
79
|
+
expect(mockRestService.patch).toHaveBeenCalledTimes(1);
|
|
80
|
+
const patchUrl = mockRestService.patch.mock.calls[0][0];
|
|
81
|
+
expect(patchUrl).toContain("Hierarchies('ExistingHierarchy')");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ===== getHierarchySummary =====
|
|
86
|
+
|
|
87
|
+
describe('getHierarchySummary', () => {
|
|
88
|
+
test('should return correct counts from OData response', async () => {
|
|
89
|
+
mockRestService.get.mockResolvedValue(createMockResponse({
|
|
90
|
+
'Cardinality': 0,
|
|
91
|
+
'Elements@odata.count': 42,
|
|
92
|
+
'Edges@odata.count': 30,
|
|
93
|
+
'ElementAttributes@odata.count': 5,
|
|
94
|
+
'Members@odata.count': 50,
|
|
95
|
+
'Levels@odata.count': 3
|
|
96
|
+
}));
|
|
97
|
+
|
|
98
|
+
const summary = await hierarchyService.getHierarchySummary('TestDimension', 'TestHierarchy');
|
|
99
|
+
|
|
100
|
+
expect(summary.Elements).toBe(42);
|
|
101
|
+
expect(summary.Edges).toBe(30);
|
|
102
|
+
expect(summary.ElementAttributes).toBe(5);
|
|
103
|
+
expect(summary.Members).toBe(50);
|
|
104
|
+
expect(summary.Levels).toBe(3);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('should default hierarchyName to dimensionName when not provided', async () => {
|
|
108
|
+
mockRestService.get.mockResolvedValue(createMockResponse({
|
|
109
|
+
'Elements@odata.count': 10,
|
|
110
|
+
'Edges@odata.count': 5,
|
|
111
|
+
'ElementAttributes@odata.count': 2,
|
|
112
|
+
'Members@odata.count': 10,
|
|
113
|
+
'Levels@odata.count': 1
|
|
114
|
+
}));
|
|
115
|
+
|
|
116
|
+
await hierarchyService.getHierarchySummary('TestDimension');
|
|
117
|
+
|
|
118
|
+
const calledUrl = mockRestService.get.mock.calls[0][0];
|
|
119
|
+
expect(calledUrl).toContain("Hierarchies('TestDimension')");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('should default missing odata counts to 0', async () => {
|
|
123
|
+
mockRestService.get.mockResolvedValue(createMockResponse({}));
|
|
124
|
+
|
|
125
|
+
const summary = await hierarchyService.getHierarchySummary('TestDimension', 'TestHierarchy');
|
|
126
|
+
|
|
127
|
+
expect(summary.Elements).toBe(0);
|
|
128
|
+
expect(summary.Edges).toBe(0);
|
|
129
|
+
expect(summary.ElementAttributes).toBe(0);
|
|
130
|
+
expect(summary.Members).toBe(0);
|
|
131
|
+
expect(summary.Levels).toBe(0);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// ===== getDefaultMember =====
|
|
136
|
+
|
|
137
|
+
describe('getDefaultMember', () => {
|
|
138
|
+
test('should return member name from response', async () => {
|
|
139
|
+
mockRestService.get.mockResolvedValue(createMockResponse({ Name: 'All Members' }));
|
|
140
|
+
|
|
141
|
+
const result = await hierarchyService.getDefaultMember('TestDimension', 'TestHierarchy');
|
|
142
|
+
|
|
143
|
+
expect(result).toBe('All Members');
|
|
144
|
+
const calledUrl = mockRestService.get.mock.calls[0][0];
|
|
145
|
+
expect(calledUrl).toContain('/DefaultMember');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('should return null on 404', async () => {
|
|
149
|
+
mockRestService.get.mockRejectedValue(
|
|
150
|
+
new TM1RestException('Not found', 404, { status: 404 })
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const result = await hierarchyService.getDefaultMember('TestDimension', 'TestHierarchy');
|
|
154
|
+
|
|
155
|
+
expect(result).toBeNull();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('should rethrow non-404 errors', async () => {
|
|
159
|
+
mockRestService.get.mockRejectedValue(
|
|
160
|
+
new TM1RestException('Server error', 500, { status: 500 })
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
await expect(
|
|
164
|
+
hierarchyService.getDefaultMember('TestDimension', 'TestHierarchy')
|
|
165
|
+
).rejects.toThrow();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('should default hierarchyName to dimensionName', async () => {
|
|
169
|
+
mockRestService.get.mockResolvedValue(createMockResponse({ Name: 'Root' }));
|
|
170
|
+
|
|
171
|
+
await hierarchyService.getDefaultMember('TestDimension');
|
|
172
|
+
|
|
173
|
+
const calledUrl = mockRestService.get.mock.calls[0][0];
|
|
174
|
+
expect(calledUrl).toContain("Hierarchies('TestDimension')");
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ===== updateDefaultMember =====
|
|
179
|
+
|
|
180
|
+
describe('updateDefaultMember', () => {
|
|
181
|
+
test('should use API approach (PATCH) when version is 12.0.0', async () => {
|
|
182
|
+
(mockRestService as any).version = '12.0.0';
|
|
183
|
+
mockRestService.patch.mockResolvedValue(createMockResponse({}, 200));
|
|
184
|
+
|
|
185
|
+
await hierarchyService.updateDefaultMember('TestDimension', 'TestHierarchy', 'RootMember');
|
|
186
|
+
|
|
187
|
+
expect(mockRestService.patch).toHaveBeenCalledTimes(1);
|
|
188
|
+
const [url, body] = mockRestService.patch.mock.calls[0];
|
|
189
|
+
expect(url).toContain("Dimensions('TestDimension')/Hierarchies('TestHierarchy')");
|
|
190
|
+
const parsed = JSON.parse(body);
|
|
191
|
+
expect(parsed['DefaultMember@odata.bind']).toContain("Elements('RootMember')");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('should use API approach when version is undefined', async () => {
|
|
195
|
+
(mockRestService as any).version = undefined;
|
|
196
|
+
mockRestService.patch.mockResolvedValue(createMockResponse({}, 200));
|
|
197
|
+
|
|
198
|
+
await hierarchyService.updateDefaultMember('TestDimension', 'TestHierarchy', 'RootMember');
|
|
199
|
+
|
|
200
|
+
expect(mockRestService.patch).toHaveBeenCalledTimes(1);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('should use props cube approach for pre-v12 (version 11.8.0)', async () => {
|
|
204
|
+
(mockRestService as any).version = '11.8.0';
|
|
205
|
+
// CellService.writeValue calls getDimensionNamesForWriting (GET) then POST
|
|
206
|
+
mockRestService.get.mockResolvedValue(createMockResponse({
|
|
207
|
+
Dimensions: [
|
|
208
|
+
{ Name: '}HierarchyProperties' },
|
|
209
|
+
{ Name: '}HierarchyProperties_dim' },
|
|
210
|
+
{ Name: 'MEMBER_DEFAULT' }
|
|
211
|
+
]
|
|
212
|
+
}));
|
|
213
|
+
mockRestService.post.mockResolvedValue(createMockResponse({}, 200));
|
|
214
|
+
|
|
215
|
+
await hierarchyService.updateDefaultMember('TestDimension', 'TestHierarchy', 'RootMember');
|
|
216
|
+
|
|
217
|
+
expect(mockRestService.post).toHaveBeenCalledTimes(1);
|
|
218
|
+
const [url, body] = mockRestService.post.mock.calls[0];
|
|
219
|
+
// The cube name '}HierarchyProperties' gets URL-encoded as '%7DHierarchyProperties'
|
|
220
|
+
expect(url).toContain('HierarchyProperties');
|
|
221
|
+
const parsed = JSON.parse(body);
|
|
222
|
+
expect(parsed.Cells[0].Value).toBe('RootMember');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('should clear default member when memberName is empty string (API approach)', async () => {
|
|
226
|
+
(mockRestService as any).version = '12.0.0';
|
|
227
|
+
mockRestService.patch.mockResolvedValue(createMockResponse({}, 200));
|
|
228
|
+
|
|
229
|
+
await hierarchyService.updateDefaultMember('TestDimension', 'TestHierarchy', '');
|
|
230
|
+
|
|
231
|
+
expect(mockRestService.patch).toHaveBeenCalledTimes(1);
|
|
232
|
+
const [, body] = mockRestService.patch.mock.calls[0];
|
|
233
|
+
const parsed = JSON.parse(body);
|
|
234
|
+
expect(parsed['DefaultMember@odata.bind']).toBeNull();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('should default hierarchyName to dimensionName', async () => {
|
|
238
|
+
(mockRestService as any).version = '12.0.0';
|
|
239
|
+
mockRestService.patch.mockResolvedValue(createMockResponse({}, 200));
|
|
240
|
+
|
|
241
|
+
await hierarchyService.updateDefaultMember('TestDimension', undefined, 'Root');
|
|
242
|
+
|
|
243
|
+
const [url] = mockRestService.patch.mock.calls[0];
|
|
244
|
+
expect(url).toContain("Hierarchies('TestDimension')");
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// ===== removeEdgesUnderConsolidation =====
|
|
249
|
+
|
|
250
|
+
describe('removeEdgesUnderConsolidation', () => {
|
|
251
|
+
test('should remove edges under the specified consolidation element', async () => {
|
|
252
|
+
// Build a hierarchy: Total -> [A, B], A -> [A1, A2]
|
|
253
|
+
const hierarchy = new Hierarchy('TestHierarchy', 'TestDimension');
|
|
254
|
+
hierarchy.addEdge('Total', 'A', 1);
|
|
255
|
+
hierarchy.addEdge('Total', 'B', 1);
|
|
256
|
+
hierarchy.addEdge('A', 'A1', 1);
|
|
257
|
+
hierarchy.addEdge('A', 'A2', 1);
|
|
258
|
+
|
|
259
|
+
let getCallCount = 0;
|
|
260
|
+
mockRestService.get.mockImplementation(async (url: string) => {
|
|
261
|
+
getCallCount++;
|
|
262
|
+
if (getCallCount === 1) {
|
|
263
|
+
// First call is the get(dimensionName, hierarchyName) inside removeEdgesUnderConsolidation
|
|
264
|
+
return createMockResponse({
|
|
265
|
+
Name: 'TestHierarchy',
|
|
266
|
+
Elements: [
|
|
267
|
+
{ Name: 'Total', Type: 'Consolidated' },
|
|
268
|
+
{ Name: 'A', Type: 'Consolidated' },
|
|
269
|
+
{ Name: 'B', Type: 'Numeric' },
|
|
270
|
+
{ Name: 'A1', Type: 'Numeric' },
|
|
271
|
+
{ Name: 'A2', Type: 'Numeric' }
|
|
272
|
+
],
|
|
273
|
+
Edges: [
|
|
274
|
+
{ ParentName: 'Total', ComponentName: 'A', Weight: 1 },
|
|
275
|
+
{ ParentName: 'Total', ComponentName: 'B', Weight: 1 },
|
|
276
|
+
{ ParentName: 'A', ComponentName: 'A1', Weight: 1 },
|
|
277
|
+
{ ParentName: 'A', ComponentName: 'A2', Weight: 1 }
|
|
278
|
+
],
|
|
279
|
+
ElementAttributes: [],
|
|
280
|
+
Subsets: []
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
// Subsequent calls from updateElementAttributes
|
|
284
|
+
return createMockResponse({ value: [] });
|
|
285
|
+
});
|
|
286
|
+
mockRestService.patch.mockResolvedValue(createMockResponse({}, 200));
|
|
287
|
+
|
|
288
|
+
await hierarchyService.removeEdgesUnderConsolidation(
|
|
289
|
+
'TestDimension', 'TestHierarchy', 'A'
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
expect(mockRestService.patch).toHaveBeenCalledTimes(1);
|
|
293
|
+
const [, body] = mockRestService.patch.mock.calls[0];
|
|
294
|
+
const parsed = JSON.parse(body);
|
|
295
|
+
// After removing edges under A, A should have no children in the hierarchy
|
|
296
|
+
const remainingEdges: Array<{ ParentName: string; ComponentName: string }> = parsed.Edges || [];
|
|
297
|
+
const aEdges = remainingEdges.filter((e: any) => e.ParentName === 'A');
|
|
298
|
+
expect(aEdges).toHaveLength(0);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// ===== addEdges =====
|
|
303
|
+
|
|
304
|
+
describe('addEdges', () => {
|
|
305
|
+
test('should delegate to ElementService.addEdges', async () => {
|
|
306
|
+
mockRestService.post.mockResolvedValue(createMockResponse({}, 201));
|
|
307
|
+
|
|
308
|
+
await hierarchyService.addEdges(
|
|
309
|
+
'TestDimension',
|
|
310
|
+
'TestHierarchy',
|
|
311
|
+
{ Parent: { Child: 1 } }
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
expect(mockRestService.post).toHaveBeenCalledTimes(1);
|
|
315
|
+
const [url] = mockRestService.post.mock.calls[0];
|
|
316
|
+
expect(url).toContain("Dimensions('TestDimension')/Hierarchies('TestHierarchy')");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('should use dimensionName as hierarchyName when hierarchyName is undefined', async () => {
|
|
320
|
+
mockRestService.post.mockResolvedValue(createMockResponse({}, 201));
|
|
321
|
+
|
|
322
|
+
await hierarchyService.addEdges('TestDimension', undefined, { P: { C: 1 } });
|
|
323
|
+
|
|
324
|
+
const [url] = mockRestService.post.mock.calls[0];
|
|
325
|
+
expect(url).toContain("Hierarchies('TestDimension')");
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// ===== addElements =====
|
|
330
|
+
|
|
331
|
+
describe('addElements', () => {
|
|
332
|
+
test('should delegate to ElementService.addElements', async () => {
|
|
333
|
+
mockRestService.post.mockResolvedValue(createMockResponse({}, 201));
|
|
334
|
+
const elements = [new Element('Elem1', ElementType.NUMERIC)];
|
|
335
|
+
|
|
336
|
+
await hierarchyService.addElements('TestDimension', 'TestHierarchy', elements);
|
|
337
|
+
|
|
338
|
+
expect(mockRestService.post).toHaveBeenCalledTimes(1);
|
|
339
|
+
const [url] = mockRestService.post.mock.calls[0];
|
|
340
|
+
expect(url).toContain("Dimensions('TestDimension')/Hierarchies('TestHierarchy')/Elements");
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// ===== addElementAttributes =====
|
|
345
|
+
|
|
346
|
+
describe('addElementAttributes', () => {
|
|
347
|
+
test('should delegate to ElementService.addElementAttributes', async () => {
|
|
348
|
+
mockRestService.post.mockResolvedValue(createMockResponse({}, 201));
|
|
349
|
+
const attrs = [new ElementAttribute('Description', 'String')];
|
|
350
|
+
|
|
351
|
+
await hierarchyService.addElementAttributes('TestDimension', 'TestHierarchy', attrs);
|
|
352
|
+
|
|
353
|
+
expect(mockRestService.post).toHaveBeenCalledTimes(1);
|
|
354
|
+
const [url] = mockRestService.post.mock.calls[0];
|
|
355
|
+
expect(url).toContain("Dimensions('TestDimension')/Hierarchies('TestHierarchy')/ElementAttributes");
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// ===== updateOrCreateHierarchyFromDataframe =====
|
|
360
|
+
|
|
361
|
+
describe('updateOrCreateHierarchyFromDataframe', () => {
|
|
362
|
+
// Helper: mock hierarchy response with given elements/edges
|
|
363
|
+
const mockHierarchyResponse = (elements: any[] = [], edges: any[] = []) =>
|
|
364
|
+
createMockResponse({
|
|
365
|
+
Name: 'TestHierarchy',
|
|
366
|
+
Elements: elements,
|
|
367
|
+
Edges: edges,
|
|
368
|
+
ElementAttributes: [],
|
|
369
|
+
Subsets: [],
|
|
370
|
+
DefaultMember: null
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// Helper: mock "hierarchy exists" check (getAllNames-style)
|
|
374
|
+
const setupExistingHierarchy = (elements: any[] = [], edges: any[] = []) => {
|
|
375
|
+
// exists() calls GET /Hierarchies?$select=Name
|
|
376
|
+
mockRestService.get.mockImplementation(async (url: string) => {
|
|
377
|
+
if (url.includes('$select=Name')) {
|
|
378
|
+
return createMockResponse({ value: [{ Name: 'TestHierarchy' }] });
|
|
379
|
+
}
|
|
380
|
+
if (url.includes('$expand=Edges')) {
|
|
381
|
+
return mockHierarchyResponse(elements, edges);
|
|
382
|
+
}
|
|
383
|
+
if (url.includes('ElementAttributes')) {
|
|
384
|
+
return createMockResponse({ value: [] });
|
|
385
|
+
}
|
|
386
|
+
return createMockResponse({});
|
|
387
|
+
});
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const setupNewHierarchy = () => {
|
|
391
|
+
let hierarchyCreated = false;
|
|
392
|
+
mockRestService.get.mockImplementation(async (url: string) => {
|
|
393
|
+
if (url.includes('$select=Name')) {
|
|
394
|
+
return createMockResponse({ value: [] }); // hierarchy doesn't exist
|
|
395
|
+
}
|
|
396
|
+
if (url.includes('$expand=Edges')) {
|
|
397
|
+
return mockHierarchyResponse(); // empty hierarchy after creation
|
|
398
|
+
}
|
|
399
|
+
if (url.includes('ElementAttributes')) {
|
|
400
|
+
return createMockResponse({ value: [] });
|
|
401
|
+
}
|
|
402
|
+
return createMockResponse({});
|
|
403
|
+
});
|
|
404
|
+
mockRestService.post.mockImplementation(async (url: string, body: any) => {
|
|
405
|
+
hierarchyCreated = true;
|
|
406
|
+
return createMockResponse({}, 201);
|
|
407
|
+
});
|
|
408
|
+
mockRestService.patch.mockResolvedValue(createMockResponse({}, 200));
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
test('should add new elements from DataFrame to existing hierarchy', async () => {
|
|
412
|
+
setupExistingHierarchy([
|
|
413
|
+
{ Name: 'Existing1', Type: 'Numeric' }
|
|
414
|
+
]);
|
|
415
|
+
mockRestService.post.mockResolvedValue(createMockResponse({}, 201));
|
|
416
|
+
|
|
417
|
+
const df = new DataFrame([
|
|
418
|
+
['Existing1', 'Numeric'],
|
|
419
|
+
['NewElem1', 'String'],
|
|
420
|
+
['NewElem2', 'Numeric']
|
|
421
|
+
], { columns: ['Element', 'ElementType'] });
|
|
422
|
+
|
|
423
|
+
await hierarchyService.updateOrCreateHierarchyFromDataframe(
|
|
424
|
+
'TestDimension', 'TestHierarchy', df,
|
|
425
|
+
{ elementColumn: 'Element' }
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
// Should POST new elements (NewElem1, NewElem2 but not Existing1)
|
|
429
|
+
const postCalls = mockRestService.post.mock.calls;
|
|
430
|
+
const elementPostCall = postCalls.find(
|
|
431
|
+
([url]) => url.includes('/Elements') && !url.includes('ElementAttributes')
|
|
432
|
+
);
|
|
433
|
+
expect(elementPostCall).toBeDefined();
|
|
434
|
+
const postedBody = JSON.parse(elementPostCall![1]);
|
|
435
|
+
expect(postedBody).toHaveLength(2);
|
|
436
|
+
expect(postedBody.map((e: any) => e.Name).sort()).toEqual(['NewElem1', 'NewElem2']);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test('should throw on duplicate elements when verifyUniqueElements=true', async () => {
|
|
440
|
+
const df = new DataFrame([
|
|
441
|
+
['Elem1', 'Numeric'],
|
|
442
|
+
['elem1', 'String'], // duplicate (case-insensitive)
|
|
443
|
+
], { columns: ['Element', 'ElementType'] });
|
|
444
|
+
|
|
445
|
+
await expect(
|
|
446
|
+
hierarchyService.updateOrCreateHierarchyFromDataframe(
|
|
447
|
+
'TestDimension', 'TestHierarchy', df,
|
|
448
|
+
{ elementColumn: 'Element', verifyUniqueElements: true }
|
|
449
|
+
)
|
|
450
|
+
).rejects.toThrow('Duplicate element');
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test('should create dimension when hierarchy does not exist', async () => {
|
|
454
|
+
setupNewHierarchy();
|
|
455
|
+
|
|
456
|
+
const df = new DataFrame([
|
|
457
|
+
['Elem1', 'Numeric']
|
|
458
|
+
], { columns: ['Element', 'ElementType'] });
|
|
459
|
+
|
|
460
|
+
await hierarchyService.updateOrCreateHierarchyFromDataframe(
|
|
461
|
+
'NewDimension', 'NewHierarchy', df,
|
|
462
|
+
{ elementColumn: 'Element' }
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
// Should have called POST for dimension creation
|
|
466
|
+
const postCalls = mockRestService.post.mock.calls;
|
|
467
|
+
const dimCreateCall = postCalls.find(([url]) => url.includes('/Dimensions'));
|
|
468
|
+
expect(dimCreateCall).toBeDefined();
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test('should unwind all edges when unwindAll=true', async () => {
|
|
472
|
+
setupExistingHierarchy(
|
|
473
|
+
[{ Name: 'Parent', Type: 'Consolidated' }, { Name: 'Child', Type: 'Numeric' }],
|
|
474
|
+
[{ ParentName: 'Parent', ComponentName: 'Child', Weight: 1 }]
|
|
475
|
+
);
|
|
476
|
+
mockRestService.post.mockResolvedValue(createMockResponse({}, 201));
|
|
477
|
+
mockRestService.patch.mockResolvedValue(createMockResponse({}, 200));
|
|
478
|
+
|
|
479
|
+
const df = new DataFrame([
|
|
480
|
+
['Child', 'Numeric']
|
|
481
|
+
], { columns: ['Element', 'ElementType'] });
|
|
482
|
+
|
|
483
|
+
await hierarchyService.updateOrCreateHierarchyFromDataframe(
|
|
484
|
+
'TestDimension', 'TestHierarchy', df,
|
|
485
|
+
{ elementColumn: 'Element', unwindAll: true }
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
// Should PATCH to remove all edges (empty Edges array)
|
|
489
|
+
const patchCalls = mockRestService.patch.mock.calls;
|
|
490
|
+
const edgePatch = patchCalls.find(([_, body]) => {
|
|
491
|
+
try { return JSON.parse(body).Edges !== undefined; } catch { return false; }
|
|
492
|
+
});
|
|
493
|
+
expect(edgePatch).toBeDefined();
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test('should re-throw unexpected errors from bulk edge add', async () => {
|
|
497
|
+
setupExistingHierarchy();
|
|
498
|
+
mockRestService.post.mockImplementation(async (url: string) => {
|
|
499
|
+
if (url.includes('/Edges')) {
|
|
500
|
+
throw new TM1RestException('Server error', 500, { status: 500 });
|
|
501
|
+
}
|
|
502
|
+
if (url.includes('/Elements')) {
|
|
503
|
+
return createMockResponse({}, 201);
|
|
504
|
+
}
|
|
505
|
+
return createMockResponse({}, 201);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
const df = new DataFrame([
|
|
509
|
+
['Child', 'Numeric', 'Parent'],
|
|
510
|
+
], { columns: ['Element', 'ElementType', 'Level001'] });
|
|
511
|
+
|
|
512
|
+
await expect(
|
|
513
|
+
hierarchyService.updateOrCreateHierarchyFromDataframe(
|
|
514
|
+
'TestDimension', 'TestHierarchy', df,
|
|
515
|
+
{ elementColumn: 'Element' }
|
|
516
|
+
)
|
|
517
|
+
).rejects.toThrow('Server error');
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
test('should fall back to individual edges on 400 from bulk', async () => {
|
|
521
|
+
setupExistingHierarchy();
|
|
522
|
+
let bulkAttempted = false;
|
|
523
|
+
mockRestService.post.mockImplementation(async (url: string, body: any) => {
|
|
524
|
+
if (url.includes('/Edges')) {
|
|
525
|
+
if (!bulkAttempted) {
|
|
526
|
+
bulkAttempted = true;
|
|
527
|
+
throw new TM1RestException('Bad request', 400, { status: 400 });
|
|
528
|
+
}
|
|
529
|
+
// Individual edge adds succeed
|
|
530
|
+
return createMockResponse({}, 201);
|
|
531
|
+
}
|
|
532
|
+
if (url.includes('/Elements')) {
|
|
533
|
+
return createMockResponse({}, 201);
|
|
534
|
+
}
|
|
535
|
+
return createMockResponse({}, 201);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
const df = new DataFrame([
|
|
539
|
+
['Child', 'Numeric', 'Parent'],
|
|
540
|
+
], { columns: ['Element', 'ElementType', 'Level001'] });
|
|
541
|
+
|
|
542
|
+
await hierarchyService.updateOrCreateHierarchyFromDataframe(
|
|
543
|
+
'TestDimension', 'TestHierarchy', df,
|
|
544
|
+
{ elementColumn: 'Element' }
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
// Bulk failed, then individual edge was attempted
|
|
548
|
+
const edgeCalls = mockRestService.post.mock.calls.filter(
|
|
549
|
+
([url]) => url.includes('/Edges')
|
|
550
|
+
);
|
|
551
|
+
expect(edgeCalls.length).toBeGreaterThan(1); // bulk + individual
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test('should delete orphaned consolidations when requested', async () => {
|
|
555
|
+
// Setup: hierarchy has a Consolidated element "Orphan" with no children
|
|
556
|
+
const getCallCount = { n: 0 };
|
|
557
|
+
mockRestService.get.mockImplementation(async (url: string) => {
|
|
558
|
+
if (url.includes('$select=Name')) {
|
|
559
|
+
return createMockResponse({ value: [{ Name: 'TestHierarchy' }] });
|
|
560
|
+
}
|
|
561
|
+
if (url.includes('$expand=Edges')) {
|
|
562
|
+
getCallCount.n++;
|
|
563
|
+
// Return hierarchy with orphaned consolidation
|
|
564
|
+
return createMockResponse({
|
|
565
|
+
Name: 'TestHierarchy',
|
|
566
|
+
Elements: [
|
|
567
|
+
{ Name: 'Leaf', Type: 'Numeric' },
|
|
568
|
+
{ Name: 'Orphan', Type: 'Consolidated' }
|
|
569
|
+
],
|
|
570
|
+
Edges: [], // No edges — Orphan is orphaned
|
|
571
|
+
ElementAttributes: [],
|
|
572
|
+
Subsets: [],
|
|
573
|
+
DefaultMember: null
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
if (url.includes('ElementAttributes')) {
|
|
577
|
+
return createMockResponse({ value: [] });
|
|
578
|
+
}
|
|
579
|
+
return createMockResponse({});
|
|
580
|
+
});
|
|
581
|
+
mockRestService.post.mockResolvedValue(createMockResponse({}, 201));
|
|
582
|
+
mockRestService.delete.mockResolvedValue(createMockResponse({}, 204));
|
|
583
|
+
|
|
584
|
+
const df = new DataFrame([
|
|
585
|
+
['Leaf', 'Numeric']
|
|
586
|
+
], { columns: ['Element', 'ElementType'] });
|
|
587
|
+
|
|
588
|
+
await hierarchyService.updateOrCreateHierarchyFromDataframe(
|
|
589
|
+
'TestDimension', 'TestHierarchy', df,
|
|
590
|
+
{ elementColumn: 'Element', deleteOrphanedConsolidations: true }
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
// Should DELETE the orphaned consolidation
|
|
594
|
+
const deleteCalls = mockRestService.delete.mock.calls;
|
|
595
|
+
const orphanDelete = deleteCalls.find(([url]) => url.includes("Elements('Orphan')"));
|
|
596
|
+
expect(orphanDelete).toBeDefined();
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
});
|
|
@@ -307,7 +307,7 @@ describe('ProcessService - Comprehensive Tests', () => {
|
|
|
307
307
|
expect(mockRestService.post).toHaveBeenCalledWith(
|
|
308
308
|
"/Processes('TestProcess')/tm1.ExecuteWithReturn?$expand=*",
|
|
309
309
|
"{}",
|
|
310
|
-
{ timeout:
|
|
310
|
+
{ timeout: 30 }
|
|
311
311
|
);
|
|
312
312
|
});
|
|
313
313
|
|
|
@@ -538,17 +538,17 @@ describe('ProcessService - Comprehensive Tests', () => {
|
|
|
538
538
|
};
|
|
539
539
|
mockRestService.get.mockResolvedValue(mockResponse(breakpointsData));
|
|
540
540
|
|
|
541
|
-
const result = await processService.
|
|
541
|
+
const result = await processService.debugGetBreakpoints('debug-123');
|
|
542
542
|
|
|
543
543
|
expect(result).toHaveLength(2);
|
|
544
544
|
expect(ProcessDebugBreakpoint.fromDict).toHaveBeenCalledTimes(2);
|
|
545
545
|
expect(mockRestService.get).toHaveBeenCalledWith("/ProcessDebugContexts('debug-123')/Breakpoints");
|
|
546
546
|
});
|
|
547
547
|
|
|
548
|
-
test('should
|
|
548
|
+
test('should add process debug breakpoint (delegates to debugAddBreakpoints)', async () => {
|
|
549
549
|
mockRestService.post.mockResolvedValue(mockResponse({}));
|
|
550
550
|
|
|
551
|
-
const result = await processService.
|
|
551
|
+
const result = await processService.debugAddBreakpoint(
|
|
552
552
|
'debug-123',
|
|
553
553
|
mockProcessDebugBreakpoint
|
|
554
554
|
);
|
|
@@ -556,14 +556,14 @@ describe('ProcessService - Comprehensive Tests', () => {
|
|
|
556
556
|
expect(result).toBeDefined();
|
|
557
557
|
expect(mockRestService.post).toHaveBeenCalledWith(
|
|
558
558
|
"/ProcessDebugContexts('debug-123')/Breakpoints",
|
|
559
|
-
mockProcessDebugBreakpoint.
|
|
559
|
+
JSON.stringify([mockProcessDebugBreakpoint.bodyAsDict])
|
|
560
560
|
);
|
|
561
561
|
});
|
|
562
562
|
|
|
563
563
|
test('should delete process debug breakpoint', async () => {
|
|
564
564
|
mockRestService.delete.mockResolvedValue(mockResponse({}));
|
|
565
565
|
|
|
566
|
-
const result = await processService.
|
|
566
|
+
const result = await processService.debugRemoveBreakpoint('debug-123', 5);
|
|
567
567
|
|
|
568
568
|
expect(result).toBeDefined();
|
|
569
569
|
expect(mockRestService.delete).toHaveBeenCalledWith("/ProcessDebugContexts('debug-123')/Breakpoints('5')");
|
|
@@ -940,7 +940,7 @@ describe('ProcessService - Comprehensive Tests', () => {
|
|
|
940
940
|
expect(mockRestService.post).toHaveBeenCalledWith(
|
|
941
941
|
expect.any(String),
|
|
942
942
|
expect.any(String),
|
|
943
|
-
{ timeout:
|
|
943
|
+
{ timeout: 60 }
|
|
944
944
|
);
|
|
945
945
|
});
|
|
946
946
|
|
|
@@ -1224,10 +1224,10 @@ describe('ProcessService - Comprehensive Tests', () => {
|
|
|
1224
1224
|
mockRestService.delete.mockResolvedValue(mockResponse({}));
|
|
1225
1225
|
|
|
1226
1226
|
// Debug workflow: set breakpoint, run debug commands, remove breakpoint
|
|
1227
|
-
await processService.
|
|
1227
|
+
await processService.debugAddBreakpoint('debug-123', mockProcessDebugBreakpoint);
|
|
1228
1228
|
await processService.debugStepOver('debug-123');
|
|
1229
1229
|
await processService.debugContinue('debug-123');
|
|
1230
|
-
await processService.
|
|
1230
|
+
await processService.debugRemoveBreakpoint('debug-123', 5);
|
|
1231
1231
|
|
|
1232
1232
|
expect(mockRestService.post).toHaveBeenCalledTimes(3);
|
|
1233
1233
|
expect(mockRestService.delete).toHaveBeenCalledTimes(1);
|
|
@@ -1244,4 +1244,4 @@ describe('ProcessService - Comprehensive Tests', () => {
|
|
|
1244
1244
|
expect(mockRestService.get).toHaveBeenCalledTimes(3);
|
|
1245
1245
|
});
|
|
1246
1246
|
});
|
|
1247
|
-
});
|
|
1247
|
+
});
|