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.
Files changed (78) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/lib/index.d.ts +1 -1
  3. package/lib/index.d.ts.map +1 -1
  4. package/lib/services/ApplicationService.d.ts +19 -3
  5. package/lib/services/ApplicationService.d.ts.map +1 -1
  6. package/lib/services/ApplicationService.js +232 -6
  7. package/lib/services/AsyncOperationService.d.ts +8 -1
  8. package/lib/services/AsyncOperationService.d.ts.map +1 -1
  9. package/lib/services/AsyncOperationService.js +69 -26
  10. package/lib/services/ElementService.d.ts +67 -1
  11. package/lib/services/ElementService.d.ts.map +1 -1
  12. package/lib/services/ElementService.js +214 -0
  13. package/lib/services/FileService.d.ts.map +1 -1
  14. package/lib/services/HierarchyService.d.ts +26 -0
  15. package/lib/services/HierarchyService.d.ts.map +1 -1
  16. package/lib/services/HierarchyService.js +306 -0
  17. package/lib/services/ProcessService.d.ts +40 -22
  18. package/lib/services/ProcessService.d.ts.map +1 -1
  19. package/lib/services/ProcessService.js +118 -111
  20. package/lib/services/RestService.d.ts +213 -25
  21. package/lib/services/RestService.d.ts.map +1 -1
  22. package/lib/services/RestService.js +841 -263
  23. package/lib/services/SubsetService.d.ts +2 -0
  24. package/lib/services/SubsetService.d.ts.map +1 -1
  25. package/lib/services/SubsetService.js +33 -0
  26. package/lib/services/TM1Service.d.ts +44 -1
  27. package/lib/services/TM1Service.d.ts.map +1 -1
  28. package/lib/services/TM1Service.js +96 -4
  29. package/lib/services/index.d.ts +1 -1
  30. package/lib/services/index.d.ts.map +1 -1
  31. package/lib/tests/100PercentParityCheck.test.js +23 -6
  32. package/lib/tests/applicationService.issue38.test.d.ts +5 -0
  33. package/lib/tests/applicationService.issue38.test.d.ts.map +1 -0
  34. package/lib/tests/applicationService.issue38.test.js +237 -0
  35. package/lib/tests/asyncOperationService.test.js +51 -45
  36. package/lib/tests/bugfix28.test.js +12 -4
  37. package/lib/tests/elementService.issue37.test.d.ts +5 -0
  38. package/lib/tests/elementService.issue37.test.d.ts.map +1 -0
  39. package/lib/tests/elementService.issue37.test.js +413 -0
  40. package/lib/tests/elementService.issue38.test.d.ts +5 -0
  41. package/lib/tests/elementService.issue38.test.d.ts.map +1 -0
  42. package/lib/tests/elementService.issue38.test.js +79 -0
  43. package/lib/tests/hierarchyService.issue38.test.d.ts +5 -0
  44. package/lib/tests/hierarchyService.issue38.test.d.ts.map +1 -0
  45. package/lib/tests/hierarchyService.issue38.test.js +460 -0
  46. package/lib/tests/processService.comprehensive.test.js +9 -9
  47. package/lib/tests/processService.test.js +234 -0
  48. package/lib/tests/restService.test.d.ts +0 -4
  49. package/lib/tests/restService.test.d.ts.map +1 -1
  50. package/lib/tests/restService.test.js +1558 -143
  51. package/lib/tests/subsetService.issue38.test.d.ts +5 -0
  52. package/lib/tests/subsetService.issue38.test.d.ts.map +1 -0
  53. package/lib/tests/subsetService.issue38.test.js +113 -0
  54. package/lib/tests/tm1Service.test.js +80 -8
  55. package/package.json +1 -1
  56. package/src/index.ts +1 -1
  57. package/src/services/ApplicationService.ts +282 -10
  58. package/src/services/AsyncOperationService.ts +76 -29
  59. package/src/services/ElementService.ts +322 -1
  60. package/src/services/FileService.ts +3 -3
  61. package/src/services/HierarchyService.ts +419 -1
  62. package/src/services/ProcessService.ts +185 -142
  63. package/src/services/RestService.ts +1021 -267
  64. package/src/services/SubsetService.ts +48 -0
  65. package/src/services/TM1Service.ts +127 -6
  66. package/src/services/index.ts +1 -1
  67. package/src/tests/100PercentParityCheck.test.ts +29 -8
  68. package/src/tests/applicationService.issue38.test.ts +293 -0
  69. package/src/tests/asyncOperationService.test.ts +52 -48
  70. package/src/tests/bugfix28.test.ts +12 -4
  71. package/src/tests/elementService.issue37.test.ts +571 -0
  72. package/src/tests/elementService.issue38.test.ts +103 -0
  73. package/src/tests/hierarchyService.issue38.test.ts +599 -0
  74. package/src/tests/processService.comprehensive.test.ts +10 -10
  75. package/src/tests/processService.test.ts +295 -3
  76. package/src/tests/restService.test.ts +1844 -139
  77. package/src/tests/subsetService.issue38.test.ts +182 -0
  78. 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: 30000 }
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.getProcessDebugBreakpoints('debug-123');
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 create process debug breakpoint', async () => {
548
+ test('should add process debug breakpoint (delegates to debugAddBreakpoints)', async () => {
549
549
  mockRestService.post.mockResolvedValue(mockResponse({}));
550
550
 
551
- const result = await processService.createProcessDebugBreakpoint(
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.body
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.deleteProcessDebugBreakpoint('debug-123', 5);
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: 60000 }
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.createProcessDebugBreakpoint('debug-123', mockProcessDebugBreakpoint);
1227
+ await processService.debugAddBreakpoint('debug-123', mockProcessDebugBreakpoint);
1228
1228
  await processService.debugStepOver('debug-123');
1229
1229
  await processService.debugContinue('debug-123');
1230
- await processService.deleteProcessDebugBreakpoint('debug-123', 5);
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
+ });