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,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 {
|
|
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
|
|