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
|
@@ -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
|
-
|
|
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
|
-
|
|
484
|
-
|
|
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
|
-
|
|
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
|
-
|
|
503
|
+
status: 200,
|
|
504
|
+
headers: {},
|
|
505
|
+
data: { value: 42 }
|
|
491
506
|
});
|
|
492
|
-
const status = await asyncService.getAsyncOperationStatus(
|
|
507
|
+
const status = await asyncService.getAsyncOperationStatus('srv-1');
|
|
493
508
|
expect(status).toBe(AsyncOperationService_1.OperationStatus.COMPLETED);
|
|
494
|
-
const operation = asyncService.getOperation(
|
|
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
|
|
498
|
-
|
|
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
|
-
|
|
515
|
+
status: 200,
|
|
516
|
+
headers: { asyncresult: '500 Internal Server Error' },
|
|
517
|
+
data: {}
|
|
505
518
|
});
|
|
506
|
-
const status = await asyncService.getAsyncOperationStatus(
|
|
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
|
|
524
|
-
|
|
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
|
-
|
|
534
|
+
status: 202,
|
|
535
|
+
headers: {},
|
|
536
|
+
data: {}
|
|
531
537
|
});
|
|
532
|
-
const status = await asyncService.getAsyncOperationStatus(
|
|
533
|
-
expect(status).toBe(AsyncOperationService_1.OperationStatus.
|
|
538
|
+
const status = await asyncService.getAsyncOperationStatus('srv-4');
|
|
539
|
+
expect(status).toBe(AsyncOperationService_1.OperationStatus.RUNNING);
|
|
534
540
|
});
|
|
535
|
-
test('
|
|
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: '
|
|
544
|
+
name: 'LocalProcess'
|
|
539
545
|
});
|
|
540
546
|
asyncService.updateOperationStatus(operationId, AsyncOperationService_1.OperationStatus.RUNNING);
|
|
541
|
-
|
|
542
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
expect(
|
|
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 @@
|
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"elementService.issue38.test.d.ts","sourceRoot":"","sources":["../../src/tests/elementService.issue38.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
|