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
@@ -273,8 +273,11 @@ describe('AsyncOperationService', () => {
273
273
  name: 'View1'
274
274
  });
275
275
  asyncService.updateOperationStatus(id3, AsyncOperationService_1.OperationStatus.COMPLETED);
276
+ // HTTP 202 = still running on the /_async endpoint.
276
277
  mockRestService.get = jest.fn().mockResolvedValue({
277
- data: { Status: 'Running' }
278
+ status: 202,
279
+ headers: {},
280
+ data: {}
278
281
  });
279
282
  const activeOps = await asyncService.listActiveAsyncOperations();
280
283
  expect(activeOps.length).toBe(2);
@@ -479,70 +482,73 @@ describe('AsyncOperationService', () => {
479
482
  expect(callback).toHaveBeenCalled();
480
483
  });
481
484
  });
485
+ // Status is inferred from the /_async('{id}') HTTP status code and the
486
+ // v12 `asyncresult` header; there is no `.Status` envelope on the new endpoint.
487
+ // These tests bypass createAsyncOperation (which tags operations as
488
+ // trackedLocally: true) to exercise the server-polling branch directly.
482
489
  describe('Server Status Mapping', () => {
483
- test('should map CompletedSuccessfully to COMPLETED', async () => {
484
- const operationId = await asyncService.createAsyncOperation({
490
+ const injectServerOperation = (id) => {
491
+ asyncService.operations.set(id, {
492
+ id,
485
493
  type: AsyncOperationService_1.OperationType.PROCESS_EXECUTION,
486
- name: 'TestProcess'
494
+ name: 'TestProcess',
495
+ status: AsyncOperationService_1.OperationStatus.RUNNING,
496
+ startTime: new Date(),
497
+ trackedLocally: false
487
498
  });
488
- asyncService.updateOperationStatus(operationId, AsyncOperationService_1.OperationStatus.RUNNING);
499
+ };
500
+ test('should map HTTP 200 without asyncresult header to COMPLETED', async () => {
501
+ injectServerOperation('srv-1');
489
502
  mockRestService.get = jest.fn().mockResolvedValue({
490
- data: { Status: 'CompletedSuccessfully', Result: { value: 42 } }
503
+ status: 200,
504
+ headers: {},
505
+ data: { value: 42 }
491
506
  });
492
- const status = await asyncService.getAsyncOperationStatus(operationId);
507
+ const status = await asyncService.getAsyncOperationStatus('srv-1');
493
508
  expect(status).toBe(AsyncOperationService_1.OperationStatus.COMPLETED);
494
- const operation = asyncService.getOperation(operationId);
509
+ const operation = asyncService.getOperation('srv-1');
495
510
  expect(operation === null || operation === void 0 ? void 0 : operation.result).toEqual({ value: 42 });
496
511
  });
497
- test('should map CompletedWithErrors to FAILED', async () => {
498
- const operationId = await asyncService.createAsyncOperation({
499
- type: AsyncOperationService_1.OperationType.PROCESS_EXECUTION,
500
- name: 'TestProcess'
501
- });
502
- asyncService.updateOperationStatus(operationId, AsyncOperationService_1.OperationStatus.RUNNING);
512
+ test('should map HTTP 200 with non-2xx asyncresult header to FAILED', async () => {
513
+ injectServerOperation('srv-2');
503
514
  mockRestService.get = jest.fn().mockResolvedValue({
504
- data: { Status: 'CompletedWithErrors', Error: 'Process failed at line 10' }
515
+ status: 200,
516
+ headers: { asyncresult: '500 Internal Server Error' },
517
+ data: {}
505
518
  });
506
- const status = await asyncService.getAsyncOperationStatus(operationId);
519
+ const status = await asyncService.getAsyncOperationStatus('srv-2');
520
+ expect(status).toBe(AsyncOperationService_1.OperationStatus.FAILED);
521
+ const operation = asyncService.getOperation('srv-2');
522
+ expect(operation === null || operation === void 0 ? void 0 : operation.error).toBe('500 Internal Server Error');
523
+ });
524
+ test('should map thrown TM1RestException to FAILED', async () => {
525
+ injectServerOperation('srv-3');
526
+ const { TM1RestException } = require('../exceptions/TM1Exception');
527
+ mockRestService.get = jest.fn().mockRejectedValue(new TM1RestException('Server error', 500));
528
+ const status = await asyncService.getAsyncOperationStatus('srv-3');
507
529
  expect(status).toBe(AsyncOperationService_1.OperationStatus.FAILED);
508
- const operation = asyncService.getOperation(operationId);
509
- expect(operation === null || operation === void 0 ? void 0 : operation.error).toBe('Process failed at line 10');
510
- });
511
- test('should map Cancelled status correctly', async () => {
512
- const operationId = await asyncService.createAsyncOperation({
513
- type: AsyncOperationService_1.OperationType.PROCESS_EXECUTION,
514
- name: 'TestProcess'
515
- });
516
- asyncService.updateOperationStatus(operationId, AsyncOperationService_1.OperationStatus.RUNNING);
517
- mockRestService.get = jest.fn().mockResolvedValue({
518
- data: { Status: 'Cancelled' }
519
- });
520
- const status = await asyncService.getAsyncOperationStatus(operationId);
521
- expect(status).toBe(AsyncOperationService_1.OperationStatus.CANCELLED);
522
530
  });
523
- test('should map Timeout status correctly', async () => {
524
- const operationId = await asyncService.createAsyncOperation({
525
- type: AsyncOperationService_1.OperationType.PROCESS_EXECUTION,
526
- name: 'TestProcess'
527
- });
528
- asyncService.updateOperationStatus(operationId, AsyncOperationService_1.OperationStatus.RUNNING);
531
+ test('should map HTTP 202 to RUNNING', async () => {
532
+ injectServerOperation('srv-4');
529
533
  mockRestService.get = jest.fn().mockResolvedValue({
530
- data: { Status: 'Timeout' }
534
+ status: 202,
535
+ headers: {},
536
+ data: {}
531
537
  });
532
- const status = await asyncService.getAsyncOperationStatus(operationId);
533
- expect(status).toBe(AsyncOperationService_1.OperationStatus.TIMEOUT);
538
+ const status = await asyncService.getAsyncOperationStatus('srv-4');
539
+ expect(status).toBe(AsyncOperationService_1.OperationStatus.RUNNING);
534
540
  });
535
- test('should default to PENDING for unknown status', async () => {
541
+ test('locally-tracked operations skip server polling and return cached status', async () => {
536
542
  const operationId = await asyncService.createAsyncOperation({
537
543
  type: AsyncOperationService_1.OperationType.PROCESS_EXECUTION,
538
- name: 'TestProcess'
544
+ name: 'LocalProcess'
539
545
  });
540
546
  asyncService.updateOperationStatus(operationId, AsyncOperationService_1.OperationStatus.RUNNING);
541
- mockRestService.get = jest.fn().mockResolvedValue({
542
- data: { Status: 'UnknownStatus' }
543
- });
547
+ const getSpy = jest.fn();
548
+ mockRestService.get = getSpy;
544
549
  const status = await asyncService.getAsyncOperationStatus(operationId);
545
- expect(status).toBe(AsyncOperationService_1.OperationStatus.PENDING);
550
+ expect(status).toBe(AsyncOperationService_1.OperationStatus.RUNNING);
551
+ expect(getSpy).not.toHaveBeenCalled();
546
552
  });
547
553
  });
548
554
  describe('Concurrent Operations', () => {
@@ -11,6 +11,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
11
11
  const SubsetService_1 = require("../services/SubsetService");
12
12
  const ApplicationService_1 = require("../services/ApplicationService");
13
13
  const SessionService_1 = require("../services/SessionService");
14
+ const TM1Exception_1 = require("../exceptions/TM1Exception");
14
15
  const Application_1 = require("../objects/Application");
15
16
  const createMockResponse = (data, status = 200) => ({
16
17
  data,
@@ -126,11 +127,18 @@ describe('Bug #12 - ApplicationService getNames() should not inject /api/v1/ pre
126
127
  expect(calledUrl).toBe("/Contents('Applications')/Contents('Planning')/Contents");
127
128
  });
128
129
  test('getNames() with isPrivate should use PrivateContents', async () => {
129
- mockRest.get.mockResolvedValue(createMockResponse({ value: [{ Name: 'App1' }] }));
130
+ // _resolvePath probes public path first (returns 404), then tries private
131
+ const notFound = new TM1Exception_1.TM1RestException('Not Found', 404, { status: 404 });
132
+ mockRest.get
133
+ .mockRejectedValueOnce(notFound) // public probe → 404
134
+ .mockRejectedValueOnce(notFound) // private probe → 404 (falls through to findBoundary)
135
+ .mockResolvedValueOnce(createMockResponse({ value: [] })) // boundary probe succeeds
136
+ .mockResolvedValueOnce(createMockResponse({ value: [{ Name: 'App1' }] })); // actual getNames
130
137
  await appService.getNames('Planning', true);
131
- const calledUrl = mockRest.get.mock.calls[0][0];
132
- expect(calledUrl).not.toContain('/api/v1/');
133
- expect(calledUrl).toBe("/Contents('Applications')/Contents('Planning')/PrivateContents");
138
+ // The final GET should target PrivateContents
139
+ const lastCall = mockRest.get.mock.calls[mockRest.get.mock.calls.length - 1][0];
140
+ expect(lastCall).not.toContain('/api/v1/');
141
+ expect(lastCall).toContain('PrivateContents');
134
142
  });
135
143
  test('getNames() with empty path', async () => {
136
144
  mockRest.get.mockResolvedValue(createMockResponse({ value: [{ Name: 'App1' }] }));
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Tests for ElementService issue #37 — 13 missing methods for tm1py parity
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=elementService.issue37.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elementService.issue37.test.d.ts","sourceRoot":"","sources":["../../src/tests/elementService.issue37.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
@@ -0,0 +1,413 @@
1
+ "use strict";
2
+ /**
3
+ * Tests for ElementService issue #37 — 13 missing methods for tm1py parity
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const ElementService_1 = require("../services/ElementService");
7
+ const TM1Exception_1 = require("../exceptions/TM1Exception");
8
+ const createMockResponse = (data, status = 200) => ({
9
+ data,
10
+ status,
11
+ statusText: 'OK',
12
+ headers: {},
13
+ config: {}
14
+ });
15
+ describe('ElementService — Issue #37: 13 missing methods', () => {
16
+ let elementService;
17
+ let mockRestService;
18
+ beforeEach(() => {
19
+ mockRestService = {
20
+ get: jest.fn(),
21
+ post: jest.fn(),
22
+ patch: jest.fn(),
23
+ delete: jest.fn(),
24
+ put: jest.fn(),
25
+ config: {},
26
+ rest: {},
27
+ buildBaseUrl: jest.fn(),
28
+ extractErrorMessage: jest.fn()
29
+ };
30
+ elementService = new ElementService_1.ElementService(mockRestService);
31
+ });
32
+ // ===== COUNT METHODS =====
33
+ describe('getNumberOfConsolidatedElements', () => {
34
+ test('should return count with Type eq 3 filter', async () => {
35
+ mockRestService.get.mockResolvedValue(createMockResponse('5'));
36
+ const count = await elementService.getNumberOfConsolidatedElements('Dim1', 'Hier1');
37
+ expect(count).toBe(5);
38
+ expect(mockRestService.get).toHaveBeenCalledWith(expect.stringContaining('$filter=Type eq 3'));
39
+ expect(mockRestService.get).toHaveBeenCalledWith(expect.stringContaining('$count'));
40
+ });
41
+ test('should return 0 for empty response', async () => {
42
+ mockRestService.get.mockResolvedValue(createMockResponse(''));
43
+ const count = await elementService.getNumberOfConsolidatedElements('Dim1', 'Hier1');
44
+ expect(count).toBe(0);
45
+ });
46
+ });
47
+ describe('getNumberOfLeafElements', () => {
48
+ test('should return count with Type ne 3 filter', async () => {
49
+ mockRestService.get.mockResolvedValue(createMockResponse('10'));
50
+ const count = await elementService.getNumberOfLeafElements('Dim1', 'Hier1');
51
+ expect(count).toBe(10);
52
+ expect(mockRestService.get).toHaveBeenCalledWith(expect.stringContaining('$filter=Type ne 3'));
53
+ });
54
+ });
55
+ describe('getNumberOfNumericElements', () => {
56
+ test('should return count with Type eq 1 filter', async () => {
57
+ mockRestService.get.mockResolvedValue(createMockResponse('7'));
58
+ const count = await elementService.getNumberOfNumericElements('Dim1', 'Hier1');
59
+ expect(count).toBe(7);
60
+ expect(mockRestService.get).toHaveBeenCalledWith(expect.stringContaining('$filter=Type eq 1'));
61
+ });
62
+ });
63
+ describe('getNumberOfStringElements', () => {
64
+ test('should return count with Type eq 2 filter', async () => {
65
+ mockRestService.get.mockResolvedValue(createMockResponse('3'));
66
+ const count = await elementService.getNumberOfStringElements('Dim1', 'Hier1');
67
+ expect(count).toBe(3);
68
+ expect(mockRestService.get).toHaveBeenCalledWith(expect.stringContaining('$filter=Type eq 2'));
69
+ });
70
+ });
71
+ // ===== IDENTIFIER METHODS =====
72
+ describe('getElementTypesFromAllHierarchies', () => {
73
+ test('should return element types from all hierarchies', async () => {
74
+ mockRestService.get.mockResolvedValue(createMockResponse({
75
+ Hierarchies: [
76
+ {
77
+ Elements: [
78
+ { Name: 'Elem1', Type: 'Numeric' },
79
+ { Name: 'Elem2', Type: 'String' }
80
+ ]
81
+ },
82
+ {
83
+ Elements: [
84
+ { Name: 'Elem3', Type: 'Consolidated' }
85
+ ]
86
+ }
87
+ ]
88
+ }));
89
+ const result = await elementService.getElementTypesFromAllHierarchies('TestDim');
90
+ expect(result.get('Elem1')).toBe('Numeric');
91
+ expect(result.get('Elem2')).toBe('String');
92
+ expect(result.get('Elem3')).toBe('Consolidated');
93
+ });
94
+ test('should include filter when skipConsolidations is true', async () => {
95
+ mockRestService.get.mockResolvedValue(createMockResponse({
96
+ Hierarchies: [{ Elements: [{ Name: 'Elem1', Type: 'Numeric' }] }]
97
+ }));
98
+ await elementService.getElementTypesFromAllHierarchies('TestDim', true);
99
+ expect(mockRestService.get).toHaveBeenCalledWith(expect.stringContaining('$filter=Type ne 3'));
100
+ });
101
+ test('should not include filter when skipConsolidations is false', async () => {
102
+ mockRestService.get.mockResolvedValue(createMockResponse({
103
+ Hierarchies: [{ Elements: [{ Name: 'Elem1', Type: 'Numeric' }] }]
104
+ }));
105
+ await elementService.getElementTypesFromAllHierarchies('TestDim', false);
106
+ const calledUrl = mockRestService.get.mock.calls[0][0];
107
+ expect(calledUrl).not.toContain('$filter');
108
+ });
109
+ test('should be case-and-space-insensitive', async () => {
110
+ mockRestService.get.mockResolvedValue(createMockResponse({
111
+ Hierarchies: [
112
+ { Elements: [{ Name: 'My Element', Type: 'Numeric' }] }
113
+ ]
114
+ }));
115
+ const result = await elementService.getElementTypesFromAllHierarchies('TestDim');
116
+ expect(result.get('myelement')).toBe('Numeric');
117
+ expect(result.get('MY ELEMENT')).toBe('Numeric');
118
+ });
119
+ });
120
+ describe('getAllLeafElementIdentifiers', () => {
121
+ test('should return element names when no alias attributes exist', async () => {
122
+ // Mock getAliasElementAttributes → empty
123
+ jest.spyOn(elementService, 'getAliasElementAttributes').mockResolvedValue([]);
124
+ // Mock executeSetMdx → returns members
125
+ jest.spyOn(elementService, 'executeSetMdx').mockResolvedValue([
126
+ [{ Name: 'Leaf1' }],
127
+ [{ Name: 'Leaf2' }],
128
+ [{ Name: 'Leaf3' }]
129
+ ]);
130
+ const result = await elementService.getAllLeafElementIdentifiers('Dim1', 'Hier1');
131
+ expect(result.has('Leaf1')).toBe(true);
132
+ expect(result.has('Leaf2')).toBe(true);
133
+ expect(result.has('Leaf3')).toBe(true);
134
+ expect(result.has('leaf1')).toBe(true); // case insensitive
135
+ });
136
+ test('should include alias values when alias attributes exist', async () => {
137
+ jest.spyOn(elementService, 'getAliasElementAttributes').mockResolvedValue(['Alias1']);
138
+ // Mock ExecuteMDX response with axes and cells
139
+ mockRestService.post.mockResolvedValue(createMockResponse({
140
+ Axes: [
141
+ { Tuples: [{ Members: [{ Name: 'Alias1' }] }] }, // column axis
142
+ {
143
+ Tuples: [
144
+ { Members: [{ Name: 'Leaf1' }] },
145
+ { Members: [{ Name: 'Leaf2' }] }
146
+ ]
147
+ } // row axis
148
+ ],
149
+ Cells: [
150
+ { Value: 'Alias_Leaf1' },
151
+ { Value: 'Alias_Leaf2' }
152
+ ]
153
+ }));
154
+ const result = await elementService.getAllLeafElementIdentifiers('Dim1', 'Hier1');
155
+ // Should contain both element names and alias values
156
+ expect(result.has('Leaf1')).toBe(true);
157
+ expect(result.has('Leaf2')).toBe(true);
158
+ expect(result.has('Alias_Leaf1')).toBe(true);
159
+ expect(result.has('Alias_Leaf2')).toBe(true);
160
+ });
161
+ test('should skip empty alias values', async () => {
162
+ jest.spyOn(elementService, 'getAliasElementAttributes').mockResolvedValue(['Alias1']);
163
+ mockRestService.post.mockResolvedValue(createMockResponse({
164
+ Axes: [
165
+ { Tuples: [{ Members: [{ Name: 'Alias1' }] }] },
166
+ { Tuples: [{ Members: [{ Name: 'Elem1' }] }] }
167
+ ],
168
+ Cells: [
169
+ { Value: '' }, // empty alias
170
+ { Value: null } // null alias
171
+ ]
172
+ }));
173
+ const result = await elementService.getAllLeafElementIdentifiers('Dim1', 'Hier1');
174
+ expect(result.has('Elem1')).toBe(true);
175
+ expect(result.size).toBe(1); // only the element name, no empty/null aliases
176
+ });
177
+ });
178
+ // ===== EXISTENCE METHODS =====
179
+ describe('attributeCubeExists', () => {
180
+ test('should return true when attribute cube exists', async () => {
181
+ mockRestService.get.mockResolvedValue(createMockResponse({}));
182
+ const result = await elementService.attributeCubeExists('TestDim');
183
+ expect(result).toBe(true);
184
+ expect(mockRestService.get).toHaveBeenCalledWith(expect.stringContaining("ElementAttributes_TestDim"));
185
+ });
186
+ test('should return false when attribute cube does not exist', async () => {
187
+ mockRestService.get.mockRejectedValue(new TM1Exception_1.TM1RestException('Not found', 404, { status: 404 }));
188
+ const result = await elementService.attributeCubeExists('NonExistentDim');
189
+ expect(result).toBe(false);
190
+ });
191
+ });
192
+ // ===== TRAVERSAL METHODS =====
193
+ describe('getParentsOfAllElements', () => {
194
+ test('should return parent mapping for all elements', async () => {
195
+ mockRestService.get.mockResolvedValue(createMockResponse({
196
+ value: [
197
+ { Name: 'Leaf1', Parents: [{ Name: 'Parent1' }, { Name: 'Parent2' }] },
198
+ { Name: 'Leaf2', Parents: [{ Name: 'Parent1' }] },
199
+ { Name: 'Parent1', Parents: [] }
200
+ ]
201
+ }));
202
+ const result = await elementService.getParentsOfAllElements('Dim1', 'Hier1');
203
+ expect(result['Leaf1']).toEqual(['Parent1', 'Parent2']);
204
+ expect(result['Leaf2']).toEqual(['Parent1']);
205
+ expect(result['Parent1']).toEqual([]);
206
+ });
207
+ test('should handle elements with no Parents property', async () => {
208
+ mockRestService.get.mockResolvedValue(createMockResponse({
209
+ value: [
210
+ { Name: 'Orphan' }
211
+ ]
212
+ }));
213
+ const result = await elementService.getParentsOfAllElements('Dim1', 'Hier1');
214
+ expect(result['Orphan']).toEqual([]);
215
+ });
216
+ test('should use correct URL with expand', async () => {
217
+ mockRestService.get.mockResolvedValue(createMockResponse({ value: [] }));
218
+ await elementService.getParentsOfAllElements('Dim1', 'Hier1');
219
+ expect(mockRestService.get).toHaveBeenCalledWith(expect.stringContaining('$expand=Parents($select=Name)'));
220
+ });
221
+ });
222
+ describe('getElementPrincipalName', () => {
223
+ test('should return the canonical element name', async () => {
224
+ mockRestService.get.mockResolvedValue(createMockResponse({
225
+ Name: 'CanonicalName',
226
+ Type: 'Numeric',
227
+ Level: 0,
228
+ Index: 1
229
+ }));
230
+ const name = await elementService.getElementPrincipalName('Dim1', 'Hier1', 'someAlias');
231
+ expect(name).toBe('CanonicalName');
232
+ });
233
+ });
234
+ // ===== RELATIONSHIP CHECK METHODS =====
235
+ describe('elementIsParent', () => {
236
+ test('should return true when element is a direct parent', async () => {
237
+ mockRestService.post.mockResolvedValue(createMockResponse({
238
+ Cardinality: 1
239
+ }));
240
+ const result = await elementService.elementIsParent('Dim1', 'Hier1', 'ParentElem', 'ChildElem');
241
+ expect(result).toBe(true);
242
+ });
243
+ test('should return false when element is not a direct parent', async () => {
244
+ mockRestService.post.mockResolvedValue(createMockResponse({
245
+ Cardinality: 0
246
+ }));
247
+ const result = await elementService.elementIsParent('Dim1', 'Hier1', 'NotParent', 'ChildElem');
248
+ expect(result).toBe(false);
249
+ });
250
+ test('should use TM1DRILLDOWNMEMBER without RECURSIVE', async () => {
251
+ mockRestService.post.mockResolvedValue(createMockResponse({ Cardinality: 0 }));
252
+ await elementService.elementIsParent('Dim1', 'Hier1', 'Parent', 'Child');
253
+ const mdxPayload = JSON.parse(mockRestService.post.mock.calls[0][1]);
254
+ expect(mdxPayload.MDX).toContain('TM1DRILLDOWNMEMBER');
255
+ expect(mdxPayload.MDX).not.toContain('RECURSIVE');
256
+ expect(mdxPayload.MDX).toContain('INTERSECT');
257
+ });
258
+ });
259
+ describe('elementIsAncestor', () => {
260
+ test('should use TM1DrillDownMember with RECURSIVE for non-admin', async () => {
261
+ // Mock isAdmin = false (default)
262
+ jest.spyOn(elementService, 'exists').mockResolvedValue(true);
263
+ mockRestService.post.mockResolvedValue(createMockResponse({ Cardinality: 1 }));
264
+ const result = await elementService.elementIsAncestor('Dim1', 'Hier1', 'Ancestor', 'Elem');
265
+ expect(result).toBe(true);
266
+ const mdxPayload = JSON.parse(mockRestService.post.mock.calls[0][1]);
267
+ expect(mdxPayload.MDX).toContain('TM1DRILLDOWNMEMBER');
268
+ expect(mdxPayload.MDX).toContain('RECURSIVE');
269
+ });
270
+ test('should return false when element does not exist (TM1DrillDownMember)', async () => {
271
+ jest.spyOn(elementService, 'exists').mockResolvedValue(false);
272
+ jest.spyOn(elementService, 'hierarchyExists').mockResolvedValue(true);
273
+ const result = await elementService.elementIsAncestor('Dim1', 'Hier1', 'Ancestor', 'NonExistent', 'TM1DrillDownMember');
274
+ expect(result).toBe(false);
275
+ });
276
+ test('should throw when hierarchy does not exist (TM1DrillDownMember)', async () => {
277
+ jest.spyOn(elementService, 'exists').mockResolvedValue(false);
278
+ jest.spyOn(elementService, 'hierarchyExists').mockResolvedValue(false);
279
+ await expect(elementService.elementIsAncestor('Dim1', 'BadHier', 'Ancestor', 'Elem', 'TM1DrillDownMember')).rejects.toThrow("Hierarchy 'BadHier' does not exist in dimension 'Dim1'");
280
+ });
281
+ test('should use DESCENDANTS method when specified', async () => {
282
+ jest.spyOn(elementService, 'exists').mockResolvedValue(true);
283
+ mockRestService.post.mockResolvedValue(createMockResponse({ Cardinality: 1 }));
284
+ await elementService.elementIsAncestor('Dim1', 'Hier1', 'Ancestor', 'Elem', 'Descendants');
285
+ const mdxPayload = JSON.parse(mockRestService.post.mock.calls[0][1]);
286
+ expect(mdxPayload.MDX).toContain('DESCENDANTS');
287
+ });
288
+ test('should use TI method when specified', async () => {
289
+ // Mock _elementIsAncestorTi
290
+ const tiSpy = jest.spyOn(elementService, '_elementIsAncestorTi').mockResolvedValue(true);
291
+ const result = await elementService.elementIsAncestor('Dim1', 'Hier1', 'Ancestor', 'Elem', 'TI');
292
+ expect(result).toBe(true);
293
+ expect(tiSpy).toHaveBeenCalledWith('Dim1', 'Hier1', 'Elem', 'Ancestor');
294
+ });
295
+ test('should check hierarchy existence when TI returns false', async () => {
296
+ jest.spyOn(elementService, '_elementIsAncestorTi').mockResolvedValue(false);
297
+ const hierSpy = jest.spyOn(elementService, 'hierarchyExists').mockResolvedValue(true);
298
+ const result = await elementService.elementIsAncestor('Dim1', 'Hier1', 'Ancestor', 'Elem', 'TI');
299
+ expect(result).toBe(false);
300
+ expect(hierSpy).toHaveBeenCalledWith('Dim1', 'Hier1');
301
+ });
302
+ test('should throw when TI returns false and hierarchy does not exist', async () => {
303
+ jest.spyOn(elementService, '_elementIsAncestorTi').mockResolvedValue(false);
304
+ jest.spyOn(elementService, 'hierarchyExists').mockResolvedValue(false);
305
+ await expect(elementService.elementIsAncestor('Dim1', 'BadHier', 'Ancestor', 'Elem', 'TI')).rejects.toThrow("Hierarchy: 'BadHier' does not exist in dimension: 'Dim1'");
306
+ });
307
+ });
308
+ // ===== EDGE METHODS =====
309
+ describe('removeEdge', () => {
310
+ test('should DELETE the correct edge URL', async () => {
311
+ mockRestService.delete.mockResolvedValue(createMockResponse({}, 204));
312
+ await elementService.removeEdge('Dim1', 'Hier1', 'ParentElem', 'ChildElem');
313
+ const calledUrl = mockRestService.delete.mock.calls[0][0];
314
+ expect(calledUrl).toContain("Elements('ParentElem')");
315
+ expect(calledUrl).toContain("ParentName='ParentElem'");
316
+ expect(calledUrl).toContain("ComponentName='ChildElem'");
317
+ expect(calledUrl).toContain('Edges(');
318
+ });
319
+ });
320
+ describe('hierarchyExists', () => {
321
+ test('should delegate to HierarchyService.exists and return true', async () => {
322
+ // HierarchyService.exists does a GET and checks names
323
+ mockRestService.get.mockResolvedValue(createMockResponse({
324
+ value: [{ Name: 'Hier1' }]
325
+ }));
326
+ const result = await elementService.hierarchyExists('Dim1', 'Hier1');
327
+ expect(result).toBe(true);
328
+ });
329
+ test('should return false when hierarchy does not exist', async () => {
330
+ mockRestService.get.mockResolvedValue(createMockResponse({
331
+ value: [{ Name: 'OtherHier' }]
332
+ }));
333
+ const result = await elementService.hierarchyExists('Dim1', 'NonExistent');
334
+ expect(result).toBe(false);
335
+ });
336
+ });
337
+ // ===== PRIVATE HELPER TESTS =====
338
+ describe('_buildDrillIntersectionMdx (via elementIsParent/elementIsAncestor)', () => {
339
+ test('should throw for invalid MDX method', () => {
340
+ const buildMdx = elementService._buildDrillIntersectionMdx.bind(elementService);
341
+ expect(() => buildMdx('D', 'H', 'A', 'B', 'INVALID', false)).toThrow("Invalid MDX Drill Method");
342
+ });
343
+ test('should build correct TM1DrillDownMember MDX', () => {
344
+ const buildMdx = elementService._buildDrillIntersectionMdx.bind(elementService);
345
+ const nonRecursive = buildMdx('Dim', 'Hier', 'Parent', 'Child', 'TM1DrillDownMember', false);
346
+ expect(nonRecursive).toBe('INTERSECT({TM1DRILLDOWNMEMBER({[Dim].[Hier].[Parent]}, ALL)}, {[Dim].[Hier].[Child]})');
347
+ const recursive = buildMdx('Dim', 'Hier', 'Ancestor', 'Leaf', 'TM1DrillDownMember', true);
348
+ expect(recursive).toBe('INTERSECT({TM1DRILLDOWNMEMBER({[Dim].[Hier].[Ancestor]}, ALL, RECURSIVE)}, {[Dim].[Hier].[Leaf]})');
349
+ });
350
+ test('should build correct DESCENDANTS MDX', () => {
351
+ const buildMdx = elementService._buildDrillIntersectionMdx.bind(elementService);
352
+ const result = buildMdx('Dim', 'Hier', 'Ancestor', 'Elem', 'Descendants', true);
353
+ expect(result).toBe('INTERSECT({DESCENDANTS([Dim].[Hier].[Ancestor], [Dim].[Hier].[Elem].Level, SELF)}, {[Dim].[Hier].[Elem]})');
354
+ });
355
+ });
356
+ describe('_getMdxSetCardinality', () => {
357
+ test('should POST to ExecuteMDXSetExpression and return cardinality', async () => {
358
+ mockRestService.post.mockResolvedValue(createMockResponse({ Cardinality: 5 }));
359
+ const getCardinality = elementService._getMdxSetCardinality.bind(elementService);
360
+ const result = await getCardinality('SOME MDX');
361
+ expect(result).toBe(5);
362
+ expect(mockRestService.post).toHaveBeenCalledWith('/ExecuteMDXSetExpression?$select=Cardinality', expect.stringContaining('SOME MDX'));
363
+ });
364
+ test('should return 0 when Cardinality is missing', async () => {
365
+ mockRestService.post.mockResolvedValue(createMockResponse({}));
366
+ const getCardinality = elementService._getMdxSetCardinality.bind(elementService);
367
+ const result = await getCardinality('SOME MDX');
368
+ expect(result).toBe(0);
369
+ });
370
+ });
371
+ // ===== EDGE CASE TESTS (P2 review feedback) =====
372
+ describe('elementIsAncestor — additional edge cases', () => {
373
+ test('should auto-select TI method when isAdmin is true', async () => {
374
+ Object.defineProperty(elementService, 'isAdmin', { get: () => true });
375
+ const tiSpy = jest.spyOn(elementService, '_elementIsAncestorTi').mockResolvedValue(true);
376
+ const result = await elementService.elementIsAncestor('Dim1', 'Hier1', 'Ancestor', 'Elem');
377
+ expect(result).toBe(true);
378
+ expect(tiSpy).toHaveBeenCalled();
379
+ });
380
+ });
381
+ describe('getElementTypesFromAllHierarchies — additional edge cases', () => {
382
+ test('should handle empty Hierarchies array', async () => {
383
+ mockRestService.get.mockResolvedValue(createMockResponse({
384
+ Hierarchies: []
385
+ }));
386
+ const result = await elementService.getElementTypesFromAllHierarchies('EmptyDim');
387
+ expect(result.size).toBe(0);
388
+ });
389
+ });
390
+ describe('getParentsOfAllElements — additional edge cases', () => {
391
+ test('should handle empty value array', async () => {
392
+ mockRestService.get.mockResolvedValue(createMockResponse({ value: [] }));
393
+ const result = await elementService.getParentsOfAllElements('Dim1', 'Hier1');
394
+ expect(Object.keys(result)).toHaveLength(0);
395
+ });
396
+ });
397
+ describe('_retrieveMdxRowsAndCellValuesAsStringSet — additional edge cases', () => {
398
+ test('should return empty set when Axes and Cells are missing', async () => {
399
+ // CellService.executeMdxRowsAndValues handles missing Axes/Cells
400
+ mockRestService.post.mockResolvedValue(createMockResponse({}));
401
+ const retrieve = elementService._retrieveMdxRowsAndCellValuesAsStringSet.bind(elementService);
402
+ const result = await retrieve('SELECT {} ON ROWS FROM [Cube]');
403
+ expect(result.size).toBe(0);
404
+ });
405
+ });
406
+ describe('getElementPrincipalName — additional edge cases', () => {
407
+ test('should propagate error when element not found', async () => {
408
+ mockRestService.get.mockRejectedValue(new TM1Exception_1.TM1RestException('Not found', 404, { status: 404 }));
409
+ await expect(elementService.getElementPrincipalName('Dim1', 'Hier1', 'NonExistent')).rejects.toThrow();
410
+ });
411
+ });
412
+ });
413
+ //# sourceMappingURL=elementService.issue37.test.js.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * ElementService Tests — Issue #38 new methods
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=elementService.issue38.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elementService.issue38.test.d.ts","sourceRoot":"","sources":["../../src/tests/elementService.issue38.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}