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
|
@@ -2,8 +2,10 @@ import { AxiosResponse } from 'axios';
|
|
|
2
2
|
import { RestService } from './RestService';
|
|
3
3
|
import { ObjectService } from './ObjectService';
|
|
4
4
|
import { Subset } from '../objects/Subset';
|
|
5
|
+
import { Element } from '../objects/Element';
|
|
5
6
|
import { ElementService } from './ElementService';
|
|
6
7
|
import { TM1RestException } from '../exceptions/TM1Exception';
|
|
8
|
+
import { formatUrl } from '../utils/Utils';
|
|
7
9
|
|
|
8
10
|
export class SubsetService extends ObjectService {
|
|
9
11
|
private elementService?: ElementService;
|
|
@@ -257,6 +259,52 @@ export class SubsetService extends ObjectService {
|
|
|
257
259
|
);
|
|
258
260
|
}
|
|
259
261
|
|
|
262
|
+
public async updateStaticElements(
|
|
263
|
+
subsetOrName: Subset | string,
|
|
264
|
+
dimensionName?: string,
|
|
265
|
+
hierarchyName?: string,
|
|
266
|
+
isPrivate: boolean = false,
|
|
267
|
+
elements?: Array<string | Element>
|
|
268
|
+
): Promise<AxiosResponse> {
|
|
269
|
+
let subsetName: string;
|
|
270
|
+
let dimName: string;
|
|
271
|
+
let hierName: string;
|
|
272
|
+
let elementNames: string[];
|
|
273
|
+
|
|
274
|
+
if (subsetOrName instanceof Subset) {
|
|
275
|
+
const subset = subsetOrName;
|
|
276
|
+
subsetName = subset.name;
|
|
277
|
+
dimName = dimensionName || subset.dimensionName;
|
|
278
|
+
hierName = hierarchyName || subset.hierarchyName;
|
|
279
|
+
elementNames = elements
|
|
280
|
+
? elements.map(e => typeof e === 'string' ? e : e.name)
|
|
281
|
+
: subset.elements;
|
|
282
|
+
} else {
|
|
283
|
+
if (!dimensionName) {
|
|
284
|
+
throw new Error('dimensionName is required when passing subset name as string');
|
|
285
|
+
}
|
|
286
|
+
subsetName = subsetOrName;
|
|
287
|
+
dimName = dimensionName;
|
|
288
|
+
hierName = hierarchyName || dimName;
|
|
289
|
+
elementNames = (elements || []).map(e => typeof e === 'string' ? e : e.name);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const subsetsCollection = this.getSubsetCollection(isPrivate);
|
|
293
|
+
const url = this.formatUrl(
|
|
294
|
+
"/Dimensions('{}')/Hierarchies('{}')/{}('{}')/Elements/$ref",
|
|
295
|
+
dimName, hierName, subsetsCollection, subsetName);
|
|
296
|
+
|
|
297
|
+
const body = {
|
|
298
|
+
value: elementNames.map(elementName => ({
|
|
299
|
+
'@odata.id': formatUrl(
|
|
300
|
+
"Dimensions('{}')/Hierarchies('{}')/Elements('{}')",
|
|
301
|
+
dimName, hierName, elementName)
|
|
302
|
+
}))
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
return await this.rest.put(url, JSON.stringify(body));
|
|
306
|
+
}
|
|
307
|
+
|
|
260
308
|
private async updateSubsetInstance(subset: Subset, isPrivate: boolean): Promise<AxiosResponse> {
|
|
261
309
|
if (subset.isStatic) {
|
|
262
310
|
await this.deleteElementsFromStaticSubset(
|
|
@@ -7,6 +7,20 @@ import { DebuggerService } from './DebuggerService';
|
|
|
7
7
|
import { BulkService } from './BulkService';
|
|
8
8
|
import { AsyncOperationService } from './AsyncOperationService';
|
|
9
9
|
import { PowerBiService } from './PowerBiService';
|
|
10
|
+
import { ApplicationService } from './ApplicationService';
|
|
11
|
+
import { AnnotationService } from './AnnotationService';
|
|
12
|
+
import { ChoreService } from './ChoreService';
|
|
13
|
+
import { GitService } from './GitService';
|
|
14
|
+
import { SandboxService } from './SandboxService';
|
|
15
|
+
import { JobService } from './JobService';
|
|
16
|
+
import { UserService } from './UserService';
|
|
17
|
+
import { ThreadService } from './ThreadService';
|
|
18
|
+
import { TransactionLogService } from './TransactionLogService';
|
|
19
|
+
import { MessageLogService } from './MessageLogService';
|
|
20
|
+
import { ConfigurationService } from './ConfigurationService';
|
|
21
|
+
import { AuditLogService } from './AuditLogService';
|
|
22
|
+
import { LoggerService } from './LoggerService';
|
|
23
|
+
import { User } from '../objects/User';
|
|
10
24
|
import {
|
|
11
25
|
CubeService,
|
|
12
26
|
ElementService,
|
|
@@ -25,6 +39,19 @@ export class TM1Service {
|
|
|
25
39
|
private _server?: ServerService;
|
|
26
40
|
private _monitoring?: MonitoringService;
|
|
27
41
|
|
|
42
|
+
private _annotations?: AnnotationService;
|
|
43
|
+
private _chores?: ChoreService;
|
|
44
|
+
private _git?: GitService;
|
|
45
|
+
private _sandboxes?: SandboxService;
|
|
46
|
+
private _jobs?: JobService;
|
|
47
|
+
private _users?: UserService;
|
|
48
|
+
private _threads?: ThreadService;
|
|
49
|
+
private _transactionLogs?: TransactionLogService;
|
|
50
|
+
private _messageLogs?: MessageLogService;
|
|
51
|
+
private _configuration?: ConfigurationService;
|
|
52
|
+
private _auditLogs?: AuditLogService;
|
|
53
|
+
private _loggers?: LoggerService;
|
|
54
|
+
|
|
28
55
|
public dimensions: DimensionService;
|
|
29
56
|
public hierarchies: HierarchyService;
|
|
30
57
|
public subsets: SubsetService;
|
|
@@ -41,6 +68,7 @@ export class TM1Service {
|
|
|
41
68
|
public bulk: BulkService;
|
|
42
69
|
public asyncOperations: AsyncOperationService;
|
|
43
70
|
public powerbi: PowerBiService;
|
|
71
|
+
public applications: ApplicationService;
|
|
44
72
|
|
|
45
73
|
constructor(config: RestServiceConfig) {
|
|
46
74
|
this._tm1Rest = new RestService(config);
|
|
@@ -67,6 +95,7 @@ export class TM1Service {
|
|
|
67
95
|
this.debugger = new DebuggerService(this._tm1Rest);
|
|
68
96
|
this.bulk = new BulkService(this._tm1Rest, this.cells, this.views);
|
|
69
97
|
this.powerbi = new PowerBiService(this._tm1Rest);
|
|
98
|
+
this.applications = new ApplicationService(this._tm1Rest);
|
|
70
99
|
}
|
|
71
100
|
|
|
72
101
|
public async connect(): Promise<void> {
|
|
@@ -95,9 +124,92 @@ export class TM1Service {
|
|
|
95
124
|
return this._monitoring;
|
|
96
125
|
}
|
|
97
126
|
|
|
98
|
-
public
|
|
99
|
-
|
|
100
|
-
|
|
127
|
+
public get annotations(): AnnotationService {
|
|
128
|
+
if (!this._annotations) {
|
|
129
|
+
this._annotations = new AnnotationService(this._tm1Rest);
|
|
130
|
+
}
|
|
131
|
+
return this._annotations;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
public get chores(): ChoreService {
|
|
135
|
+
if (!this._chores) {
|
|
136
|
+
this._chores = new ChoreService(this._tm1Rest);
|
|
137
|
+
}
|
|
138
|
+
return this._chores;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public get git(): GitService {
|
|
142
|
+
if (!this._git) {
|
|
143
|
+
this._git = new GitService(this._tm1Rest);
|
|
144
|
+
}
|
|
145
|
+
return this._git;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public get sandboxes(): SandboxService {
|
|
149
|
+
if (!this._sandboxes) {
|
|
150
|
+
this._sandboxes = new SandboxService(this._tm1Rest);
|
|
151
|
+
}
|
|
152
|
+
return this._sandboxes;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
public get jobs(): JobService {
|
|
156
|
+
if (!this._jobs) {
|
|
157
|
+
this._jobs = new JobService(this._tm1Rest);
|
|
158
|
+
}
|
|
159
|
+
return this._jobs;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
public get users(): UserService {
|
|
163
|
+
if (!this._users) {
|
|
164
|
+
this._users = new UserService(this._tm1Rest);
|
|
165
|
+
}
|
|
166
|
+
return this._users;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
public get threads(): ThreadService {
|
|
170
|
+
if (!this._threads) {
|
|
171
|
+
this._threads = new ThreadService(this._tm1Rest);
|
|
172
|
+
}
|
|
173
|
+
return this._threads;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
public get transactionLogs(): TransactionLogService {
|
|
177
|
+
if (!this._transactionLogs) {
|
|
178
|
+
this._transactionLogs = new TransactionLogService(this._tm1Rest);
|
|
179
|
+
}
|
|
180
|
+
return this._transactionLogs;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
public get messageLogs(): MessageLogService {
|
|
184
|
+
if (!this._messageLogs) {
|
|
185
|
+
this._messageLogs = new MessageLogService(this._tm1Rest);
|
|
186
|
+
}
|
|
187
|
+
return this._messageLogs;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
public get configuration(): ConfigurationService {
|
|
191
|
+
if (!this._configuration) {
|
|
192
|
+
this._configuration = new ConfigurationService(this._tm1Rest);
|
|
193
|
+
}
|
|
194
|
+
return this._configuration;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
public get auditLogs(): AuditLogService {
|
|
198
|
+
if (!this._auditLogs) {
|
|
199
|
+
this._auditLogs = new AuditLogService(this._tm1Rest);
|
|
200
|
+
}
|
|
201
|
+
return this._auditLogs;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
public get loggers(): LoggerService {
|
|
205
|
+
if (!this._loggers) {
|
|
206
|
+
this._loggers = new LoggerService(this._tm1Rest);
|
|
207
|
+
}
|
|
208
|
+
return this._loggers;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
public async whoami(): Promise<User> {
|
|
212
|
+
return await this.security.getCurrentUser();
|
|
101
213
|
}
|
|
102
214
|
|
|
103
215
|
public async getMetadata(): Promise<any> {
|
|
@@ -105,9 +217,12 @@ export class TM1Service {
|
|
|
105
217
|
return response.data;
|
|
106
218
|
}
|
|
107
219
|
|
|
220
|
+
public get version(): string | undefined {
|
|
221
|
+
return this._tm1Rest.version;
|
|
222
|
+
}
|
|
223
|
+
|
|
108
224
|
public async getVersion(): Promise<string> {
|
|
109
|
-
|
|
110
|
-
return response.data.value;
|
|
225
|
+
return await this._tm1Rest.getVersion();
|
|
111
226
|
}
|
|
112
227
|
|
|
113
228
|
public get connection(): RestService {
|
|
@@ -130,6 +245,12 @@ export class TM1Service {
|
|
|
130
245
|
return this._tm1Rest.isLoggedIn();
|
|
131
246
|
}
|
|
132
247
|
|
|
248
|
+
/** Reconnects without teardown. Use reAuthenticate() for full disconnect+reconnect. */
|
|
249
|
+
public async reConnect(): Promise<void> {
|
|
250
|
+
await this._tm1Rest.connect();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Full teardown + reconnect. If disconnect() throws, connect() is not attempted. */
|
|
133
254
|
public async reAuthenticate(): Promise<void> {
|
|
134
255
|
await this._tm1Rest.disconnect();
|
|
135
256
|
await this._tm1Rest.connect();
|
|
@@ -150,4 +271,4 @@ export class TM1Service {
|
|
|
150
271
|
console.warn(`Logout failed due to exception: ${error}`);
|
|
151
272
|
}
|
|
152
273
|
}
|
|
153
|
-
}
|
|
274
|
+
}
|
package/src/services/index.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
export { CubeService } from './CubeService';
|
|
3
3
|
export { ElementService } from './ElementService';
|
|
4
4
|
export { CellService } from './CellService';
|
|
5
|
-
export { ProcessService } from './ProcessService';
|
|
5
|
+
export { ProcessService, CompileSyntaxError } from './ProcessService';
|
|
6
6
|
export { ViewService } from './ViewService';
|
|
7
7
|
export { SecurityService } from './SecurityService';
|
|
8
8
|
export { FileService } from './FileService';
|
|
@@ -65,15 +65,30 @@ describe('100% TM1py Parity Function Existence', () => {
|
|
|
65
65
|
});
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
describe('ProcessService - Debug Operations (
|
|
69
|
-
test('All
|
|
68
|
+
describe('ProcessService - Debug Operations & Parity (11 functions)', () => {
|
|
69
|
+
test('All ProcessService debug and parity functions should exist', () => {
|
|
70
|
+
// Debug step/continue (from #33)
|
|
70
71
|
expect(typeof processService.debugStepOver).toBe('function');
|
|
71
72
|
expect(typeof processService.debugStepIn).toBe('function');
|
|
72
73
|
expect(typeof processService.debugStepOut).toBe('function');
|
|
73
74
|
expect(typeof processService.debugContinue).toBe('function');
|
|
74
75
|
expect(typeof processService.evaluateBooleanTiExpression).toBe('function');
|
|
75
76
|
|
|
76
|
-
|
|
77
|
+
// Debug breakpoint methods — renamed for tm1py parity (#36)
|
|
78
|
+
expect(typeof processService.debugGetBreakpoints).toBe('function');
|
|
79
|
+
expect(typeof processService.debugAddBreakpoint).toBe('function');
|
|
80
|
+
expect(typeof processService.debugRemoveBreakpoint).toBe('function');
|
|
81
|
+
|
|
82
|
+
// Async polling — rewritten for tm1py parity (#36)
|
|
83
|
+
expect(typeof processService.pollExecuteWithReturn).toBe('function');
|
|
84
|
+
|
|
85
|
+
// Unbound process compile (#36)
|
|
86
|
+
expect(typeof processService.compileProcess).toBe('function');
|
|
87
|
+
|
|
88
|
+
// TI expression evaluation (#36)
|
|
89
|
+
expect(typeof processService.evaluateTiExpression).toBe('function');
|
|
90
|
+
|
|
91
|
+
console.log('✅ All 11 ProcessService functions exist');
|
|
77
92
|
});
|
|
78
93
|
});
|
|
79
94
|
|
|
@@ -96,11 +111,11 @@ describe('100% TM1py Parity Function Existence', () => {
|
|
|
96
111
|
});
|
|
97
112
|
|
|
98
113
|
describe('100% Parity Achievement Summary', () => {
|
|
99
|
-
test('should confirm all
|
|
114
|
+
test('should confirm all 28 functions are implemented', () => {
|
|
100
115
|
const implementedFunctions = {
|
|
101
116
|
ElementService: [
|
|
102
117
|
'deleteElementsUseTi',
|
|
103
|
-
'deleteEdgesUseBlob',
|
|
118
|
+
'deleteEdgesUseBlob',
|
|
104
119
|
'getElementsByLevel',
|
|
105
120
|
'getElementsFilteredByWildcard',
|
|
106
121
|
'getAttributeOfElements',
|
|
@@ -119,9 +134,15 @@ describe('100% TM1py Parity Function Existence', () => {
|
|
|
119
134
|
ProcessService: [
|
|
120
135
|
'debugStepOver',
|
|
121
136
|
'debugStepIn',
|
|
122
|
-
'debugStepOut',
|
|
137
|
+
'debugStepOut',
|
|
123
138
|
'debugContinue',
|
|
124
|
-
'evaluateBooleanTiExpression'
|
|
139
|
+
'evaluateBooleanTiExpression',
|
|
140
|
+
'debugGetBreakpoints',
|
|
141
|
+
'debugAddBreakpoint',
|
|
142
|
+
'debugRemoveBreakpoint',
|
|
143
|
+
'pollExecuteWithReturn',
|
|
144
|
+
'compileProcess',
|
|
145
|
+
'evaluateTiExpression'
|
|
125
146
|
],
|
|
126
147
|
CellService: [
|
|
127
148
|
'writeDataframeAsync',
|
|
@@ -153,7 +174,7 @@ describe('100% TM1py Parity Function Existence', () => {
|
|
|
153
174
|
const totalFunctions = Object.values(implementedFunctions)
|
|
154
175
|
.reduce((total, funcs) => total + funcs.length, 0);
|
|
155
176
|
|
|
156
|
-
expect(totalFunctions).toBe(
|
|
177
|
+
expect(totalFunctions).toBe(31); // 16 + 11 + 3 + 1
|
|
157
178
|
|
|
158
179
|
console.log('🎉 100% TM1py Parity Achieved!');
|
|
159
180
|
console.log(`✅ ${implementedFunctions.ElementService.length} ElementService functions`);
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ApplicationService Tests — Issue #38 new methods
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ApplicationService } from '../services/ApplicationService';
|
|
6
|
+
import { RestService } from '../services/RestService';
|
|
7
|
+
import { TM1RestException } from '../exceptions/TM1Exception';
|
|
8
|
+
import { FolderApplication, CubeApplication, ApplicationTypes } from '../objects/Application';
|
|
9
|
+
|
|
10
|
+
const createMockResponse = (data: any, status: number = 200) => ({
|
|
11
|
+
data,
|
|
12
|
+
status,
|
|
13
|
+
statusText: status === 200 ? 'OK' : status === 201 ? 'Created' : status === 204 ? 'No Content' : 'Error',
|
|
14
|
+
headers: {},
|
|
15
|
+
config: {} as any
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('ApplicationService — Issue #38 new methods', () => {
|
|
19
|
+
let applicationService: ApplicationService;
|
|
20
|
+
let mockRestService: jest.Mocked<RestService>;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
mockRestService = {
|
|
24
|
+
get: jest.fn(),
|
|
25
|
+
post: jest.fn(),
|
|
26
|
+
patch: jest.fn(),
|
|
27
|
+
delete: jest.fn(),
|
|
28
|
+
put: jest.fn(),
|
|
29
|
+
config: {} as any,
|
|
30
|
+
rest: {} as any,
|
|
31
|
+
buildBaseUrl: jest.fn(),
|
|
32
|
+
extractErrorMessage: jest.fn()
|
|
33
|
+
} as any;
|
|
34
|
+
|
|
35
|
+
applicationService = new ApplicationService(mockRestService);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// ===== discover =====
|
|
39
|
+
|
|
40
|
+
describe('discover', () => {
|
|
41
|
+
test('should return public items at root when includePrivate=false', async () => {
|
|
42
|
+
mockRestService.get.mockImplementation(async (url: string) => {
|
|
43
|
+
if (url.includes('Contents') && !url.includes('PrivateContents')) {
|
|
44
|
+
return createMockResponse({
|
|
45
|
+
value: [
|
|
46
|
+
{ '@odata.type': '#ibm.tm1.api.v1.CubeApplication', Name: 'SalesCube' },
|
|
47
|
+
{ '@odata.type': '#ibm.tm1.api.v1.FolderApplication', Name: 'Reports' }
|
|
48
|
+
]
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return createMockResponse({ value: [] });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const results = await applicationService.discover('', false, false);
|
|
55
|
+
|
|
56
|
+
expect(results.length).toBeGreaterThanOrEqual(2);
|
|
57
|
+
const names = results.map(r => r.name);
|
|
58
|
+
expect(names).toContain('SalesCube');
|
|
59
|
+
expect(names).toContain('Reports');
|
|
60
|
+
// All should be public
|
|
61
|
+
results.forEach(r => expect(r.is_private).toBe(false));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('should include private items when includePrivate=true', async () => {
|
|
65
|
+
mockRestService.get.mockImplementation(async (url: string) => {
|
|
66
|
+
if (url.includes('PrivateContents')) {
|
|
67
|
+
return createMockResponse({
|
|
68
|
+
value: [
|
|
69
|
+
{ '@odata.type': '#ibm.tm1.api.v1.CubeApplication', Name: 'PrivateReport' }
|
|
70
|
+
]
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return createMockResponse({
|
|
74
|
+
value: [
|
|
75
|
+
{ '@odata.type': '#ibm.tm1.api.v1.CubeApplication', Name: 'PublicCube' }
|
|
76
|
+
]
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const results = await applicationService.discover('', true, false);
|
|
81
|
+
|
|
82
|
+
const privateItems = results.filter(r => r.is_private);
|
|
83
|
+
const publicItems = results.filter(r => !r.is_private);
|
|
84
|
+
expect(privateItems.length).toBeGreaterThanOrEqual(1);
|
|
85
|
+
expect(publicItems.length).toBeGreaterThanOrEqual(1);
|
|
86
|
+
const privateNames = privateItems.map(r => r.name);
|
|
87
|
+
expect(privateNames).toContain('PrivateReport');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('should find private items inside public folders with includePrivate', async () => {
|
|
91
|
+
mockRestService.get.mockImplementation(async (url: string) => {
|
|
92
|
+
// Public root contents: one public folder
|
|
93
|
+
if (url === "/Contents('Applications')/Contents") {
|
|
94
|
+
return createMockResponse({
|
|
95
|
+
value: [
|
|
96
|
+
{ '@odata.type': '#ibm.tm1.api.v1.CubeApplication', Name: 'PublicCube' }
|
|
97
|
+
]
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
// Private root contents: uses public path + PrivateContents leaf
|
|
101
|
+
if (url === "/Contents('Applications')/PrivateContents") {
|
|
102
|
+
return createMockResponse({
|
|
103
|
+
value: [
|
|
104
|
+
{ '@odata.type': '#ibm.tm1.api.v1.CubeApplication', Name: 'PrivateInPublicRoot' }
|
|
105
|
+
]
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return createMockResponse({ value: [] });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const results = await applicationService.discover('', true, false);
|
|
112
|
+
|
|
113
|
+
const names = results.map(r => r.name);
|
|
114
|
+
expect(names).toContain('PublicCube');
|
|
115
|
+
expect(names).toContain('PrivateInPublicRoot');
|
|
116
|
+
const privateItem = results.find(r => r.name === 'PrivateInPublicRoot');
|
|
117
|
+
expect(privateItem?.is_private).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('should recurse into folders when recursive=true', async () => {
|
|
121
|
+
// TM1 returns @odata.type like '#ibm.tm1.api.v1.FolderApplication'.
|
|
122
|
+
// _extractTypeFromOdata strips 'Application' suffix → 'Folder', which triggers recursion.
|
|
123
|
+
mockRestService.get.mockImplementation(async (url: string) => {
|
|
124
|
+
if (url.includes('PrivateContents')) {
|
|
125
|
+
return createMockResponse({ value: [] });
|
|
126
|
+
}
|
|
127
|
+
// Root Contents call
|
|
128
|
+
if (url.match(/Contents\('Applications'\)\/Contents$/)) {
|
|
129
|
+
return createMockResponse({
|
|
130
|
+
value: [
|
|
131
|
+
{ '@odata.type': '#ibm.tm1.api.v1.FolderApplication', Name: 'Reports' }
|
|
132
|
+
]
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
// Sub-folder Contents call
|
|
136
|
+
if (url.includes("Contents('Reports')/Contents")) {
|
|
137
|
+
return createMockResponse({
|
|
138
|
+
value: [
|
|
139
|
+
{ '@odata.type': '#ibm.tm1.api.v1.CubeApplication', Name: 'SalesReport' }
|
|
140
|
+
]
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return createMockResponse({ value: [] });
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const results = await applicationService.discover('', false, true);
|
|
147
|
+
|
|
148
|
+
const names = results.map(r => r.name);
|
|
149
|
+
expect(names).toContain('Reports');
|
|
150
|
+
expect(names).toContain('SalesReport');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('should use PrivateContents for nested paths in private context', async () => {
|
|
154
|
+
mockRestService.get.mockImplementation(async (url: string) => {
|
|
155
|
+
// Root private contents returns a folder
|
|
156
|
+
if (url === "/Contents('Applications')/PrivateContents") {
|
|
157
|
+
return createMockResponse({
|
|
158
|
+
value: [
|
|
159
|
+
{ '@odata.type': '#ibm.tm1.api.v1.FolderApplication', Name: 'PrivateFolder' }
|
|
160
|
+
]
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
// Nested private folder contents — must use PrivateContents, not Contents
|
|
164
|
+
if (url.includes("PrivateContents('PrivateFolder')/PrivateContents")) {
|
|
165
|
+
return createMockResponse({
|
|
166
|
+
value: [
|
|
167
|
+
{ '@odata.type': '#ibm.tm1.api.v1.CubeApplication', Name: 'DeepCube' }
|
|
168
|
+
]
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
// If the code incorrectly uses Contents for nested private paths, this will 404
|
|
172
|
+
if (url.includes("Contents('PrivateFolder')/Contents")) {
|
|
173
|
+
throw new TM1RestException('Not found', 404, { status: 404 });
|
|
174
|
+
}
|
|
175
|
+
return createMockResponse({ value: [] });
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const results = await applicationService.discover('', true, true);
|
|
179
|
+
|
|
180
|
+
const names = results.map(r => r.name);
|
|
181
|
+
expect(names).toContain('PrivateFolder');
|
|
182
|
+
expect(names).toContain('DeepCube');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('should return empty array on 404', async () => {
|
|
186
|
+
mockRestService.get.mockRejectedValue(
|
|
187
|
+
new TM1RestException('Not found', 404, { status: 404 })
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const results = await applicationService.discover('NonExistentPath', false, false);
|
|
191
|
+
|
|
192
|
+
expect(results).toEqual([]);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('should include type in result items', async () => {
|
|
196
|
+
mockRestService.get.mockImplementation(async (url: string) => {
|
|
197
|
+
if (url.includes('PrivateContents')) {
|
|
198
|
+
return createMockResponse({ value: [] });
|
|
199
|
+
}
|
|
200
|
+
return createMockResponse({
|
|
201
|
+
value: [
|
|
202
|
+
{ '@odata.type': '#ibm.tm1.api.v1.CubeApplication', Name: 'MyCube' }
|
|
203
|
+
]
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const results = await applicationService.discover('', false, false);
|
|
208
|
+
|
|
209
|
+
const cubeItem = results.find(r => r.name === 'MyCube');
|
|
210
|
+
expect(cubeItem).toBeDefined();
|
|
211
|
+
expect(cubeItem?.type).toBe('Cube');
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// ===== getNames with private path resolution (_resolvePath) =====
|
|
216
|
+
|
|
217
|
+
describe('getNames — private path', () => {
|
|
218
|
+
test('should use PrivateContents leaf collection even when parent path is public', async () => {
|
|
219
|
+
const calledUrls: string[] = [];
|
|
220
|
+
mockRestService.get.mockImplementation(async (url: string) => {
|
|
221
|
+
calledUrls.push(url);
|
|
222
|
+
if (url.includes('?$top=0')) {
|
|
223
|
+
// Public path probe succeeds — folder is public
|
|
224
|
+
return createMockResponse({});
|
|
225
|
+
}
|
|
226
|
+
if (url.includes('/PrivateContents')) {
|
|
227
|
+
return createMockResponse({
|
|
228
|
+
value: [{ Name: 'PrivateApp' }]
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
return createMockResponse({
|
|
232
|
+
value: [{ Name: 'PublicApp' }]
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const names = await applicationService.getNames('MyFolder', true);
|
|
237
|
+
|
|
238
|
+
expect(names).toContain('PrivateApp');
|
|
239
|
+
// The final data-fetch URL must use PrivateContents for the leaf
|
|
240
|
+
const dataUrl = calledUrls.find(u => !u.includes('$top=0') && u.includes('PrivateContents'));
|
|
241
|
+
expect(dataUrl).toBeDefined();
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// ===== exists with private path =====
|
|
246
|
+
|
|
247
|
+
describe('exists — private path', () => {
|
|
248
|
+
test('should use _resolvePath for private paths', async () => {
|
|
249
|
+
// Simulate all-public probe succeeds
|
|
250
|
+
mockRestService.get.mockImplementation(async (url: string) => {
|
|
251
|
+
if (url.includes('?$top=0')) {
|
|
252
|
+
return createMockResponse({});
|
|
253
|
+
}
|
|
254
|
+
// exists check — return 200
|
|
255
|
+
return createMockResponse({ Name: 'MyCubeApp' });
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const result = await applicationService.exists(
|
|
259
|
+
'MyFolder',
|
|
260
|
+
ApplicationTypes.CUBE,
|
|
261
|
+
'MyCubeApp',
|
|
262
|
+
true
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
expect(typeof result).toBe('boolean');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test('should return false when private path resolution fails with not-found', async () => {
|
|
269
|
+
mockRestService.get.mockRejectedValue(
|
|
270
|
+
new TM1RestException('Not found', 404, { status: 404 })
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
const result = await applicationService.exists(
|
|
274
|
+
'NonExistent',
|
|
275
|
+
ApplicationTypes.CUBE,
|
|
276
|
+
'SomeCube',
|
|
277
|
+
true
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
expect(result).toBe(false);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test('should rethrow server errors from private exists', async () => {
|
|
284
|
+
mockRestService.get.mockRejectedValue(
|
|
285
|
+
new TM1RestException('Internal server error', 500, { status: 500 })
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
await expect(
|
|
289
|
+
applicationService.exists('SomePath', ApplicationTypes.CUBE, 'SomeCube', true)
|
|
290
|
+
).rejects.toThrow('Internal server error');
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
});
|