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 @@
|
|
|
1
|
+
{"version":3,"file":"subsetService.issue38.test.d.ts","sourceRoot":"","sources":["../../src/tests/subsetService.issue38.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SubsetService Tests — Issue #38 new methods
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const SubsetService_1 = require("../services/SubsetService");
|
|
7
|
+
const Subset_1 = require("../objects/Subset");
|
|
8
|
+
const Element_1 = require("../objects/Element");
|
|
9
|
+
const createMockResponse = (data, status = 200) => ({
|
|
10
|
+
data,
|
|
11
|
+
status,
|
|
12
|
+
statusText: status === 200 ? 'OK' : status === 201 ? 'Created' : status === 204 ? 'No Content' : 'Error',
|
|
13
|
+
headers: {},
|
|
14
|
+
config: {}
|
|
15
|
+
});
|
|
16
|
+
describe('SubsetService — Issue #38 new methods', () => {
|
|
17
|
+
let subsetService;
|
|
18
|
+
let mockRestService;
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
mockRestService = {
|
|
21
|
+
get: jest.fn(),
|
|
22
|
+
post: jest.fn(),
|
|
23
|
+
patch: jest.fn(),
|
|
24
|
+
delete: jest.fn(),
|
|
25
|
+
put: jest.fn(),
|
|
26
|
+
config: {},
|
|
27
|
+
rest: {},
|
|
28
|
+
buildBaseUrl: jest.fn(),
|
|
29
|
+
extractErrorMessage: jest.fn()
|
|
30
|
+
};
|
|
31
|
+
subsetService = new SubsetService_1.SubsetService(mockRestService);
|
|
32
|
+
});
|
|
33
|
+
// ===== updateStaticElements =====
|
|
34
|
+
describe('updateStaticElements', () => {
|
|
35
|
+
test('should PUT to Elements/$ref with correct @odata.id body', async () => {
|
|
36
|
+
mockRestService.put.mockResolvedValue(createMockResponse({}, 200));
|
|
37
|
+
await subsetService.updateStaticElements('MySubset', 'TestDimension', 'TestHierarchy', false, ['ElemA', 'ElemB']);
|
|
38
|
+
expect(mockRestService.put).toHaveBeenCalledTimes(1);
|
|
39
|
+
const [url, body] = mockRestService.put.mock.calls[0];
|
|
40
|
+
expect(url).toContain("Dimensions('TestDimension')/Hierarchies('TestHierarchy')");
|
|
41
|
+
expect(url).toContain("Subsets('MySubset')/Elements/$ref");
|
|
42
|
+
const parsed = JSON.parse(body);
|
|
43
|
+
expect(Array.isArray(parsed.value)).toBe(true);
|
|
44
|
+
expect(parsed.value).toHaveLength(2);
|
|
45
|
+
expect(parsed.value[0]['@odata.id']).toContain("Elements('ElemA')");
|
|
46
|
+
expect(parsed.value[1]['@odata.id']).toContain("Elements('ElemB')");
|
|
47
|
+
});
|
|
48
|
+
test('should accept a Subset object and use its elements', async () => {
|
|
49
|
+
mockRestService.put.mockResolvedValue(createMockResponse({}, 200));
|
|
50
|
+
const subset = new Subset_1.Subset('MySubset', 'TestDimension', 'TestHierarchy', undefined, undefined, ['X', 'Y', 'Z']);
|
|
51
|
+
await subsetService.updateStaticElements(subset);
|
|
52
|
+
expect(mockRestService.put).toHaveBeenCalledTimes(1);
|
|
53
|
+
const [, body] = mockRestService.put.mock.calls[0];
|
|
54
|
+
const parsed = JSON.parse(body);
|
|
55
|
+
expect(parsed.value).toHaveLength(3);
|
|
56
|
+
expect(parsed.value[0]['@odata.id']).toContain("Elements('X')");
|
|
57
|
+
expect(parsed.value[2]['@odata.id']).toContain("Elements('Z')");
|
|
58
|
+
});
|
|
59
|
+
test('should accept a Subset object with overriding elements array', async () => {
|
|
60
|
+
mockRestService.put.mockResolvedValue(createMockResponse({}, 200));
|
|
61
|
+
const subset = new Subset_1.Subset('MySubset', 'TestDimension', 'TestHierarchy', undefined, undefined, ['X', 'Y']);
|
|
62
|
+
// Override elements explicitly
|
|
63
|
+
await subsetService.updateStaticElements(subset, undefined, undefined, false, ['Override1']);
|
|
64
|
+
const [, body] = mockRestService.put.mock.calls[0];
|
|
65
|
+
const parsed = JSON.parse(body);
|
|
66
|
+
expect(parsed.value).toHaveLength(1);
|
|
67
|
+
expect(parsed.value[0]['@odata.id']).toContain("Elements('Override1')");
|
|
68
|
+
});
|
|
69
|
+
test('should accept Element objects (not just strings)', async () => {
|
|
70
|
+
mockRestService.put.mockResolvedValue(createMockResponse({}, 200));
|
|
71
|
+
const elem1 = new Element_1.Element('Alpha', Element_1.ElementType.NUMERIC);
|
|
72
|
+
const elem2 = new Element_1.Element('Beta', Element_1.ElementType.NUMERIC);
|
|
73
|
+
await subsetService.updateStaticElements('MySubset', 'TestDimension', 'TestHierarchy', false, [elem1, elem2]);
|
|
74
|
+
const [, body] = mockRestService.put.mock.calls[0];
|
|
75
|
+
const parsed = JSON.parse(body);
|
|
76
|
+
expect(parsed.value[0]['@odata.id']).toContain("Elements('Alpha')");
|
|
77
|
+
expect(parsed.value[1]['@odata.id']).toContain("Elements('Beta')");
|
|
78
|
+
});
|
|
79
|
+
test('should use PrivateSubsets collection when isPrivate=true', async () => {
|
|
80
|
+
mockRestService.put.mockResolvedValue(createMockResponse({}, 200));
|
|
81
|
+
await subsetService.updateStaticElements('PrivateSubset', 'TestDimension', 'TestHierarchy', true, ['E1']);
|
|
82
|
+
const [url] = mockRestService.put.mock.calls[0];
|
|
83
|
+
expect(url).toContain('PrivateSubsets');
|
|
84
|
+
// PrivateSubsets is the only subsets segment — there must be no plain '/Subsets(' prefix
|
|
85
|
+
expect(url).not.toMatch(/\/Subsets\(/);
|
|
86
|
+
});
|
|
87
|
+
test('should use public Subsets collection when isPrivate=false', async () => {
|
|
88
|
+
mockRestService.put.mockResolvedValue(createMockResponse({}, 200));
|
|
89
|
+
await subsetService.updateStaticElements('PublicSubset', 'TestDimension', 'TestHierarchy', false, ['E1']);
|
|
90
|
+
const [url] = mockRestService.put.mock.calls[0];
|
|
91
|
+
expect(url).toContain("Subsets('PublicSubset')");
|
|
92
|
+
expect(url).not.toContain('PrivateSubsets');
|
|
93
|
+
});
|
|
94
|
+
test('should throw when dimensionName is missing for string argument', async () => {
|
|
95
|
+
await expect(subsetService.updateStaticElements('MySubset', undefined, 'TestHierarchy', false, ['E1'])).rejects.toThrow('dimensionName is required when passing subset name as string');
|
|
96
|
+
});
|
|
97
|
+
test('should default hierarchyName to dimensionName for string argument', async () => {
|
|
98
|
+
mockRestService.put.mockResolvedValue(createMockResponse({}, 200));
|
|
99
|
+
await subsetService.updateStaticElements('MySubset', 'TestDimension', undefined, // no hierarchyName — should default to dimensionName
|
|
100
|
+
false, ['E1']);
|
|
101
|
+
const [url] = mockRestService.put.mock.calls[0];
|
|
102
|
+
expect(url).toContain("Hierarchies('TestDimension')");
|
|
103
|
+
});
|
|
104
|
+
test('should send empty value array when no elements provided', async () => {
|
|
105
|
+
mockRestService.put.mockResolvedValue(createMockResponse({}, 200));
|
|
106
|
+
await subsetService.updateStaticElements('EmptySubset', 'TestDimension', 'TestHierarchy', false, []);
|
|
107
|
+
const [, body] = mockRestService.put.mock.calls[0];
|
|
108
|
+
const parsed = JSON.parse(body);
|
|
109
|
+
expect(parsed.value).toEqual([]);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
//# sourceMappingURL=subsetService.issue38.test.js.map
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
8
|
const TM1Service_1 = require("../services/TM1Service");
|
|
9
9
|
const RestService_1 = require("../services/RestService");
|
|
10
|
+
const User_1 = require("../objects/User");
|
|
10
11
|
// Mock all service dependencies
|
|
11
12
|
jest.mock('../services/RestService');
|
|
12
13
|
jest.mock('../services/DimensionService');
|
|
@@ -22,6 +23,19 @@ jest.mock('../services/FileService');
|
|
|
22
23
|
jest.mock('../services/SessionService');
|
|
23
24
|
jest.mock('../services/ServerService');
|
|
24
25
|
jest.mock('../services/MonitoringService');
|
|
26
|
+
jest.mock('../services/AnnotationService');
|
|
27
|
+
jest.mock('../services/ChoreService');
|
|
28
|
+
jest.mock('../services/GitService');
|
|
29
|
+
jest.mock('../services/ApplicationService');
|
|
30
|
+
jest.mock('../services/SandboxService');
|
|
31
|
+
jest.mock('../services/JobService');
|
|
32
|
+
jest.mock('../services/UserService');
|
|
33
|
+
jest.mock('../services/ThreadService');
|
|
34
|
+
jest.mock('../services/TransactionLogService');
|
|
35
|
+
jest.mock('../services/MessageLogService');
|
|
36
|
+
jest.mock('../services/ConfigurationService');
|
|
37
|
+
jest.mock('../services/AuditLogService');
|
|
38
|
+
jest.mock('../services/LoggerService');
|
|
25
39
|
describe('TM1Service', () => {
|
|
26
40
|
let tm1Service;
|
|
27
41
|
let mockRestService;
|
|
@@ -56,6 +70,8 @@ describe('TM1Service', () => {
|
|
|
56
70
|
setSandbox: jest.fn(),
|
|
57
71
|
getSandbox: jest.fn().mockReturnValue('test-sandbox'),
|
|
58
72
|
isLoggedIn: jest.fn().mockReturnValue(true),
|
|
73
|
+
getVersion: jest.fn().mockResolvedValue('12.0.0'),
|
|
74
|
+
version: undefined,
|
|
59
75
|
};
|
|
60
76
|
// Mock RestService constructor
|
|
61
77
|
RestService_1.RestService.mockImplementation(() => mockRestService);
|
|
@@ -75,6 +91,7 @@ describe('TM1Service', () => {
|
|
|
75
91
|
expect(tm1Service.security).toBeDefined();
|
|
76
92
|
expect(tm1Service.files).toBeDefined();
|
|
77
93
|
expect(tm1Service.sessions).toBeDefined();
|
|
94
|
+
expect(tm1Service.applications).toBeDefined();
|
|
78
95
|
});
|
|
79
96
|
test('should create RestService with provided config', () => {
|
|
80
97
|
expect(RestService_1.RestService).toHaveBeenCalledWith(mockConfig);
|
|
@@ -123,14 +140,16 @@ describe('TM1Service', () => {
|
|
|
123
140
|
});
|
|
124
141
|
});
|
|
125
142
|
describe('User and Authentication', () => {
|
|
126
|
-
test('should get current user with whoami', async () => {
|
|
127
|
-
|
|
143
|
+
test('should get current user as User object with whoami', async () => {
|
|
144
|
+
const expectedUser = new User_1.User('test-user', ['ADMIN'], 'Test User', undefined, User_1.UserType.Admin, true);
|
|
128
145
|
const mockSecurityService = {
|
|
129
|
-
getCurrentUser: jest.fn().mockResolvedValue(
|
|
146
|
+
getCurrentUser: jest.fn().mockResolvedValue(expectedUser)
|
|
130
147
|
};
|
|
131
148
|
tm1Service.security = mockSecurityService;
|
|
132
149
|
const result = await tm1Service.whoami();
|
|
133
|
-
expect(result).
|
|
150
|
+
expect(result).toBeInstanceOf(User_1.User);
|
|
151
|
+
expect(result.name).toBe('test-user');
|
|
152
|
+
expect(result).toBe(expectedUser);
|
|
134
153
|
expect(mockSecurityService.getCurrentUser).toHaveBeenCalledTimes(1);
|
|
135
154
|
});
|
|
136
155
|
test('should check if user is logged in', () => {
|
|
@@ -157,11 +176,11 @@ describe('TM1Service', () => {
|
|
|
157
176
|
expect(result).toEqual({ metadata: 'test-metadata' });
|
|
158
177
|
expect(mockRestService.get).toHaveBeenCalledWith('/$metadata');
|
|
159
178
|
});
|
|
160
|
-
test('should get TM1 version', async () => {
|
|
161
|
-
mockRestService.
|
|
179
|
+
test('should get TM1 version via cached RestService.getVersion', async () => {
|
|
180
|
+
mockRestService.getVersion.mockResolvedValueOnce('12.0.0');
|
|
162
181
|
const result = await tm1Service.getVersion();
|
|
163
182
|
expect(result).toBe('12.0.0');
|
|
164
|
-
expect(mockRestService.
|
|
183
|
+
expect(mockRestService.getVersion).toHaveBeenCalledTimes(1);
|
|
165
184
|
});
|
|
166
185
|
test('should handle metadata retrieval errors', async () => {
|
|
167
186
|
const metadataError = new Error('Metadata not available');
|
|
@@ -170,7 +189,7 @@ describe('TM1Service', () => {
|
|
|
170
189
|
});
|
|
171
190
|
test('should handle version retrieval errors', async () => {
|
|
172
191
|
const versionError = new Error('Version not available');
|
|
173
|
-
mockRestService.
|
|
192
|
+
mockRestService.getVersion.mockRejectedValueOnce(versionError);
|
|
174
193
|
await expect(tm1Service.getVersion()).rejects.toThrow('Version not available');
|
|
175
194
|
});
|
|
176
195
|
});
|
|
@@ -286,5 +305,58 @@ describe('TM1Service', () => {
|
|
|
286
305
|
expect(monitoring1).toBe(monitoring2);
|
|
287
306
|
});
|
|
288
307
|
});
|
|
308
|
+
describe('Lazy Services (Issue #82)', () => {
|
|
309
|
+
const lazyServiceNames = [
|
|
310
|
+
'annotations',
|
|
311
|
+
'chores',
|
|
312
|
+
'git',
|
|
313
|
+
'sandboxes',
|
|
314
|
+
'jobs',
|
|
315
|
+
'users',
|
|
316
|
+
'threads',
|
|
317
|
+
'transactionLogs',
|
|
318
|
+
'messageLogs',
|
|
319
|
+
'configuration',
|
|
320
|
+
'auditLogs',
|
|
321
|
+
'loggers',
|
|
322
|
+
];
|
|
323
|
+
test.each(lazyServiceNames)('should lazy-initialize %s service', (serviceName) => {
|
|
324
|
+
const instance = tm1Service[serviceName];
|
|
325
|
+
expect(instance).toBeDefined();
|
|
326
|
+
});
|
|
327
|
+
test.each(lazyServiceNames)('should cache %s service instance across accesses', (serviceName) => {
|
|
328
|
+
const first = tm1Service[serviceName];
|
|
329
|
+
const second = tm1Service[serviceName];
|
|
330
|
+
expect(second).toBe(first);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
describe('Version Getter (Issue #82)', () => {
|
|
334
|
+
test('should expose cached version via sync getter', () => {
|
|
335
|
+
Object.defineProperty(mockRestService, 'version', {
|
|
336
|
+
get: () => '11.8.0',
|
|
337
|
+
configurable: true,
|
|
338
|
+
});
|
|
339
|
+
expect(tm1Service.version).toBe('11.8.0');
|
|
340
|
+
});
|
|
341
|
+
test('should return undefined when version has not been fetched yet', () => {
|
|
342
|
+
Object.defineProperty(mockRestService, 'version', {
|
|
343
|
+
get: () => undefined,
|
|
344
|
+
configurable: true,
|
|
345
|
+
});
|
|
346
|
+
expect(tm1Service.version).toBeUndefined();
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
describe('reConnect (Issue #82)', () => {
|
|
350
|
+
test('should call connect without disconnecting (tm1py parity)', async () => {
|
|
351
|
+
await tm1Service.reConnect();
|
|
352
|
+
expect(mockRestService.connect).toHaveBeenCalledTimes(1);
|
|
353
|
+
expect(mockRestService.disconnect).not.toHaveBeenCalled();
|
|
354
|
+
});
|
|
355
|
+
test('should propagate errors from connect', async () => {
|
|
356
|
+
mockRestService.connect.mockRejectedValueOnce(new Error('Connect failed'));
|
|
357
|
+
await expect(tm1Service.reConnect()).rejects.toThrow('Connect failed');
|
|
358
|
+
expect(mockRestService.connect).toHaveBeenCalledTimes(1);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
289
361
|
});
|
|
290
362
|
//# sourceMappingURL=tm1Service.test.js.map
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AxiosResponse } from 'axios';
|
|
2
2
|
import { promises as fs } from 'fs';
|
|
3
3
|
import { RestService } from './RestService';
|
|
4
4
|
import { ObjectService } from './ObjectService';
|
|
@@ -17,12 +17,19 @@ import {
|
|
|
17
17
|
getApplicationMetadata
|
|
18
18
|
} from '../objects/Application';
|
|
19
19
|
import { formatUrl, verifyVersion } from '../utils/Utils';
|
|
20
|
+
import { TM1RestException } from '../exceptions/TM1Exception';
|
|
20
21
|
|
|
21
22
|
export class ApplicationService extends ObjectService {
|
|
23
|
+
private pathCache: Map<string, number> = new Map();
|
|
24
|
+
|
|
22
25
|
constructor(rest: RestService) {
|
|
23
26
|
super(rest);
|
|
24
27
|
}
|
|
25
28
|
|
|
29
|
+
public clearPathCache(): void {
|
|
30
|
+
this.pathCache.clear();
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
public async getAllPublicRootNames(): Promise<string[]> {
|
|
27
34
|
const url = "/Contents('Applications')/Contents";
|
|
28
35
|
const response = await this.rest.get(url);
|
|
@@ -35,7 +42,14 @@ export class ApplicationService extends ObjectService {
|
|
|
35
42
|
return response.data.value.map((application: any) => application.Name);
|
|
36
43
|
}
|
|
37
44
|
|
|
38
|
-
public async getNames(path: string, isPrivate: boolean = false): Promise<string[]> {
|
|
45
|
+
public async getNames(path: string, isPrivate: boolean = false, useCache: boolean = false): Promise<string[]> {
|
|
46
|
+
if (isPrivate) {
|
|
47
|
+
const resolved = await this._resolvePath(path, true, useCache);
|
|
48
|
+
// Leaf collection is always PrivateContents when isPrivate — even if parent path is public
|
|
49
|
+
const url = resolved.baseUrl + '/PrivateContents';
|
|
50
|
+
const response = await this.rest.get(url);
|
|
51
|
+
return response.data.value.map((application: any) => application.Name);
|
|
52
|
+
}
|
|
39
53
|
const contents = this.getContentsCollection(isPrivate);
|
|
40
54
|
const mid = this.buildPathSegments(path);
|
|
41
55
|
const baseUrl = "/Contents('Applications')" + mid + "/" + contents;
|
|
@@ -47,7 +61,8 @@ export class ApplicationService extends ObjectService {
|
|
|
47
61
|
path: string,
|
|
48
62
|
applicationType: string | ApplicationTypes,
|
|
49
63
|
name: string,
|
|
50
|
-
isPrivate: boolean = false
|
|
64
|
+
isPrivate: boolean = false,
|
|
65
|
+
useCache: boolean = false
|
|
51
66
|
): Promise<Application> {
|
|
52
67
|
const appType = this.parseApplicationType(applicationType);
|
|
53
68
|
|
|
@@ -56,7 +71,13 @@ export class ApplicationService extends ObjectService {
|
|
|
56
71
|
}
|
|
57
72
|
|
|
58
73
|
const requestName = this.withLegacySuffix(name, appType);
|
|
59
|
-
|
|
74
|
+
let baseUrl: string;
|
|
75
|
+
if (isPrivate) {
|
|
76
|
+
const resolved = await this._resolvePath(path, true, useCache);
|
|
77
|
+
baseUrl = formatUrl(resolved.baseUrl + "/PrivateContents('{}')", requestName);
|
|
78
|
+
} else {
|
|
79
|
+
baseUrl = this.buildApplicationUrl(path, isPrivate, requestName);
|
|
80
|
+
}
|
|
60
81
|
|
|
61
82
|
switch (appType) {
|
|
62
83
|
case ApplicationTypes.CUBE: {
|
|
@@ -79,7 +100,6 @@ export class ApplicationService extends ObjectService {
|
|
|
79
100
|
return new FolderApplication(path, name);
|
|
80
101
|
}
|
|
81
102
|
case ApplicationTypes.LINK: {
|
|
82
|
-
await this.rest.get(baseUrl);
|
|
83
103
|
const response = await this.rest.get(baseUrl + "?$expand=*");
|
|
84
104
|
return new LinkApplication(path, response.data?.Name || name, response.data?.URL || '');
|
|
85
105
|
}
|
|
@@ -132,7 +152,7 @@ export class ApplicationService extends ObjectService {
|
|
|
132
152
|
requestName
|
|
133
153
|
);
|
|
134
154
|
|
|
135
|
-
const arrayBufferResponse = await this.rest.get(contentUrl, { responseType: 'arraybuffer' }
|
|
155
|
+
const arrayBufferResponse = await this.rest.get(contentUrl, { responseType: 'arraybuffer' });
|
|
136
156
|
const metadataResponse = await this.rest.get(metadataUrl);
|
|
137
157
|
|
|
138
158
|
const buffer = Buffer.from(arrayBufferResponse.data);
|
|
@@ -163,7 +183,7 @@ export class ApplicationService extends ObjectService {
|
|
|
163
183
|
);
|
|
164
184
|
await this.rest.put(contentUrl, application.content, {
|
|
165
185
|
headers: this.binaryHttpHeader
|
|
166
|
-
}
|
|
186
|
+
});
|
|
167
187
|
}
|
|
168
188
|
|
|
169
189
|
return response;
|
|
@@ -184,7 +204,7 @@ export class ApplicationService extends ObjectService {
|
|
|
184
204
|
);
|
|
185
205
|
return await this.rest.post(url, application.content, {
|
|
186
206
|
headers: this.binaryHttpHeader
|
|
187
|
-
}
|
|
207
|
+
});
|
|
188
208
|
}
|
|
189
209
|
|
|
190
210
|
const url = formatUrl(
|
|
@@ -232,11 +252,28 @@ export class ApplicationService extends ObjectService {
|
|
|
232
252
|
path: string,
|
|
233
253
|
applicationType: ApplicationTypes,
|
|
234
254
|
name: string,
|
|
235
|
-
isPrivate: boolean = false
|
|
255
|
+
isPrivate: boolean = false,
|
|
256
|
+
useCache: boolean = false
|
|
236
257
|
): Promise<boolean> {
|
|
258
|
+
const requestName = this.withLegacySuffix(name, applicationType);
|
|
259
|
+
if (isPrivate) {
|
|
260
|
+
try {
|
|
261
|
+
const resolved = await this._resolvePath(path, true, useCache);
|
|
262
|
+
const url = formatUrl(resolved.baseUrl + "/PrivateContents('{}')", requestName);
|
|
263
|
+
return await this._exists(url);
|
|
264
|
+
} catch (error: any) {
|
|
265
|
+
// Path-not-found from _resolvePath means item doesn't exist
|
|
266
|
+
if (error instanceof Error && error.message.includes('not found')) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
if (error instanceof TM1RestException && error.statusCode === 404) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
237
275
|
const contents = this.getContentsCollection(isPrivate);
|
|
238
276
|
const mid = this.buildPathSegments(path);
|
|
239
|
-
const requestName = this.withLegacySuffix(name, applicationType);
|
|
240
277
|
const url = formatUrl(
|
|
241
278
|
"/Contents('Applications')" + mid + "/" + contents + "('{}')",
|
|
242
279
|
requestName
|
|
@@ -287,6 +324,230 @@ export class ApplicationService extends ObjectService {
|
|
|
287
324
|
return await this.update(document, isPrivate);
|
|
288
325
|
}
|
|
289
326
|
|
|
327
|
+
// NOTE: `flat` parameter is accepted for tm1py signature parity but output is always flat.
|
|
328
|
+
// Nested (tree) mode is not yet implemented.
|
|
329
|
+
public async discover(
|
|
330
|
+
path: string = '',
|
|
331
|
+
includePrivate: boolean = false,
|
|
332
|
+
recursive: boolean = false,
|
|
333
|
+
_flat: boolean = false
|
|
334
|
+
): Promise<Array<{ type: string; name: string; path: string; is_private: boolean }>> {
|
|
335
|
+
return this._discoverAtPath(path, includePrivate, recursive, false);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private async _discoverAtPath(
|
|
339
|
+
path: string,
|
|
340
|
+
includePrivate: boolean,
|
|
341
|
+
recursive: boolean,
|
|
342
|
+
inPrivateContext: boolean
|
|
343
|
+
): Promise<Array<{ type: string; name: string; path: string; is_private: boolean }>> {
|
|
344
|
+
const results: Array<{ type: string; name: string; path: string; is_private: boolean }> = [];
|
|
345
|
+
|
|
346
|
+
if (!inPrivateContext) {
|
|
347
|
+
const publicItems = await this._getContentsRaw(path, false, false);
|
|
348
|
+
const processed = await this._processItems(
|
|
349
|
+
publicItems, path, false, includePrivate, recursive, false
|
|
350
|
+
);
|
|
351
|
+
results.push(...processed);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (includePrivate || inPrivateContext) {
|
|
355
|
+
const privateItems = await this._getContentsRaw(path, true, inPrivateContext);
|
|
356
|
+
const processed = await this._processItems(
|
|
357
|
+
privateItems, path, true, includePrivate, recursive, true
|
|
358
|
+
);
|
|
359
|
+
results.push(...processed);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return results;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private async _getContentsRaw(path: string, isPrivate: boolean, inPrivateContext: boolean): Promise<any[]> {
|
|
366
|
+
try {
|
|
367
|
+
// Path segments: use PrivateContents only if we're already inside a private folder tree.
|
|
368
|
+
// Leaf collection: PrivateContents if isPrivate or inPrivateContext, else Contents.
|
|
369
|
+
const mid = inPrivateContext
|
|
370
|
+
? this.buildPrivatePathSegments(path)
|
|
371
|
+
: this.buildPathSegments(path);
|
|
372
|
+
const collection = (inPrivateContext || isPrivate) ? 'PrivateContents' : 'Contents';
|
|
373
|
+
const url = "/Contents('Applications')" + mid + "/" + collection;
|
|
374
|
+
const response = await this.rest.get(url);
|
|
375
|
+
return response.data?.value || [];
|
|
376
|
+
} catch (error: any) {
|
|
377
|
+
if (error instanceof TM1RestException && error.statusCode === 404) {
|
|
378
|
+
return [];
|
|
379
|
+
}
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private async _processItems(
|
|
385
|
+
items: any[],
|
|
386
|
+
path: string,
|
|
387
|
+
isPrivate: boolean,
|
|
388
|
+
includePrivate: boolean,
|
|
389
|
+
recursive: boolean,
|
|
390
|
+
inPrivateContext: boolean
|
|
391
|
+
): Promise<Array<{ type: string; name: string; path: string; is_private: boolean }>> {
|
|
392
|
+
const results: Array<{ type: string; name: string; path: string; is_private: boolean }> = [];
|
|
393
|
+
|
|
394
|
+
const folderPromises: Array<Promise<Array<{ type: string; name: string; path: string; is_private: boolean }>>> = [];
|
|
395
|
+
|
|
396
|
+
for (const item of items) {
|
|
397
|
+
const odataType = item['@odata.type'] || '';
|
|
398
|
+
const typeName = this._extractTypeFromOdata(odataType);
|
|
399
|
+
const itemName = item.Name || '';
|
|
400
|
+
const itemPath = path ? `${path}/${itemName}` : itemName;
|
|
401
|
+
|
|
402
|
+
results.push({
|
|
403
|
+
type: typeName,
|
|
404
|
+
name: itemName,
|
|
405
|
+
path: itemPath,
|
|
406
|
+
is_private: isPrivate || inPrivateContext
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
if (recursive && typeName === 'Folder') {
|
|
410
|
+
folderPromises.push(
|
|
411
|
+
this._discoverAtPath(
|
|
412
|
+
itemPath, includePrivate, recursive, inPrivateContext || isPrivate
|
|
413
|
+
)
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const folderResults = await Promise.all(folderPromises);
|
|
419
|
+
for (const subItems of folderResults) {
|
|
420
|
+
results.push(...subItems);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return results;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private _extractTypeFromOdata(odataType: string): string {
|
|
427
|
+
if (!odataType) return 'Unknown';
|
|
428
|
+
const parts = odataType.split('.');
|
|
429
|
+
const raw = parts[parts.length - 1] || 'Unknown';
|
|
430
|
+
// TM1 returns types like 'FolderApplication', 'CubeApplication' — strip suffix
|
|
431
|
+
// so folder check works consistently
|
|
432
|
+
return raw.replace(/Application$/, '') || raw;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private async _findPrivateBoundary(segments: string[]): Promise<number> {
|
|
436
|
+
let publicUrl = "/Contents('Applications')";
|
|
437
|
+
for (let i = 0; i < segments.length; i++) {
|
|
438
|
+
const testUrl = publicUrl + formatUrl("/Contents('{}')", segments[i]) + "?$top=0";
|
|
439
|
+
try {
|
|
440
|
+
await this.rest.get(testUrl);
|
|
441
|
+
publicUrl += formatUrl("/Contents('{}')", segments[i]);
|
|
442
|
+
} catch (error: any) {
|
|
443
|
+
if (error instanceof TM1RestException && error.statusCode === 404) {
|
|
444
|
+
const privateTestUrl = publicUrl + formatUrl("/PrivateContents('{}')", segments[i]) + "?$top=0";
|
|
445
|
+
try {
|
|
446
|
+
await this.rest.get(privateTestUrl);
|
|
447
|
+
return i;
|
|
448
|
+
} catch (innerError: any) {
|
|
449
|
+
if (innerError instanceof TM1RestException && innerError.statusCode === 404) {
|
|
450
|
+
return -1;
|
|
451
|
+
}
|
|
452
|
+
throw innerError;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
throw error;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return segments.length;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private _buildPathUrl(segments: string[], privateBoundary?: number): string {
|
|
462
|
+
if (!segments.length) return '';
|
|
463
|
+
|
|
464
|
+
let url = '';
|
|
465
|
+
const boundary = privateBoundary ?? segments.length;
|
|
466
|
+
|
|
467
|
+
for (let i = 0; i < segments.length; i++) {
|
|
468
|
+
if (i < boundary) {
|
|
469
|
+
url += formatUrl("/Contents('{}')", segments[i]);
|
|
470
|
+
} else {
|
|
471
|
+
url += formatUrl("/PrivateContents('{}')", segments[i]);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return url;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private async _resolvePath(
|
|
478
|
+
path: string,
|
|
479
|
+
isPrivate: boolean = false,
|
|
480
|
+
useCache: boolean = false
|
|
481
|
+
): Promise<{ baseUrl: string; inPrivateContext: boolean }> {
|
|
482
|
+
if (!path || !path.trim()) {
|
|
483
|
+
return { baseUrl: "/Contents('Applications')", inPrivateContext: isPrivate };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const segments = path.split('/').filter(s => s.trim().length > 0);
|
|
487
|
+
|
|
488
|
+
if (!isPrivate) {
|
|
489
|
+
const mid = segments.map(s => formatUrl("/Contents('{}')", s)).join('');
|
|
490
|
+
return { baseUrl: "/Contents('Applications')" + mid, inPrivateContext: false };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const cacheKey = path;
|
|
494
|
+
if (useCache && this.pathCache.has(cacheKey)) {
|
|
495
|
+
const boundary = this.pathCache.get(cacheKey)!;
|
|
496
|
+
const mid = this._buildPathUrl(segments, boundary);
|
|
497
|
+
return {
|
|
498
|
+
baseUrl: "/Contents('Applications')" + mid,
|
|
499
|
+
inPrivateContext: boundary < segments.length
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Try all-public first (optimistic)
|
|
504
|
+
try {
|
|
505
|
+
const publicUrl = "/Contents('Applications')" +
|
|
506
|
+
segments.map(s => formatUrl("/Contents('{}')", s)).join('') + "?$top=0";
|
|
507
|
+
await this.rest.get(publicUrl);
|
|
508
|
+
if (useCache) this.pathCache.set(cacheKey, segments.length);
|
|
509
|
+
return {
|
|
510
|
+
baseUrl: "/Contents('Applications')" +
|
|
511
|
+
segments.map(s => formatUrl("/Contents('{}')", s)).join(''),
|
|
512
|
+
inPrivateContext: false
|
|
513
|
+
};
|
|
514
|
+
} catch (error: any) {
|
|
515
|
+
if (!(error instanceof TM1RestException && error.statusCode === 404)) {
|
|
516
|
+
throw error;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Try all-private
|
|
521
|
+
try {
|
|
522
|
+
const privateUrl = "/Contents('Applications')" +
|
|
523
|
+
segments.map(s => formatUrl("/PrivateContents('{}')", s)).join('') + "?$top=0";
|
|
524
|
+
await this.rest.get(privateUrl);
|
|
525
|
+
if (useCache) this.pathCache.set(cacheKey, 0);
|
|
526
|
+
return {
|
|
527
|
+
baseUrl: "/Contents('Applications')" +
|
|
528
|
+
segments.map(s => formatUrl("/PrivateContents('{}')", s)).join(''),
|
|
529
|
+
inPrivateContext: true
|
|
530
|
+
};
|
|
531
|
+
} catch (error: any) {
|
|
532
|
+
if (!(error instanceof TM1RestException && error.statusCode === 404)) {
|
|
533
|
+
throw error;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Iterative boundary search
|
|
538
|
+
const boundary = await this._findPrivateBoundary(segments);
|
|
539
|
+
if (boundary === -1) {
|
|
540
|
+
throw new Error(`Application path not found: ${path}`);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (useCache) this.pathCache.set(cacheKey, boundary);
|
|
544
|
+
const mid = this._buildPathUrl(segments, boundary);
|
|
545
|
+
return {
|
|
546
|
+
baseUrl: "/Contents('Applications')" + mid,
|
|
547
|
+
inPrivateContext: boundary < segments.length
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
290
551
|
private parseApplicationType(applicationType: string | ApplicationTypes): ApplicationTypes {
|
|
291
552
|
if (typeof applicationType === 'string') {
|
|
292
553
|
const upper = applicationType.toUpperCase();
|
|
@@ -309,6 +570,17 @@ export class ApplicationService extends ObjectService {
|
|
|
309
570
|
.join('');
|
|
310
571
|
}
|
|
311
572
|
|
|
573
|
+
private buildPrivatePathSegments(path: string): string {
|
|
574
|
+
if (!path || !path.trim()) {
|
|
575
|
+
return '';
|
|
576
|
+
}
|
|
577
|
+
return path
|
|
578
|
+
.split('/')
|
|
579
|
+
.filter(segment => segment.trim().length > 0)
|
|
580
|
+
.map(segment => formatUrl("/PrivateContents('{}')", segment))
|
|
581
|
+
.join('');
|
|
582
|
+
}
|
|
583
|
+
|
|
312
584
|
private getContentsCollection(isPrivate: boolean): string {
|
|
313
585
|
return isPrivate ? 'PrivateContents' : 'Contents';
|
|
314
586
|
}
|