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,15 +2,36 @@ import { AxiosResponse } from 'axios';
2
2
  import { RestService } from './RestService';
3
3
  import { Hierarchy } from '../objects/Hierarchy';
4
4
  import { ElementAttribute } from '../objects/ElementAttribute';
5
+ import { Element, ElementType } from '../objects/Element';
6
+ import { Dimension } from '../objects/Dimension';
5
7
  import { TM1RestException } from '../exceptions/TM1Exception';
6
8
  import { ObjectService } from './ObjectService';
7
- import { CaseAndSpaceInsensitiveDict, caseAndSpaceInsensitiveEquals } from '../utils/Utils';
9
+ import { ElementService } from './ElementService';
10
+ import { CellService } from './CellService';
11
+ import { DimensionService } from './DimensionService';
12
+ import { DataFrame } from '../utils/DataFrame';
13
+ import {
14
+ CaseAndSpaceInsensitiveDict,
15
+ CaseAndSpaceInsensitiveSet,
16
+ caseAndSpaceInsensitiveEquals,
17
+ formatUrl,
18
+ verifyVersion
19
+ } from '../utils/Utils';
8
20
 
9
21
  export class HierarchyService extends ObjectService {
22
+ private elementService?: ElementService;
23
+
10
24
  constructor(rest: RestService) {
11
25
  super(rest);
12
26
  }
13
27
 
28
+ private getElementService(): ElementService {
29
+ if (!this.elementService) {
30
+ this.elementService = new ElementService(this.rest);
31
+ }
32
+ return this.elementService;
33
+ }
34
+
14
35
  public async create(hierarchy: Hierarchy): Promise<AxiosResponse> {
15
36
  const url = this.formatUrl("/Dimensions('{}')/Hierarchies", hierarchy.dimensionName);
16
37
  const response = await this.rest.post(url, hierarchy.body);
@@ -43,6 +64,13 @@ export class HierarchyService extends ObjectService {
43
64
  return responses;
44
65
  }
45
66
 
67
+ public async updateOrCreate(hierarchy: Hierarchy): Promise<AxiosResponse | AxiosResponse[]> {
68
+ if (await this.exists(hierarchy.dimensionName, hierarchy.name)) {
69
+ return await this.update(hierarchy);
70
+ }
71
+ return await this.create(hierarchy);
72
+ }
73
+
46
74
  public async delete(dimensionName: string, hierarchyName: string): Promise<AxiosResponse> {
47
75
  const url = this.formatUrl("/Dimensions('{}')/Hierarchies('{}')", dimensionName, hierarchyName);
48
76
  return await this.rest.delete(url);
@@ -78,6 +106,396 @@ export class HierarchyService extends ObjectService {
78
106
  return response.data.value.map((h: any) => Hierarchy.fromDict(h, dimensionName));
79
107
  }
80
108
 
109
+ public async getHierarchySummary(
110
+ dimensionName: string,
111
+ hierarchyName?: string
112
+ ): Promise<Record<string, number>> {
113
+ const hierarchy = hierarchyName || dimensionName;
114
+ const url = this.formatUrl(
115
+ "/Dimensions('{}')/Hierarchies('{}')" +
116
+ "?$expand=Edges/$count,Elements/$count,ElementAttributes/$count,Members/$count,Levels/$count" +
117
+ "&$select=Cardinality",
118
+ dimensionName, hierarchy);
119
+ const response = await this.rest.get(url);
120
+ const data = response.data;
121
+ return {
122
+ Elements: data['Elements@odata.count'] ?? 0,
123
+ Edges: data['Edges@odata.count'] ?? 0,
124
+ ElementAttributes: data['ElementAttributes@odata.count'] ?? 0,
125
+ Members: data['Members@odata.count'] ?? 0,
126
+ Levels: data['Levels@odata.count'] ?? 0
127
+ };
128
+ }
129
+
130
+ public async getDefaultMember(
131
+ dimensionName: string,
132
+ hierarchyName?: string
133
+ ): Promise<string | null> {
134
+ const hierarchy = hierarchyName || dimensionName;
135
+ const url = this.formatUrl(
136
+ "/Dimensions('{}')/Hierarchies('{}')/DefaultMember",
137
+ dimensionName, hierarchy);
138
+ try {
139
+ const response = await this.rest.get(url);
140
+ return response.data?.Name || null;
141
+ } catch (error) {
142
+ if (error instanceof TM1RestException && error.statusCode === 404) {
143
+ return null;
144
+ }
145
+ throw error;
146
+ }
147
+ }
148
+
149
+ public async updateDefaultMember(
150
+ dimensionName: string,
151
+ hierarchyName?: string,
152
+ memberName: string = ''
153
+ ): Promise<AxiosResponse | void> {
154
+ const hierarchy = hierarchyName || dimensionName;
155
+ // Default to API approach (v12+) when version is unknown
156
+ if (!this.version || verifyVersion(this.version, '12.0.0')) {
157
+ return this._updateDefaultMemberViaApi(dimensionName, hierarchy, memberName);
158
+ }
159
+ return this._updateDefaultMemberViaPropsCube(dimensionName, hierarchy, memberName);
160
+ }
161
+
162
+ private async _updateDefaultMemberViaApi(
163
+ dimensionName: string,
164
+ hierarchyName: string,
165
+ memberName: string
166
+ ): Promise<AxiosResponse> {
167
+ const url = this.formatUrl(
168
+ "/Dimensions('{}')/Hierarchies('{}')",
169
+ dimensionName, hierarchyName);
170
+ const body: Record<string, any> = {};
171
+ if (memberName) {
172
+ body['DefaultMember@odata.bind'] = formatUrl(
173
+ "Dimensions('{}')/Hierarchies('{}')/Elements('{}')",
174
+ dimensionName, hierarchyName, memberName);
175
+ } else {
176
+ body['DefaultMember@odata.bind'] = null;
177
+ }
178
+ return await this.rest.patch(url, JSON.stringify(body));
179
+ }
180
+
181
+ private async _updateDefaultMemberViaPropsCube(
182
+ dimensionName: string,
183
+ hierarchyName: string,
184
+ memberName: string
185
+ ): Promise<void> {
186
+ const cellService = new CellService(this.rest);
187
+ const value = memberName || '';
188
+ await cellService.writeValue(
189
+ '}HierarchyProperties',
190
+ [dimensionName, hierarchyName, 'MEMBER_DEFAULT'],
191
+ value
192
+ );
193
+ }
194
+
195
+ public async removeEdgesUnderConsolidation(
196
+ dimensionName: string,
197
+ hierarchyName: string,
198
+ consolidationElement: string
199
+ ): Promise<AxiosResponse[]> {
200
+ const hierarchy = await this.get(dimensionName, hierarchyName);
201
+
202
+ const descendants = hierarchy.getDescendants(consolidationElement, true);
203
+ const membersUnderConsolidation = new CaseAndSpaceInsensitiveSet(descendants);
204
+ membersUnderConsolidation.add(consolidationElement);
205
+
206
+ const edgesToRemove: Array<[string, string]> = [];
207
+ for (const [parent, children] of hierarchy.edges) {
208
+ if (membersUnderConsolidation.has(parent)) {
209
+ for (const child of children.keys()) {
210
+ edgesToRemove.push([parent, child]);
211
+ }
212
+ }
213
+ }
214
+
215
+ for (const [parent, child] of edgesToRemove) {
216
+ hierarchy.removeEdge(parent, child);
217
+ }
218
+
219
+ return await this.update(hierarchy);
220
+ }
221
+
222
+ public async addEdges(
223
+ dimensionName: string,
224
+ hierarchyName: string | undefined,
225
+ edges: { [parent: string]: { [child: string]: number } }
226
+ ): Promise<AxiosResponse> {
227
+ return this.getElementService().addEdges(dimensionName, hierarchyName || dimensionName, edges);
228
+ }
229
+
230
+ public async addElements(
231
+ dimensionName: string,
232
+ hierarchyName: string,
233
+ elements: Element[]
234
+ ): Promise<AxiosResponse> {
235
+ return this.getElementService().addElements(dimensionName, hierarchyName, elements);
236
+ }
237
+
238
+ public async addElementAttributes(
239
+ dimensionName: string,
240
+ hierarchyName: string,
241
+ elementAttributes: ElementAttribute[]
242
+ ): Promise<AxiosResponse> {
243
+ return this.getElementService().addElementAttributes(dimensionName, hierarchyName, elementAttributes);
244
+ }
245
+
246
+ public async updateOrCreateHierarchyFromDataframe(
247
+ dimensionName: string,
248
+ hierarchyName: string,
249
+ df: DataFrame,
250
+ options: {
251
+ elementColumn?: string;
252
+ verifyUniqueElements?: boolean;
253
+ elementTypeColumn?: string;
254
+ unwindAll?: boolean;
255
+ unwindConsolidations?: string[];
256
+ deleteOrphanedConsolidations?: boolean;
257
+ } = {}
258
+ ): Promise<void> {
259
+ const {
260
+ elementColumn: elemCol,
261
+ verifyUniqueElements = false,
262
+ elementTypeColumn = 'ElementType',
263
+ unwindAll = false,
264
+ unwindConsolidations,
265
+ deleteOrphanedConsolidations = false
266
+ } = options;
267
+
268
+ const elementColumn = elemCol || df.columns[0];
269
+ const rows = df.toJson();
270
+
271
+ const numericSuffix = (col: string) => parseInt(col.replace(/\D/g, ''), 10);
272
+ const levelColumns = df.columns
273
+ .filter(c => /^Level\d+$/i.test(c))
274
+ .sort((a, b) => numericSuffix(a) - numericSuffix(b));
275
+ const weightColumns = df.columns
276
+ .filter(c => /^Weight\d+$/i.test(c))
277
+ .sort((a, b) => numericSuffix(a) - numericSuffix(b));
278
+
279
+ const reservedColumns = new Set([
280
+ elementColumn.toLowerCase(),
281
+ elementTypeColumn.toLowerCase(),
282
+ ...levelColumns.map(c => c.toLowerCase()),
283
+ ...weightColumns.map(c => c.toLowerCase())
284
+ ]);
285
+ const attributeColumns = df.columns.filter(c => !reservedColumns.has(c.toLowerCase()));
286
+
287
+ if (verifyUniqueElements) {
288
+ const seen = new CaseAndSpaceInsensitiveSet();
289
+ for (const row of rows) {
290
+ const name = String(row[elementColumn]);
291
+ if (seen.has(name)) {
292
+ throw new Error(`Duplicate element found: '${name}'`);
293
+ }
294
+ seen.add(name);
295
+ }
296
+ }
297
+
298
+ // Parse DataFrame rows into elements, edges, and attribute values
299
+ const elementsToAdd: Element[] = [];
300
+ const edgesToAdd: { [parent: string]: { [child: string]: number } } = {};
301
+ const attributeValues = new Map<string, Map<string, any>>();
302
+
303
+ for (const row of rows) {
304
+ const elementName = String(row[elementColumn]);
305
+ const elementTypeStr = df.columns.includes(elementTypeColumn)
306
+ ? String(row[elementTypeColumn] || 'Numeric')
307
+ : 'Numeric';
308
+
309
+ // Element constructor handles string→ElementType conversion
310
+ elementsToAdd.push(new Element(elementName, elementTypeStr));
311
+
312
+ for (let i = 0; i < levelColumns.length; i++) {
313
+ const parentName = row[levelColumns[i]];
314
+ if (parentName && String(parentName).trim()) {
315
+ const weight = weightColumns[i] ? Number(row[weightColumns[i]] || 1) : 1;
316
+ const parentStr = String(parentName);
317
+ if (!edgesToAdd[parentStr]) {
318
+ edgesToAdd[parentStr] = {};
319
+ }
320
+ edgesToAdd[parentStr][elementName] = weight;
321
+ }
322
+ }
323
+
324
+ if (attributeColumns.length > 0) {
325
+ const attrs = new Map<string, any>();
326
+ for (const col of attributeColumns) {
327
+ if (row[col] !== undefined && row[col] !== null && row[col] !== '') {
328
+ attrs.set(col, row[col]);
329
+ }
330
+ }
331
+ if (attrs.size > 0) {
332
+ attributeValues.set(elementName, attrs);
333
+ }
334
+ }
335
+ }
336
+
337
+ // Get or create the dimension/hierarchy
338
+ const dimensionService = new DimensionService(this.rest);
339
+ const hierarchyExists = await this.exists(dimensionName, hierarchyName).catch(() => false);
340
+
341
+ let hierarchy: Hierarchy;
342
+ if (hierarchyExists) {
343
+ hierarchy = await this.get(dimensionName, hierarchyName);
344
+ } else {
345
+ const newHierarchy = new Hierarchy(hierarchyName, dimensionName);
346
+ try {
347
+ const dim = new Dimension(dimensionName, [newHierarchy]);
348
+ await dimensionService.create(dim);
349
+ } catch (e: any) {
350
+ const isAlreadyExists = (e instanceof TM1RestException && e.statusCode === 409)
351
+ || (e.message && e.message.includes('already exists'));
352
+ if (isAlreadyExists) {
353
+ // Dimension exists but hierarchy doesn't — create just the hierarchy
354
+ await this.create(newHierarchy);
355
+ } else {
356
+ throw e;
357
+ }
358
+ }
359
+ hierarchy = await this.get(dimensionName, hierarchyName);
360
+ }
361
+
362
+ // Unwind edges if requested
363
+ if (unwindAll) {
364
+ await this.removeAllEdges(dimensionName, hierarchyName);
365
+ } else if (unwindConsolidations && unwindConsolidations.length > 0) {
366
+ for (const consolidation of unwindConsolidations) {
367
+ try {
368
+ await this.removeEdgesUnderConsolidation(dimensionName, hierarchyName, consolidation);
369
+ } catch (e: any) {
370
+ if (!(e instanceof TM1RestException && e.statusCode === 404)) {
371
+ throw e;
372
+ }
373
+ }
374
+ }
375
+ }
376
+
377
+ // Re-fetch hierarchy only if edges were modified
378
+ if (unwindAll || (unwindConsolidations && unwindConsolidations.length > 0)) {
379
+ hierarchy = await this.get(dimensionName, hierarchyName);
380
+ }
381
+
382
+ // Add new elements not already in hierarchy
383
+ const existingElementNames = new CaseAndSpaceInsensitiveSet(
384
+ hierarchy.elements.map(e => e.name)
385
+ );
386
+ const newElements = elementsToAdd.filter(e => !existingElementNames.has(e.name));
387
+
388
+ // Ensure parent elements from edges exist as Consolidated type
389
+ const newElementNames = new CaseAndSpaceInsensitiveSet(newElements.map(e => e.name));
390
+ const parentElementsToAdd: Element[] = [];
391
+ for (const parentName of Object.keys(edgesToAdd)) {
392
+ if (!existingElementNames.has(parentName) && !newElementNames.has(parentName)) {
393
+ parentElementsToAdd.push(new Element(parentName, ElementType.CONSOLIDATED));
394
+ }
395
+ }
396
+
397
+ const allNewElements = [...newElements, ...parentElementsToAdd];
398
+ if (allNewElements.length > 0) {
399
+ await this.addElements(dimensionName, hierarchyName, allNewElements);
400
+ }
401
+
402
+ // Create missing element attributes and write values
403
+ if (attributeColumns.length > 0) {
404
+ const existingAttrs = await this.getElementAttributes(dimensionName, hierarchyName);
405
+ const existingAttrNames = new CaseAndSpaceInsensitiveSet(existingAttrs.map(a => a.name));
406
+
407
+ const newAttrs: ElementAttribute[] = [];
408
+ for (const col of attributeColumns) {
409
+ if (!existingAttrNames.has(col)) {
410
+ const values = rows.map(r => r[col]).filter(v => v !== undefined && v !== null && v !== '');
411
+ const isNumeric = values.length > 0 && values.every(v => !isNaN(Number(v)));
412
+ newAttrs.push(new ElementAttribute(col, isNumeric ? 'Numeric' : 'String'));
413
+ }
414
+ }
415
+
416
+ if (newAttrs.length > 0) {
417
+ await this.addElementAttributes(dimensionName, hierarchyName, newAttrs);
418
+ }
419
+
420
+ // Write attribute values in parallel batches
421
+ if (attributeValues.size > 0) {
422
+ const cellService = new CellService(this.rest);
423
+ const cubeName = `}ElementAttributes_${dimensionName}`;
424
+ const writePromises: Promise<void>[] = [];
425
+
426
+ for (const [elementName, attrs] of attributeValues) {
427
+ for (const [attrName, value] of attrs) {
428
+ writePromises.push(
429
+ cellService.writeValue(cubeName, [elementName, attrName], value)
430
+ .catch((e: any) => {
431
+ // Expected: element may not exist in control dimension
432
+ if (!(e instanceof TM1RestException && e.statusCode === 404)) {
433
+ throw e;
434
+ }
435
+ })
436
+ );
437
+ }
438
+ }
439
+ await Promise.all(writePromises);
440
+ }
441
+ }
442
+
443
+ // Add edges
444
+ if (Object.keys(edgesToAdd).length > 0) {
445
+ try {
446
+ await this.addEdges(dimensionName, hierarchyName, edgesToAdd);
447
+ } catch (bulkError: any) {
448
+ // Only fall back on client errors (400, 422); re-throw server/network errors
449
+ if (!(bulkError instanceof TM1RestException) ||
450
+ (bulkError.statusCode !== 400 && bulkError.statusCode !== 422)) {
451
+ throw bulkError;
452
+ }
453
+ // Bulk failed due to validation; fall back to individual edge adds
454
+ const edgePromises: Promise<AxiosResponse>[] = [];
455
+ for (const [parent, children] of Object.entries(edgesToAdd)) {
456
+ for (const [child, weight] of Object.entries(children)) {
457
+ edgePromises.push(
458
+ this.addEdges(dimensionName, hierarchyName, { [parent]: { [child]: weight } })
459
+ .catch((e: any) => {
460
+ if (e instanceof TM1RestException && e.statusCode === 409) {
461
+ return null as any;
462
+ }
463
+ throw e;
464
+ })
465
+ );
466
+ }
467
+ }
468
+ await Promise.all(edgePromises);
469
+ }
470
+ }
471
+
472
+ // Delete orphaned consolidations (consolidated elements with no children)
473
+ if (deleteOrphanedConsolidations) {
474
+ const updatedHierarchy = await this.get(dimensionName, hierarchyName);
475
+ const parentNames = new CaseAndSpaceInsensitiveSet();
476
+ for (const [parent] of updatedHierarchy.edges) {
477
+ parentNames.add(parent);
478
+ }
479
+
480
+ const elemService = this.getElementService();
481
+ const deletePromises: Promise<any>[] = [];
482
+ for (const element of updatedHierarchy.elements) {
483
+ if (element.elementType === ElementType.CONSOLIDATED && !parentNames.has(element.name)) {
484
+ deletePromises.push(
485
+ elemService.delete(dimensionName, hierarchyName, element.name)
486
+ .catch((e: any) => {
487
+ // Expected: element may have been deleted already (404)
488
+ if (!(e instanceof TM1RestException && e.statusCode === 404)) {
489
+ throw e;
490
+ }
491
+ })
492
+ );
493
+ }
494
+ }
495
+ await Promise.all(deletePromises);
496
+ }
497
+ }
498
+
81
499
  public async updateElementAttributes(hierarchy: Hierarchy, keepExisting: boolean = false): Promise<void> {
82
500
  const existingAttributes = await this.getElementAttributes(hierarchy.dimensionName, hierarchy.name);
83
501