tm1npm 1.5.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/lib/index.d.ts +1 -1
  3. package/lib/index.d.ts.map +1 -1
  4. package/lib/services/ApplicationService.d.ts +19 -3
  5. package/lib/services/ApplicationService.d.ts.map +1 -1
  6. package/lib/services/ApplicationService.js +232 -6
  7. package/lib/services/AsyncOperationService.d.ts +8 -1
  8. package/lib/services/AsyncOperationService.d.ts.map +1 -1
  9. package/lib/services/AsyncOperationService.js +69 -26
  10. package/lib/services/ElementService.d.ts +67 -1
  11. package/lib/services/ElementService.d.ts.map +1 -1
  12. package/lib/services/ElementService.js +214 -0
  13. package/lib/services/FileService.d.ts.map +1 -1
  14. package/lib/services/HierarchyService.d.ts +26 -0
  15. package/lib/services/HierarchyService.d.ts.map +1 -1
  16. package/lib/services/HierarchyService.js +306 -0
  17. package/lib/services/ProcessService.d.ts +40 -22
  18. package/lib/services/ProcessService.d.ts.map +1 -1
  19. package/lib/services/ProcessService.js +118 -111
  20. package/lib/services/RestService.d.ts +213 -25
  21. package/lib/services/RestService.d.ts.map +1 -1
  22. package/lib/services/RestService.js +841 -263
  23. package/lib/services/SubsetService.d.ts +2 -0
  24. package/lib/services/SubsetService.d.ts.map +1 -1
  25. package/lib/services/SubsetService.js +33 -0
  26. package/lib/services/TM1Service.d.ts +44 -1
  27. package/lib/services/TM1Service.d.ts.map +1 -1
  28. package/lib/services/TM1Service.js +96 -4
  29. package/lib/services/index.d.ts +1 -1
  30. package/lib/services/index.d.ts.map +1 -1
  31. package/lib/tests/100PercentParityCheck.test.js +23 -6
  32. package/lib/tests/applicationService.issue38.test.d.ts +5 -0
  33. package/lib/tests/applicationService.issue38.test.d.ts.map +1 -0
  34. package/lib/tests/applicationService.issue38.test.js +237 -0
  35. package/lib/tests/asyncOperationService.test.js +51 -45
  36. package/lib/tests/bugfix28.test.js +12 -4
  37. package/lib/tests/elementService.issue37.test.d.ts +5 -0
  38. package/lib/tests/elementService.issue37.test.d.ts.map +1 -0
  39. package/lib/tests/elementService.issue37.test.js +413 -0
  40. package/lib/tests/elementService.issue38.test.d.ts +5 -0
  41. package/lib/tests/elementService.issue38.test.d.ts.map +1 -0
  42. package/lib/tests/elementService.issue38.test.js +79 -0
  43. package/lib/tests/hierarchyService.issue38.test.d.ts +5 -0
  44. package/lib/tests/hierarchyService.issue38.test.d.ts.map +1 -0
  45. package/lib/tests/hierarchyService.issue38.test.js +460 -0
  46. package/lib/tests/processService.comprehensive.test.js +9 -9
  47. package/lib/tests/processService.test.js +234 -0
  48. package/lib/tests/restService.test.d.ts +0 -4
  49. package/lib/tests/restService.test.d.ts.map +1 -1
  50. package/lib/tests/restService.test.js +1558 -143
  51. package/lib/tests/subsetService.issue38.test.d.ts +5 -0
  52. package/lib/tests/subsetService.issue38.test.d.ts.map +1 -0
  53. package/lib/tests/subsetService.issue38.test.js +113 -0
  54. package/lib/tests/tm1Service.test.js +80 -8
  55. package/package.json +1 -1
  56. package/src/index.ts +1 -1
  57. package/src/services/ApplicationService.ts +282 -10
  58. package/src/services/AsyncOperationService.ts +76 -29
  59. package/src/services/ElementService.ts +322 -1
  60. package/src/services/FileService.ts +3 -3
  61. package/src/services/HierarchyService.ts +419 -1
  62. package/src/services/ProcessService.ts +185 -142
  63. package/src/services/RestService.ts +1021 -267
  64. package/src/services/SubsetService.ts +48 -0
  65. package/src/services/TM1Service.ts +127 -6
  66. package/src/services/index.ts +1 -1
  67. package/src/tests/100PercentParityCheck.test.ts +29 -8
  68. package/src/tests/applicationService.issue38.test.ts +293 -0
  69. package/src/tests/asyncOperationService.test.ts +52 -48
  70. package/src/tests/bugfix28.test.ts +12 -4
  71. package/src/tests/elementService.issue37.test.ts +571 -0
  72. package/src/tests/elementService.issue38.test.ts +103 -0
  73. package/src/tests/hierarchyService.issue38.test.ts +599 -0
  74. package/src/tests/processService.comprehensive.test.ts +10 -10
  75. package/src/tests/processService.test.ts +295 -3
  76. package/src/tests/restService.test.ts +1844 -139
  77. package/src/tests/subsetService.issue38.test.ts +182 -0
  78. package/src/tests/tm1Service.test.ts +95 -11
@@ -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 async whoami(): Promise<string> {
99
- const user = await this.security.getCurrentUser();
100
- return user.name;
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
- const response = await this._tm1Rest.get('/Configuration/ProductVersion');
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
+ }
@@ -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 (5 functions)', () => {
69
- test('All new ProcessService functions should exist', () => {
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
- console.log('✅ All 5 ProcessService functions exist');
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 22 functions are implemented', () => {
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(25); // 16 + 5 + 3 + 1
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
+ });