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,571 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for ElementService issue #37 — 13 missing methods for tm1py parity
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ElementService } from '../services/ElementService';
|
|
6
|
+
import { RestService } from '../services/RestService';
|
|
7
|
+
import { TM1RestException } from '../exceptions/TM1Exception';
|
|
8
|
+
|
|
9
|
+
const createMockResponse = (data: any, status: number = 200) => ({
|
|
10
|
+
data,
|
|
11
|
+
status,
|
|
12
|
+
statusText: 'OK',
|
|
13
|
+
headers: {},
|
|
14
|
+
config: {} as any
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('ElementService — Issue #37: 13 missing methods', () => {
|
|
18
|
+
let elementService: ElementService;
|
|
19
|
+
let mockRestService: jest.Mocked<RestService>;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
mockRestService = {
|
|
23
|
+
get: jest.fn(),
|
|
24
|
+
post: jest.fn(),
|
|
25
|
+
patch: jest.fn(),
|
|
26
|
+
delete: jest.fn(),
|
|
27
|
+
put: jest.fn(),
|
|
28
|
+
config: {} as any,
|
|
29
|
+
rest: {} as any,
|
|
30
|
+
buildBaseUrl: jest.fn(),
|
|
31
|
+
extractErrorMessage: jest.fn()
|
|
32
|
+
} as any;
|
|
33
|
+
|
|
34
|
+
elementService = new ElementService(mockRestService);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ===== COUNT METHODS =====
|
|
38
|
+
|
|
39
|
+
describe('getNumberOfConsolidatedElements', () => {
|
|
40
|
+
test('should return count with Type eq 3 filter', async () => {
|
|
41
|
+
mockRestService.get.mockResolvedValue(createMockResponse('5'));
|
|
42
|
+
|
|
43
|
+
const count = await elementService.getNumberOfConsolidatedElements('Dim1', 'Hier1');
|
|
44
|
+
|
|
45
|
+
expect(count).toBe(5);
|
|
46
|
+
expect(mockRestService.get).toHaveBeenCalledWith(
|
|
47
|
+
expect.stringContaining('$filter=Type eq 3')
|
|
48
|
+
);
|
|
49
|
+
expect(mockRestService.get).toHaveBeenCalledWith(
|
|
50
|
+
expect.stringContaining('$count')
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('should return 0 for empty response', async () => {
|
|
55
|
+
mockRestService.get.mockResolvedValue(createMockResponse(''));
|
|
56
|
+
|
|
57
|
+
const count = await elementService.getNumberOfConsolidatedElements('Dim1', 'Hier1');
|
|
58
|
+
expect(count).toBe(0);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('getNumberOfLeafElements', () => {
|
|
63
|
+
test('should return count with Type ne 3 filter', async () => {
|
|
64
|
+
mockRestService.get.mockResolvedValue(createMockResponse('10'));
|
|
65
|
+
|
|
66
|
+
const count = await elementService.getNumberOfLeafElements('Dim1', 'Hier1');
|
|
67
|
+
|
|
68
|
+
expect(count).toBe(10);
|
|
69
|
+
expect(mockRestService.get).toHaveBeenCalledWith(
|
|
70
|
+
expect.stringContaining('$filter=Type ne 3')
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('getNumberOfNumericElements', () => {
|
|
76
|
+
test('should return count with Type eq 1 filter', async () => {
|
|
77
|
+
mockRestService.get.mockResolvedValue(createMockResponse('7'));
|
|
78
|
+
|
|
79
|
+
const count = await elementService.getNumberOfNumericElements('Dim1', 'Hier1');
|
|
80
|
+
|
|
81
|
+
expect(count).toBe(7);
|
|
82
|
+
expect(mockRestService.get).toHaveBeenCalledWith(
|
|
83
|
+
expect.stringContaining('$filter=Type eq 1')
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('getNumberOfStringElements', () => {
|
|
89
|
+
test('should return count with Type eq 2 filter', async () => {
|
|
90
|
+
mockRestService.get.mockResolvedValue(createMockResponse('3'));
|
|
91
|
+
|
|
92
|
+
const count = await elementService.getNumberOfStringElements('Dim1', 'Hier1');
|
|
93
|
+
|
|
94
|
+
expect(count).toBe(3);
|
|
95
|
+
expect(mockRestService.get).toHaveBeenCalledWith(
|
|
96
|
+
expect.stringContaining('$filter=Type eq 2')
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ===== IDENTIFIER METHODS =====
|
|
102
|
+
|
|
103
|
+
describe('getElementTypesFromAllHierarchies', () => {
|
|
104
|
+
test('should return element types from all hierarchies', async () => {
|
|
105
|
+
mockRestService.get.mockResolvedValue(createMockResponse({
|
|
106
|
+
Hierarchies: [
|
|
107
|
+
{
|
|
108
|
+
Elements: [
|
|
109
|
+
{ Name: 'Elem1', Type: 'Numeric' },
|
|
110
|
+
{ Name: 'Elem2', Type: 'String' }
|
|
111
|
+
]
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
Elements: [
|
|
115
|
+
{ Name: 'Elem3', Type: 'Consolidated' }
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
}));
|
|
120
|
+
|
|
121
|
+
const result = await elementService.getElementTypesFromAllHierarchies('TestDim');
|
|
122
|
+
|
|
123
|
+
expect(result.get('Elem1')).toBe('Numeric');
|
|
124
|
+
expect(result.get('Elem2')).toBe('String');
|
|
125
|
+
expect(result.get('Elem3')).toBe('Consolidated');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('should include filter when skipConsolidations is true', async () => {
|
|
129
|
+
mockRestService.get.mockResolvedValue(createMockResponse({
|
|
130
|
+
Hierarchies: [{ Elements: [{ Name: 'Elem1', Type: 'Numeric' }] }]
|
|
131
|
+
}));
|
|
132
|
+
|
|
133
|
+
await elementService.getElementTypesFromAllHierarchies('TestDim', true);
|
|
134
|
+
|
|
135
|
+
expect(mockRestService.get).toHaveBeenCalledWith(
|
|
136
|
+
expect.stringContaining('$filter=Type ne 3')
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('should not include filter when skipConsolidations is false', async () => {
|
|
141
|
+
mockRestService.get.mockResolvedValue(createMockResponse({
|
|
142
|
+
Hierarchies: [{ Elements: [{ Name: 'Elem1', Type: 'Numeric' }] }]
|
|
143
|
+
}));
|
|
144
|
+
|
|
145
|
+
await elementService.getElementTypesFromAllHierarchies('TestDim', false);
|
|
146
|
+
|
|
147
|
+
const calledUrl = mockRestService.get.mock.calls[0][0];
|
|
148
|
+
expect(calledUrl).not.toContain('$filter');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('should be case-and-space-insensitive', async () => {
|
|
152
|
+
mockRestService.get.mockResolvedValue(createMockResponse({
|
|
153
|
+
Hierarchies: [
|
|
154
|
+
{ Elements: [{ Name: 'My Element', Type: 'Numeric' }] }
|
|
155
|
+
]
|
|
156
|
+
}));
|
|
157
|
+
|
|
158
|
+
const result = await elementService.getElementTypesFromAllHierarchies('TestDim');
|
|
159
|
+
expect(result.get('myelement')).toBe('Numeric');
|
|
160
|
+
expect(result.get('MY ELEMENT')).toBe('Numeric');
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('getAllLeafElementIdentifiers', () => {
|
|
165
|
+
test('should return element names when no alias attributes exist', async () => {
|
|
166
|
+
// Mock getAliasElementAttributes → empty
|
|
167
|
+
jest.spyOn(elementService, 'getAliasElementAttributes').mockResolvedValue([]);
|
|
168
|
+
// Mock executeSetMdx → returns members
|
|
169
|
+
jest.spyOn(elementService, 'executeSetMdx').mockResolvedValue([
|
|
170
|
+
[{ Name: 'Leaf1' }],
|
|
171
|
+
[{ Name: 'Leaf2' }],
|
|
172
|
+
[{ Name: 'Leaf3' }]
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
const result = await elementService.getAllLeafElementIdentifiers('Dim1', 'Hier1');
|
|
176
|
+
|
|
177
|
+
expect(result.has('Leaf1')).toBe(true);
|
|
178
|
+
expect(result.has('Leaf2')).toBe(true);
|
|
179
|
+
expect(result.has('Leaf3')).toBe(true);
|
|
180
|
+
expect(result.has('leaf1')).toBe(true); // case insensitive
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('should include alias values when alias attributes exist', async () => {
|
|
184
|
+
jest.spyOn(elementService, 'getAliasElementAttributes').mockResolvedValue(['Alias1']);
|
|
185
|
+
|
|
186
|
+
// Mock ExecuteMDX response with axes and cells
|
|
187
|
+
mockRestService.post.mockResolvedValue(createMockResponse({
|
|
188
|
+
Axes: [
|
|
189
|
+
{ Tuples: [{ Members: [{ Name: 'Alias1' }] }] }, // column axis
|
|
190
|
+
{
|
|
191
|
+
Tuples: [
|
|
192
|
+
{ Members: [{ Name: 'Leaf1' }] },
|
|
193
|
+
{ Members: [{ Name: 'Leaf2' }] }
|
|
194
|
+
]
|
|
195
|
+
} // row axis
|
|
196
|
+
],
|
|
197
|
+
Cells: [
|
|
198
|
+
{ Value: 'Alias_Leaf1' },
|
|
199
|
+
{ Value: 'Alias_Leaf2' }
|
|
200
|
+
]
|
|
201
|
+
}));
|
|
202
|
+
|
|
203
|
+
const result = await elementService.getAllLeafElementIdentifiers('Dim1', 'Hier1');
|
|
204
|
+
|
|
205
|
+
// Should contain both element names and alias values
|
|
206
|
+
expect(result.has('Leaf1')).toBe(true);
|
|
207
|
+
expect(result.has('Leaf2')).toBe(true);
|
|
208
|
+
expect(result.has('Alias_Leaf1')).toBe(true);
|
|
209
|
+
expect(result.has('Alias_Leaf2')).toBe(true);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('should skip empty alias values', async () => {
|
|
213
|
+
jest.spyOn(elementService, 'getAliasElementAttributes').mockResolvedValue(['Alias1']);
|
|
214
|
+
|
|
215
|
+
mockRestService.post.mockResolvedValue(createMockResponse({
|
|
216
|
+
Axes: [
|
|
217
|
+
{ Tuples: [{ Members: [{ Name: 'Alias1' }] }] },
|
|
218
|
+
{ Tuples: [{ Members: [{ Name: 'Elem1' }] }] }
|
|
219
|
+
],
|
|
220
|
+
Cells: [
|
|
221
|
+
{ Value: '' }, // empty alias
|
|
222
|
+
{ Value: null } // null alias
|
|
223
|
+
]
|
|
224
|
+
}));
|
|
225
|
+
|
|
226
|
+
const result = await elementService.getAllLeafElementIdentifiers('Dim1', 'Hier1');
|
|
227
|
+
|
|
228
|
+
expect(result.has('Elem1')).toBe(true);
|
|
229
|
+
expect(result.size).toBe(1); // only the element name, no empty/null aliases
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// ===== EXISTENCE METHODS =====
|
|
234
|
+
|
|
235
|
+
describe('attributeCubeExists', () => {
|
|
236
|
+
test('should return true when attribute cube exists', async () => {
|
|
237
|
+
mockRestService.get.mockResolvedValue(createMockResponse({}));
|
|
238
|
+
|
|
239
|
+
const result = await elementService.attributeCubeExists('TestDim');
|
|
240
|
+
|
|
241
|
+
expect(result).toBe(true);
|
|
242
|
+
expect(mockRestService.get).toHaveBeenCalledWith(
|
|
243
|
+
expect.stringContaining("ElementAttributes_TestDim")
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test('should return false when attribute cube does not exist', async () => {
|
|
248
|
+
mockRestService.get.mockRejectedValue(
|
|
249
|
+
new TM1RestException('Not found', 404, { status: 404 })
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const result = await elementService.attributeCubeExists('NonExistentDim');
|
|
253
|
+
expect(result).toBe(false);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// ===== TRAVERSAL METHODS =====
|
|
258
|
+
|
|
259
|
+
describe('getParentsOfAllElements', () => {
|
|
260
|
+
test('should return parent mapping for all elements', async () => {
|
|
261
|
+
mockRestService.get.mockResolvedValue(createMockResponse({
|
|
262
|
+
value: [
|
|
263
|
+
{ Name: 'Leaf1', Parents: [{ Name: 'Parent1' }, { Name: 'Parent2' }] },
|
|
264
|
+
{ Name: 'Leaf2', Parents: [{ Name: 'Parent1' }] },
|
|
265
|
+
{ Name: 'Parent1', Parents: [] }
|
|
266
|
+
]
|
|
267
|
+
}));
|
|
268
|
+
|
|
269
|
+
const result = await elementService.getParentsOfAllElements('Dim1', 'Hier1');
|
|
270
|
+
|
|
271
|
+
expect(result['Leaf1']).toEqual(['Parent1', 'Parent2']);
|
|
272
|
+
expect(result['Leaf2']).toEqual(['Parent1']);
|
|
273
|
+
expect(result['Parent1']).toEqual([]);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test('should handle elements with no Parents property', async () => {
|
|
277
|
+
mockRestService.get.mockResolvedValue(createMockResponse({
|
|
278
|
+
value: [
|
|
279
|
+
{ Name: 'Orphan' }
|
|
280
|
+
]
|
|
281
|
+
}));
|
|
282
|
+
|
|
283
|
+
const result = await elementService.getParentsOfAllElements('Dim1', 'Hier1');
|
|
284
|
+
expect(result['Orphan']).toEqual([]);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('should use correct URL with expand', async () => {
|
|
288
|
+
mockRestService.get.mockResolvedValue(createMockResponse({ value: [] }));
|
|
289
|
+
|
|
290
|
+
await elementService.getParentsOfAllElements('Dim1', 'Hier1');
|
|
291
|
+
|
|
292
|
+
expect(mockRestService.get).toHaveBeenCalledWith(
|
|
293
|
+
expect.stringContaining('$expand=Parents($select=Name)')
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe('getElementPrincipalName', () => {
|
|
299
|
+
test('should return the canonical element name', async () => {
|
|
300
|
+
mockRestService.get.mockResolvedValue(createMockResponse({
|
|
301
|
+
Name: 'CanonicalName',
|
|
302
|
+
Type: 'Numeric',
|
|
303
|
+
Level: 0,
|
|
304
|
+
Index: 1
|
|
305
|
+
}));
|
|
306
|
+
|
|
307
|
+
const name = await elementService.getElementPrincipalName('Dim1', 'Hier1', 'someAlias');
|
|
308
|
+
expect(name).toBe('CanonicalName');
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// ===== RELATIONSHIP CHECK METHODS =====
|
|
313
|
+
|
|
314
|
+
describe('elementIsParent', () => {
|
|
315
|
+
test('should return true when element is a direct parent', async () => {
|
|
316
|
+
mockRestService.post.mockResolvedValue(createMockResponse({
|
|
317
|
+
Cardinality: 1
|
|
318
|
+
}));
|
|
319
|
+
|
|
320
|
+
const result = await elementService.elementIsParent('Dim1', 'Hier1', 'ParentElem', 'ChildElem');
|
|
321
|
+
expect(result).toBe(true);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test('should return false when element is not a direct parent', async () => {
|
|
325
|
+
mockRestService.post.mockResolvedValue(createMockResponse({
|
|
326
|
+
Cardinality: 0
|
|
327
|
+
}));
|
|
328
|
+
|
|
329
|
+
const result = await elementService.elementIsParent('Dim1', 'Hier1', 'NotParent', 'ChildElem');
|
|
330
|
+
expect(result).toBe(false);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test('should use TM1DRILLDOWNMEMBER without RECURSIVE', async () => {
|
|
334
|
+
mockRestService.post.mockResolvedValue(createMockResponse({ Cardinality: 0 }));
|
|
335
|
+
|
|
336
|
+
await elementService.elementIsParent('Dim1', 'Hier1', 'Parent', 'Child');
|
|
337
|
+
|
|
338
|
+
const mdxPayload = JSON.parse(mockRestService.post.mock.calls[0][1]);
|
|
339
|
+
expect(mdxPayload.MDX).toContain('TM1DRILLDOWNMEMBER');
|
|
340
|
+
expect(mdxPayload.MDX).not.toContain('RECURSIVE');
|
|
341
|
+
expect(mdxPayload.MDX).toContain('INTERSECT');
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe('elementIsAncestor', () => {
|
|
346
|
+
test('should use TM1DrillDownMember with RECURSIVE for non-admin', async () => {
|
|
347
|
+
// Mock isAdmin = false (default)
|
|
348
|
+
jest.spyOn(elementService, 'exists').mockResolvedValue(true);
|
|
349
|
+
mockRestService.post.mockResolvedValue(createMockResponse({ Cardinality: 1 }));
|
|
350
|
+
|
|
351
|
+
const result = await elementService.elementIsAncestor('Dim1', 'Hier1', 'Ancestor', 'Elem');
|
|
352
|
+
|
|
353
|
+
expect(result).toBe(true);
|
|
354
|
+
const mdxPayload = JSON.parse(mockRestService.post.mock.calls[0][1]);
|
|
355
|
+
expect(mdxPayload.MDX).toContain('TM1DRILLDOWNMEMBER');
|
|
356
|
+
expect(mdxPayload.MDX).toContain('RECURSIVE');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test('should return false when element does not exist (TM1DrillDownMember)', async () => {
|
|
360
|
+
jest.spyOn(elementService, 'exists').mockResolvedValue(false);
|
|
361
|
+
jest.spyOn(elementService, 'hierarchyExists').mockResolvedValue(true);
|
|
362
|
+
|
|
363
|
+
const result = await elementService.elementIsAncestor(
|
|
364
|
+
'Dim1', 'Hier1', 'Ancestor', 'NonExistent', 'TM1DrillDownMember'
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
expect(result).toBe(false);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test('should throw when hierarchy does not exist (TM1DrillDownMember)', async () => {
|
|
371
|
+
jest.spyOn(elementService, 'exists').mockResolvedValue(false);
|
|
372
|
+
jest.spyOn(elementService, 'hierarchyExists').mockResolvedValue(false);
|
|
373
|
+
|
|
374
|
+
await expect(
|
|
375
|
+
elementService.elementIsAncestor('Dim1', 'BadHier', 'Ancestor', 'Elem', 'TM1DrillDownMember')
|
|
376
|
+
).rejects.toThrow("Hierarchy 'BadHier' does not exist in dimension 'Dim1'");
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test('should use DESCENDANTS method when specified', async () => {
|
|
380
|
+
jest.spyOn(elementService, 'exists').mockResolvedValue(true);
|
|
381
|
+
mockRestService.post.mockResolvedValue(createMockResponse({ Cardinality: 1 }));
|
|
382
|
+
|
|
383
|
+
await elementService.elementIsAncestor('Dim1', 'Hier1', 'Ancestor', 'Elem', 'Descendants');
|
|
384
|
+
|
|
385
|
+
const mdxPayload = JSON.parse(mockRestService.post.mock.calls[0][1]);
|
|
386
|
+
expect(mdxPayload.MDX).toContain('DESCENDANTS');
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test('should use TI method when specified', async () => {
|
|
390
|
+
// Mock _elementIsAncestorTi
|
|
391
|
+
const tiSpy = jest.spyOn(elementService as any, '_elementIsAncestorTi').mockResolvedValue(true);
|
|
392
|
+
|
|
393
|
+
const result = await elementService.elementIsAncestor('Dim1', 'Hier1', 'Ancestor', 'Elem', 'TI');
|
|
394
|
+
|
|
395
|
+
expect(result).toBe(true);
|
|
396
|
+
expect(tiSpy).toHaveBeenCalledWith('Dim1', 'Hier1', 'Elem', 'Ancestor');
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test('should check hierarchy existence when TI returns false', async () => {
|
|
400
|
+
jest.spyOn(elementService as any, '_elementIsAncestorTi').mockResolvedValue(false);
|
|
401
|
+
const hierSpy = jest.spyOn(elementService, 'hierarchyExists').mockResolvedValue(true);
|
|
402
|
+
|
|
403
|
+
const result = await elementService.elementIsAncestor('Dim1', 'Hier1', 'Ancestor', 'Elem', 'TI');
|
|
404
|
+
|
|
405
|
+
expect(result).toBe(false);
|
|
406
|
+
expect(hierSpy).toHaveBeenCalledWith('Dim1', 'Hier1');
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test('should throw when TI returns false and hierarchy does not exist', async () => {
|
|
410
|
+
jest.spyOn(elementService as any, '_elementIsAncestorTi').mockResolvedValue(false);
|
|
411
|
+
jest.spyOn(elementService, 'hierarchyExists').mockResolvedValue(false);
|
|
412
|
+
|
|
413
|
+
await expect(
|
|
414
|
+
elementService.elementIsAncestor('Dim1', 'BadHier', 'Ancestor', 'Elem', 'TI')
|
|
415
|
+
).rejects.toThrow("Hierarchy: 'BadHier' does not exist in dimension: 'Dim1'");
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// ===== EDGE METHODS =====
|
|
420
|
+
|
|
421
|
+
describe('removeEdge', () => {
|
|
422
|
+
test('should DELETE the correct edge URL', async () => {
|
|
423
|
+
mockRestService.delete.mockResolvedValue(createMockResponse({}, 204));
|
|
424
|
+
|
|
425
|
+
await elementService.removeEdge('Dim1', 'Hier1', 'ParentElem', 'ChildElem');
|
|
426
|
+
|
|
427
|
+
const calledUrl = mockRestService.delete.mock.calls[0][0];
|
|
428
|
+
expect(calledUrl).toContain("Elements('ParentElem')");
|
|
429
|
+
expect(calledUrl).toContain("ParentName='ParentElem'");
|
|
430
|
+
expect(calledUrl).toContain("ComponentName='ChildElem'");
|
|
431
|
+
expect(calledUrl).toContain('Edges(');
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
describe('hierarchyExists', () => {
|
|
436
|
+
test('should delegate to HierarchyService.exists and return true', async () => {
|
|
437
|
+
// HierarchyService.exists does a GET and checks names
|
|
438
|
+
mockRestService.get.mockResolvedValue(createMockResponse({
|
|
439
|
+
value: [{ Name: 'Hier1' }]
|
|
440
|
+
}));
|
|
441
|
+
|
|
442
|
+
const result = await elementService.hierarchyExists('Dim1', 'Hier1');
|
|
443
|
+
expect(result).toBe(true);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test('should return false when hierarchy does not exist', async () => {
|
|
447
|
+
mockRestService.get.mockResolvedValue(createMockResponse({
|
|
448
|
+
value: [{ Name: 'OtherHier' }]
|
|
449
|
+
}));
|
|
450
|
+
|
|
451
|
+
const result = await elementService.hierarchyExists('Dim1', 'NonExistent');
|
|
452
|
+
expect(result).toBe(false);
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// ===== PRIVATE HELPER TESTS =====
|
|
457
|
+
|
|
458
|
+
describe('_buildDrillIntersectionMdx (via elementIsParent/elementIsAncestor)', () => {
|
|
459
|
+
test('should throw for invalid MDX method', () => {
|
|
460
|
+
const buildMdx = (elementService as any)._buildDrillIntersectionMdx.bind(elementService);
|
|
461
|
+
expect(() => buildMdx('D', 'H', 'A', 'B', 'INVALID', false)).toThrow(
|
|
462
|
+
"Invalid MDX Drill Method"
|
|
463
|
+
);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test('should build correct TM1DrillDownMember MDX', () => {
|
|
467
|
+
const buildMdx = (elementService as any)._buildDrillIntersectionMdx.bind(elementService);
|
|
468
|
+
|
|
469
|
+
const nonRecursive = buildMdx('Dim', 'Hier', 'Parent', 'Child', 'TM1DrillDownMember', false);
|
|
470
|
+
expect(nonRecursive).toBe(
|
|
471
|
+
'INTERSECT({TM1DRILLDOWNMEMBER({[Dim].[Hier].[Parent]}, ALL)}, {[Dim].[Hier].[Child]})'
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
const recursive = buildMdx('Dim', 'Hier', 'Ancestor', 'Leaf', 'TM1DrillDownMember', true);
|
|
475
|
+
expect(recursive).toBe(
|
|
476
|
+
'INTERSECT({TM1DRILLDOWNMEMBER({[Dim].[Hier].[Ancestor]}, ALL, RECURSIVE)}, {[Dim].[Hier].[Leaf]})'
|
|
477
|
+
);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
test('should build correct DESCENDANTS MDX', () => {
|
|
481
|
+
const buildMdx = (elementService as any)._buildDrillIntersectionMdx.bind(elementService);
|
|
482
|
+
|
|
483
|
+
const result = buildMdx('Dim', 'Hier', 'Ancestor', 'Elem', 'Descendants', true);
|
|
484
|
+
expect(result).toBe(
|
|
485
|
+
'INTERSECT({DESCENDANTS([Dim].[Hier].[Ancestor], [Dim].[Hier].[Elem].Level, SELF)}, {[Dim].[Hier].[Elem]})'
|
|
486
|
+
);
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
describe('_getMdxSetCardinality', () => {
|
|
491
|
+
test('should POST to ExecuteMDXSetExpression and return cardinality', async () => {
|
|
492
|
+
mockRestService.post.mockResolvedValue(createMockResponse({ Cardinality: 5 }));
|
|
493
|
+
|
|
494
|
+
const getCardinality = (elementService as any)._getMdxSetCardinality.bind(elementService);
|
|
495
|
+
const result = await getCardinality('SOME MDX');
|
|
496
|
+
|
|
497
|
+
expect(result).toBe(5);
|
|
498
|
+
expect(mockRestService.post).toHaveBeenCalledWith(
|
|
499
|
+
'/ExecuteMDXSetExpression?$select=Cardinality',
|
|
500
|
+
expect.stringContaining('SOME MDX')
|
|
501
|
+
);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
test('should return 0 when Cardinality is missing', async () => {
|
|
505
|
+
mockRestService.post.mockResolvedValue(createMockResponse({}));
|
|
506
|
+
|
|
507
|
+
const getCardinality = (elementService as any)._getMdxSetCardinality.bind(elementService);
|
|
508
|
+
const result = await getCardinality('SOME MDX');
|
|
509
|
+
|
|
510
|
+
expect(result).toBe(0);
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// ===== EDGE CASE TESTS (P2 review feedback) =====
|
|
515
|
+
|
|
516
|
+
describe('elementIsAncestor — additional edge cases', () => {
|
|
517
|
+
test('should auto-select TI method when isAdmin is true', async () => {
|
|
518
|
+
Object.defineProperty(elementService, 'isAdmin', { get: () => true });
|
|
519
|
+
const tiSpy = jest.spyOn(elementService as any, '_elementIsAncestorTi').mockResolvedValue(true);
|
|
520
|
+
|
|
521
|
+
const result = await elementService.elementIsAncestor('Dim1', 'Hier1', 'Ancestor', 'Elem');
|
|
522
|
+
|
|
523
|
+
expect(result).toBe(true);
|
|
524
|
+
expect(tiSpy).toHaveBeenCalled();
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
describe('getElementTypesFromAllHierarchies — additional edge cases', () => {
|
|
529
|
+
test('should handle empty Hierarchies array', async () => {
|
|
530
|
+
mockRestService.get.mockResolvedValue(createMockResponse({
|
|
531
|
+
Hierarchies: []
|
|
532
|
+
}));
|
|
533
|
+
|
|
534
|
+
const result = await elementService.getElementTypesFromAllHierarchies('EmptyDim');
|
|
535
|
+
expect(result.size).toBe(0);
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
describe('getParentsOfAllElements — additional edge cases', () => {
|
|
540
|
+
test('should handle empty value array', async () => {
|
|
541
|
+
mockRestService.get.mockResolvedValue(createMockResponse({ value: [] }));
|
|
542
|
+
|
|
543
|
+
const result = await elementService.getParentsOfAllElements('Dim1', 'Hier1');
|
|
544
|
+
expect(Object.keys(result)).toHaveLength(0);
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
describe('_retrieveMdxRowsAndCellValuesAsStringSet — additional edge cases', () => {
|
|
549
|
+
test('should return empty set when Axes and Cells are missing', async () => {
|
|
550
|
+
// CellService.executeMdxRowsAndValues handles missing Axes/Cells
|
|
551
|
+
mockRestService.post.mockResolvedValue(createMockResponse({}));
|
|
552
|
+
|
|
553
|
+
const retrieve = (elementService as any)._retrieveMdxRowsAndCellValuesAsStringSet.bind(elementService);
|
|
554
|
+
const result = await retrieve('SELECT {} ON ROWS FROM [Cube]');
|
|
555
|
+
|
|
556
|
+
expect(result.size).toBe(0);
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
describe('getElementPrincipalName — additional edge cases', () => {
|
|
561
|
+
test('should propagate error when element not found', async () => {
|
|
562
|
+
mockRestService.get.mockRejectedValue(
|
|
563
|
+
new TM1RestException('Not found', 404, { status: 404 })
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
await expect(
|
|
567
|
+
elementService.getElementPrincipalName('Dim1', 'Hier1', 'NonExistent')
|
|
568
|
+
).rejects.toThrow();
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ElementService Tests — Issue #38 new methods
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ElementService } from '../services/ElementService';
|
|
6
|
+
import { RestService } from '../services/RestService';
|
|
7
|
+
import { ElementAttribute } from '../objects/ElementAttribute';
|
|
8
|
+
|
|
9
|
+
const createMockResponse = (data: any, status: number = 200) => ({
|
|
10
|
+
data,
|
|
11
|
+
status,
|
|
12
|
+
statusText: status === 200 ? 'OK' : status === 201 ? 'Created' : status === 204 ? 'No Content' : 'Error',
|
|
13
|
+
headers: {},
|
|
14
|
+
config: {} as any
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('ElementService — Issue #38 new methods', () => {
|
|
18
|
+
let elementService: ElementService;
|
|
19
|
+
let mockRestService: jest.Mocked<RestService>;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
mockRestService = {
|
|
23
|
+
get: jest.fn(),
|
|
24
|
+
post: jest.fn(),
|
|
25
|
+
patch: jest.fn(),
|
|
26
|
+
delete: jest.fn(),
|
|
27
|
+
put: jest.fn(),
|
|
28
|
+
config: {} as any,
|
|
29
|
+
rest: {} as any,
|
|
30
|
+
buildBaseUrl: jest.fn(),
|
|
31
|
+
extractErrorMessage: jest.fn()
|
|
32
|
+
} as any;
|
|
33
|
+
|
|
34
|
+
elementService = new ElementService(mockRestService);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ===== addElementAttributes =====
|
|
38
|
+
|
|
39
|
+
describe('addElementAttributes', () => {
|
|
40
|
+
test('should POST to ElementAttributes endpoint with correct body', async () => {
|
|
41
|
+
mockRestService.post.mockResolvedValue(createMockResponse({}, 201));
|
|
42
|
+
|
|
43
|
+
const attrs = [
|
|
44
|
+
new ElementAttribute('Description', 'String'),
|
|
45
|
+
new ElementAttribute('Score', 'Numeric')
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
await elementService.addElementAttributes('TestDimension', 'TestHierarchy', attrs);
|
|
49
|
+
|
|
50
|
+
expect(mockRestService.post).toHaveBeenCalledTimes(1);
|
|
51
|
+
const [url, body] = mockRestService.post.mock.calls[0];
|
|
52
|
+
|
|
53
|
+
expect(url).toContain("Dimensions('TestDimension')/Hierarchies('TestHierarchy')/ElementAttributes");
|
|
54
|
+
|
|
55
|
+
const parsed = JSON.parse(body);
|
|
56
|
+
expect(Array.isArray(parsed)).toBe(true);
|
|
57
|
+
expect(parsed).toHaveLength(2);
|
|
58
|
+
expect(parsed[0].Name).toBe('Description');
|
|
59
|
+
expect(parsed[0].Type).toBe('String');
|
|
60
|
+
expect(parsed[1].Name).toBe('Score');
|
|
61
|
+
expect(parsed[1].Type).toBe('Numeric');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('should POST an empty array when no attributes provided', async () => {
|
|
65
|
+
mockRestService.post.mockResolvedValue(createMockResponse({}, 201));
|
|
66
|
+
|
|
67
|
+
await elementService.addElementAttributes('TestDimension', 'TestHierarchy', []);
|
|
68
|
+
|
|
69
|
+
expect(mockRestService.post).toHaveBeenCalledTimes(1);
|
|
70
|
+
const [, body] = mockRestService.post.mock.calls[0];
|
|
71
|
+
const parsed = JSON.parse(body);
|
|
72
|
+
expect(parsed).toEqual([]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('should POST a single attribute', async () => {
|
|
76
|
+
mockRestService.post.mockResolvedValue(createMockResponse({}, 201));
|
|
77
|
+
|
|
78
|
+
const attrs = [new ElementAttribute('Alias', 'Alias')];
|
|
79
|
+
|
|
80
|
+
await elementService.addElementAttributes('Dim', 'Hier', attrs);
|
|
81
|
+
|
|
82
|
+
const [, body] = mockRestService.post.mock.calls[0];
|
|
83
|
+
const parsed = JSON.parse(body);
|
|
84
|
+
expect(parsed).toHaveLength(1);
|
|
85
|
+
expect(parsed[0].Name).toBe('Alias');
|
|
86
|
+
expect(parsed[0].Type).toBe('Alias');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('should include dimension and hierarchy names in the URL', async () => {
|
|
90
|
+
mockRestService.post.mockResolvedValue(createMockResponse({}, 201));
|
|
91
|
+
|
|
92
|
+
await elementService.addElementAttributes(
|
|
93
|
+
'MyDimension',
|
|
94
|
+
'MyHierarchy',
|
|
95
|
+
[new ElementAttribute('Attr1', 'String')]
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const [url] = mockRestService.post.mock.calls[0];
|
|
99
|
+
expect(url).toContain("MyDimension");
|
|
100
|
+
expect(url).toContain("MyHierarchy");
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|