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
|
@@ -52,6 +52,12 @@ export interface AsyncOperation {
|
|
|
52
52
|
result?: any;
|
|
53
53
|
parameters?: Record<string, any>;
|
|
54
54
|
metadata?: Record<string, any>;
|
|
55
|
+
/**
|
|
56
|
+
* When true, the operation is tracked with a client-side UUID and the
|
|
57
|
+
* TM1 server has no record of it. `getAsyncOperationStatus` returns the
|
|
58
|
+
* cached status instead of polling `/_async('{id}')`.
|
|
59
|
+
*/
|
|
60
|
+
trackedLocally?: boolean;
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
/**
|
|
@@ -113,30 +119,81 @@ export class AsyncOperationService {
|
|
|
113
119
|
return operation.status;
|
|
114
120
|
}
|
|
115
121
|
|
|
116
|
-
//
|
|
122
|
+
// Locally tracked operations hold a client-side UUID; the TM1 server
|
|
123
|
+
// would return 404 for them. Rely on the in-memory cache, which is
|
|
124
|
+
// populated by background .then()/.catch() callbacks in helpers like
|
|
125
|
+
// ProcessService.executeWithReturnAsync.
|
|
126
|
+
if (operation.trackedLocally) {
|
|
127
|
+
return operation.status;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Poll TM1 server for updated status. /_async('{id}') returns 202 while
|
|
131
|
+
// the op is pending, and 200/201 with the final operation payload once done.
|
|
132
|
+
// TM1 v12 may encode embedded failures in the `asyncresult` response header.
|
|
117
133
|
try {
|
|
118
|
-
const url = formatUrl("/
|
|
119
|
-
const response = await this.rest.get(url);
|
|
120
|
-
const serverStatus = this.
|
|
134
|
+
const url = formatUrl("/_async('{}')", operationId);
|
|
135
|
+
const response = await this.rest.get(url, { asyncRequestsMode: false });
|
|
136
|
+
const serverStatus = this.deriveStatusFromResponse(response);
|
|
121
137
|
|
|
122
|
-
// Update operation status
|
|
123
138
|
operation.status = serverStatus;
|
|
124
139
|
if (this.isTerminalStatus(serverStatus)) {
|
|
125
140
|
operation.endTime = new Date();
|
|
126
|
-
if (serverStatus === OperationStatus.COMPLETED
|
|
127
|
-
operation.result = response.data
|
|
128
|
-
} else if (serverStatus === OperationStatus.FAILED
|
|
129
|
-
operation.error = response
|
|
141
|
+
if (serverStatus === OperationStatus.COMPLETED) {
|
|
142
|
+
operation.result = response.data;
|
|
143
|
+
} else if (serverStatus === OperationStatus.FAILED) {
|
|
144
|
+
operation.error = this.extractErrorFromResponse(response);
|
|
130
145
|
}
|
|
131
146
|
}
|
|
132
147
|
|
|
133
148
|
return serverStatus;
|
|
134
|
-
} catch (error) {
|
|
135
|
-
//
|
|
149
|
+
} catch (error: any) {
|
|
150
|
+
// HTTP 4xx/5xx surfaces as a thrown TM1RestException — treat as a terminal FAILED
|
|
151
|
+
// so callers stop polling. Network errors (no status) leave cached status intact.
|
|
152
|
+
// Note: 404 is treated as FAILED here (operation never materialized). This differs
|
|
153
|
+
// from ProcessService.pollExecuteWithReturn which treats 404 as "not ready yet"
|
|
154
|
+
// because process async IDs can take a moment to register on the server.
|
|
155
|
+
const status = error?.status ?? error?.response?.status;
|
|
156
|
+
if (typeof status === 'number' && status >= 400) {
|
|
157
|
+
operation.status = OperationStatus.FAILED;
|
|
158
|
+
operation.endTime = new Date();
|
|
159
|
+
operation.error = error?.message ?? String(error);
|
|
160
|
+
return OperationStatus.FAILED;
|
|
161
|
+
}
|
|
136
162
|
return operation.status;
|
|
137
163
|
}
|
|
138
164
|
}
|
|
139
165
|
|
|
166
|
+
private deriveStatusFromResponse(response: any): OperationStatus {
|
|
167
|
+
if (response.status === 202) {
|
|
168
|
+
return OperationStatus.RUNNING;
|
|
169
|
+
}
|
|
170
|
+
const asyncResult = response.headers?.['asyncresult'];
|
|
171
|
+
if (typeof asyncResult === 'string') {
|
|
172
|
+
const embedded = parseInt(asyncResult.trim().split(/\s+/)[0], 10);
|
|
173
|
+
if (!Number.isNaN(embedded) && (embedded < 200 || embedded >= 300)) {
|
|
174
|
+
return OperationStatus.FAILED;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (response.status === 200 || response.status === 201) {
|
|
178
|
+
return OperationStatus.COMPLETED;
|
|
179
|
+
}
|
|
180
|
+
return OperationStatus.PENDING;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private extractErrorFromResponse(response: any): string {
|
|
184
|
+
const asyncResult = response.headers?.['asyncresult'];
|
|
185
|
+
if (typeof asyncResult === 'string') {
|
|
186
|
+
return asyncResult;
|
|
187
|
+
}
|
|
188
|
+
if (typeof response.data === 'string') {
|
|
189
|
+
return response.data;
|
|
190
|
+
}
|
|
191
|
+
if (response.data?.error?.message) {
|
|
192
|
+
return response.data.error.message;
|
|
193
|
+
}
|
|
194
|
+
return JSON.stringify(response.data);
|
|
195
|
+
}
|
|
196
|
+
|
|
140
197
|
/**
|
|
141
198
|
* List all active async operations
|
|
142
199
|
*
|
|
@@ -178,11 +235,10 @@ export class AsyncOperationService {
|
|
|
178
235
|
this.stopPolling(operationId);
|
|
179
236
|
|
|
180
237
|
try {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
await this.rest.post(url, {});
|
|
238
|
+
const url = formatUrl("/_async('{}')", operationId);
|
|
239
|
+
await this.rest.delete(url, { asyncRequestsMode: false });
|
|
184
240
|
} catch (error) {
|
|
185
|
-
|
|
241
|
+
console.warn(`Failed to cancel async operation ${operationId} on server:`, error);
|
|
186
242
|
}
|
|
187
243
|
|
|
188
244
|
// Update operation status
|
|
@@ -243,6 +299,9 @@ export class AsyncOperationService {
|
|
|
243
299
|
public async createAsyncOperation(definition: AsyncOperationDefinition): Promise<string> {
|
|
244
300
|
const operationId = this.generateOperationId();
|
|
245
301
|
|
|
302
|
+
// generateOperationId produces a client-side UUID; the server has no
|
|
303
|
+
// record of it, so polling /_async('{id}') would 404. Mark as locally
|
|
304
|
+
// tracked so getAsyncOperationStatus returns the cached status instead.
|
|
246
305
|
const operation: AsyncOperation = {
|
|
247
306
|
id: operationId,
|
|
248
307
|
type: definition.type,
|
|
@@ -250,7 +309,8 @@ export class AsyncOperationService {
|
|
|
250
309
|
status: OperationStatus.PENDING,
|
|
251
310
|
startTime: new Date(),
|
|
252
311
|
parameters: definition.parameters,
|
|
253
|
-
metadata: definition.metadata
|
|
312
|
+
metadata: definition.metadata,
|
|
313
|
+
trackedLocally: true
|
|
254
314
|
};
|
|
255
315
|
|
|
256
316
|
this.operations.set(operationId, operation);
|
|
@@ -442,19 +502,6 @@ export class AsyncOperationService {
|
|
|
442
502
|
status === OperationStatus.TIMEOUT;
|
|
443
503
|
}
|
|
444
504
|
|
|
445
|
-
private mapServerStatus(serverStatus: string): OperationStatus {
|
|
446
|
-
const statusMap: Record<string, OperationStatus> = {
|
|
447
|
-
'Pending': OperationStatus.PENDING,
|
|
448
|
-
'Running': OperationStatus.RUNNING,
|
|
449
|
-
'CompletedSuccessfully': OperationStatus.COMPLETED,
|
|
450
|
-
'CompletedWithErrors': OperationStatus.FAILED,
|
|
451
|
-
'Cancelled': OperationStatus.CANCELLED,
|
|
452
|
-
'Timeout': OperationStatus.TIMEOUT
|
|
453
|
-
};
|
|
454
|
-
|
|
455
|
-
return statusMap[serverStatus] || OperationStatus.PENDING;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
505
|
private stopPolling(operationId: string): void {
|
|
459
506
|
const intervalId = this.pollingIntervals.get(operationId);
|
|
460
507
|
if (intervalId) {
|
|
@@ -4,9 +4,15 @@ import { ObjectService } from './ObjectService';
|
|
|
4
4
|
import { Element, ElementType } from '../objects/Element';
|
|
5
5
|
import { ElementAttribute } from '../objects/ElementAttribute';
|
|
6
6
|
import { Process } from '../objects/Process';
|
|
7
|
+
import { ProcessService } from './ProcessService';
|
|
8
|
+
import { HierarchyService } from './HierarchyService';
|
|
9
|
+
import { CellService } from './CellService';
|
|
7
10
|
import {
|
|
8
11
|
formatUrl,
|
|
9
|
-
|
|
12
|
+
escapeODataValue,
|
|
13
|
+
requireDataAdmin,
|
|
14
|
+
CaseAndSpaceInsensitiveDict,
|
|
15
|
+
CaseAndSpaceInsensitiveSet
|
|
10
16
|
} from '../utils/Utils';
|
|
11
17
|
|
|
12
18
|
export interface ElementsDataFrameOptions {
|
|
@@ -408,6 +414,21 @@ export class ElementService extends ObjectService {
|
|
|
408
414
|
return await this.rest.post(url, JSON.stringify(body));
|
|
409
415
|
}
|
|
410
416
|
|
|
417
|
+
/**
|
|
418
|
+
* Add multiple element attributes in bulk
|
|
419
|
+
*/
|
|
420
|
+
public async addElementAttributes(
|
|
421
|
+
dimensionName: string,
|
|
422
|
+
hierarchyName: string,
|
|
423
|
+
elementAttributes: ElementAttribute[]
|
|
424
|
+
): Promise<AxiosResponse> {
|
|
425
|
+
const url = formatUrl(
|
|
426
|
+
"/Dimensions('{}')/Hierarchies('{}')/ElementAttributes",
|
|
427
|
+
dimensionName, hierarchyName);
|
|
428
|
+
const body = elementAttributes.map(ea => ea.bodyAsDict);
|
|
429
|
+
return await this.rest.post(url, JSON.stringify(body));
|
|
430
|
+
}
|
|
431
|
+
|
|
411
432
|
/**
|
|
412
433
|
* Delete multiple elements in bulk
|
|
413
434
|
*/
|
|
@@ -1204,4 +1225,304 @@ export class ElementService extends ObjectService {
|
|
|
1204
1225
|
|
|
1205
1226
|
return elements.map(name => `[${dimensionName}].[${hierarchy}].[${name}]`);
|
|
1206
1227
|
}
|
|
1228
|
+
|
|
1229
|
+
public async getNumberOfConsolidatedElements(
|
|
1230
|
+
dimensionName: string,
|
|
1231
|
+
hierarchyName: string
|
|
1232
|
+
): Promise<number> {
|
|
1233
|
+
return this._getElementCountWithFilter(dimensionName, hierarchyName, `Type eq ${ElementType.CONSOLIDATED}`);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
public async getNumberOfLeafElements(
|
|
1237
|
+
dimensionName: string,
|
|
1238
|
+
hierarchyName: string
|
|
1239
|
+
): Promise<number> {
|
|
1240
|
+
return this._getElementCountWithFilter(dimensionName, hierarchyName, `Type ne ${ElementType.CONSOLIDATED}`);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
public async getNumberOfNumericElements(
|
|
1244
|
+
dimensionName: string,
|
|
1245
|
+
hierarchyName: string
|
|
1246
|
+
): Promise<number> {
|
|
1247
|
+
return this._getElementCountWithFilter(dimensionName, hierarchyName, `Type eq ${ElementType.NUMERIC}`);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
public async getNumberOfStringElements(
|
|
1251
|
+
dimensionName: string,
|
|
1252
|
+
hierarchyName: string
|
|
1253
|
+
): Promise<number> {
|
|
1254
|
+
return this._getElementCountWithFilter(dimensionName, hierarchyName, `Type eq ${ElementType.STRING}`);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
/**
|
|
1258
|
+
* Get element types from all hierarchies in a single API call
|
|
1259
|
+
*/
|
|
1260
|
+
public async getElementTypesFromAllHierarchies(
|
|
1261
|
+
dimensionName: string,
|
|
1262
|
+
skipConsolidations: boolean = false
|
|
1263
|
+
): Promise<CaseAndSpaceInsensitiveDict<string>> {
|
|
1264
|
+
let url = formatUrl(
|
|
1265
|
+
"/Dimensions('{}')?$expand=Hierarchies($select=Elements;$expand=Elements($select=Name,Type",
|
|
1266
|
+
dimensionName
|
|
1267
|
+
);
|
|
1268
|
+
url += skipConsolidations ? ";$filter=Type ne 3))" : "))";
|
|
1269
|
+
|
|
1270
|
+
const response = await this.rest.get(url);
|
|
1271
|
+
const result = new CaseAndSpaceInsensitiveDict<string>();
|
|
1272
|
+
for (const hierarchy of response.data.Hierarchies) {
|
|
1273
|
+
for (const element of hierarchy.Elements) {
|
|
1274
|
+
result.set(element.Name, element.Type);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
return result;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
/**
|
|
1281
|
+
* Check if the element attributes cube exists for a dimension
|
|
1282
|
+
*/
|
|
1283
|
+
public async attributeCubeExists(dimensionName: string): Promise<boolean> {
|
|
1284
|
+
const url = formatUrl("/Cubes('{}')", Element.ELEMENT_ATTRIBUTES_PREFIX + dimensionName);
|
|
1285
|
+
return this._exists(url);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
/**
|
|
1289
|
+
* Get parent mapping for all elements in a hierarchy
|
|
1290
|
+
*/
|
|
1291
|
+
public async getParentsOfAllElements(
|
|
1292
|
+
dimensionName: string,
|
|
1293
|
+
hierarchyName: string
|
|
1294
|
+
): Promise<{ [elementName: string]: string[] }> {
|
|
1295
|
+
const url = formatUrl(
|
|
1296
|
+
"/Dimensions('{}')/Hierarchies('{}')/Elements?$select=Name&$expand=Parents($select=Name)",
|
|
1297
|
+
dimensionName, hierarchyName
|
|
1298
|
+
);
|
|
1299
|
+
const response = await this.rest.get(url);
|
|
1300
|
+
const result: { [elementName: string]: string[] } = {};
|
|
1301
|
+
for (const child of response.data.value) {
|
|
1302
|
+
result[child.Name] = (child.Parents || []).map((p: any) => p.Name);
|
|
1303
|
+
}
|
|
1304
|
+
return result;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
/**
|
|
1308
|
+
* Get the canonical/principal name of an element (resolves aliases)
|
|
1309
|
+
*/
|
|
1310
|
+
public async getElementPrincipalName(
|
|
1311
|
+
dimensionName: string,
|
|
1312
|
+
hierarchyName: string,
|
|
1313
|
+
elementName: string
|
|
1314
|
+
): Promise<string> {
|
|
1315
|
+
const element = await this.get(dimensionName, hierarchyName, elementName);
|
|
1316
|
+
return element.name;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
/**
|
|
1320
|
+
* Check if one element is a direct parent of another
|
|
1321
|
+
*
|
|
1322
|
+
* Unlike the related function in TM1 (ELISPAR or ElementIsParent), this function will return false
|
|
1323
|
+
* if an invalid element is passed. An invalid dimension or hierarchy will cause the underlying
|
|
1324
|
+
* REST call to throw (the error propagates from the TM1 server).
|
|
1325
|
+
*/
|
|
1326
|
+
public async elementIsParent(
|
|
1327
|
+
dimensionName: string,
|
|
1328
|
+
hierarchyName: string,
|
|
1329
|
+
parentName: string,
|
|
1330
|
+
elementName: string
|
|
1331
|
+
): Promise<boolean> {
|
|
1332
|
+
const mdx = this._buildDrillIntersectionMdx(
|
|
1333
|
+
dimensionName, hierarchyName,
|
|
1334
|
+
parentName, elementName,
|
|
1335
|
+
'TM1DrillDownMember', false
|
|
1336
|
+
);
|
|
1337
|
+
const cardinality = await this._getMdxSetCardinality(mdx);
|
|
1338
|
+
return cardinality > 0;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
/**
|
|
1342
|
+
* Check if one element is an ancestor of another
|
|
1343
|
+
*
|
|
1344
|
+
* Unlike the related function in TM1 (ELISANC or ElementIsAncestor), this function will return false
|
|
1345
|
+
* if an invalid element is passed; but will raise an exception if an invalid dimension or hierarchy is passed.
|
|
1346
|
+
*
|
|
1347
|
+
* For method you can pass three values:
|
|
1348
|
+
* - 'TI' performs best, but requires admin permissions
|
|
1349
|
+
* - 'TM1DrillDownMember' performs well when element is a leaf
|
|
1350
|
+
* - 'Descendants' performs well when ancestorName and elementName are Consolidations
|
|
1351
|
+
*
|
|
1352
|
+
* If no method is passed, defaults to 'TI' for admin users, 'TM1DrillDownMember' otherwise.
|
|
1353
|
+
* Note: isAdmin is determined from RestService state; if not set, defaults to 'TM1DrillDownMember'.
|
|
1354
|
+
*/
|
|
1355
|
+
public async elementIsAncestor(
|
|
1356
|
+
dimensionName: string,
|
|
1357
|
+
hierarchyName: string,
|
|
1358
|
+
ancestorName: string,
|
|
1359
|
+
elementName: string,
|
|
1360
|
+
method?: string
|
|
1361
|
+
): Promise<boolean> {
|
|
1362
|
+
if (!method) {
|
|
1363
|
+
method = this.isAdmin ? 'TI' : 'TM1DrillDownMember';
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
if (method.toUpperCase() === 'TI') {
|
|
1367
|
+
if (await this._elementIsAncestorTi(dimensionName, hierarchyName, elementName, ancestorName)) {
|
|
1368
|
+
return true;
|
|
1369
|
+
}
|
|
1370
|
+
if (await this.hierarchyExists(dimensionName, hierarchyName)) {
|
|
1371
|
+
return false;
|
|
1372
|
+
}
|
|
1373
|
+
throw new Error(`Hierarchy: '${hierarchyName}' does not exist in dimension: '${dimensionName}'`);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
if (method.toUpperCase() === 'DESCENDANTS' || method.toUpperCase() === 'TM1DRILLDOWNMEMBER') {
|
|
1377
|
+
if (!await this.exists(dimensionName, hierarchyName, elementName)) {
|
|
1378
|
+
if (!await this.hierarchyExists(dimensionName, hierarchyName)) {
|
|
1379
|
+
throw new Error(`Hierarchy '${hierarchyName}' does not exist in dimension '${dimensionName}'`);
|
|
1380
|
+
}
|
|
1381
|
+
return false;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
const mdx = this._buildDrillIntersectionMdx(
|
|
1386
|
+
dimensionName, hierarchyName,
|
|
1387
|
+
ancestorName, elementName,
|
|
1388
|
+
method, true
|
|
1389
|
+
);
|
|
1390
|
+
const cardinality = await this._getMdxSetCardinality(mdx);
|
|
1391
|
+
return cardinality > 0;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
/**
|
|
1395
|
+
* Remove a single edge from a hierarchy
|
|
1396
|
+
*/
|
|
1397
|
+
public async removeEdge(
|
|
1398
|
+
dimensionName: string,
|
|
1399
|
+
hierarchyName: string,
|
|
1400
|
+
parentName: string,
|
|
1401
|
+
componentName: string
|
|
1402
|
+
): Promise<AxiosResponse> {
|
|
1403
|
+
const url = formatUrl(
|
|
1404
|
+
"/Dimensions('{}')/Hierarchies('{}')/Elements('{}')/Edges(ParentName='{}',ComponentName='{}')",
|
|
1405
|
+
dimensionName, hierarchyName, parentName, parentName, componentName
|
|
1406
|
+
);
|
|
1407
|
+
return this.rest.delete(url);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
/**
|
|
1411
|
+
* Check if a hierarchy exists in a dimension (convenience delegation to HierarchyService)
|
|
1412
|
+
*/
|
|
1413
|
+
public async hierarchyExists(
|
|
1414
|
+
dimensionName: string,
|
|
1415
|
+
hierarchyName: string
|
|
1416
|
+
): Promise<boolean> {
|
|
1417
|
+
const hierarchyService = new HierarchyService(this.rest);
|
|
1418
|
+
return hierarchyService.exists(dimensionName, hierarchyName);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
/**
|
|
1422
|
+
* Get all element names and alias values for leaf elements as a case-and-space-insensitive set
|
|
1423
|
+
*/
|
|
1424
|
+
public async getAllLeafElementIdentifiers(
|
|
1425
|
+
dimensionName: string,
|
|
1426
|
+
hierarchyName: string
|
|
1427
|
+
): Promise<CaseAndSpaceInsensitiveSet> {
|
|
1428
|
+
const mdxElements = `{ Tm1FilterByLevel ( { Tm1SubsetAll ([${dimensionName}].[${hierarchyName}]) } , 0 ) }`;
|
|
1429
|
+
|
|
1430
|
+
const aliasAttributes = await this.getAliasElementAttributes(dimensionName, hierarchyName);
|
|
1431
|
+
|
|
1432
|
+
if (aliasAttributes.length === 0) {
|
|
1433
|
+
const result = await this.executeSetMdx(mdxElements, undefined, ['Name'], null, null);
|
|
1434
|
+
const identifiers = new CaseAndSpaceInsensitiveSet();
|
|
1435
|
+
for (const record of result) {
|
|
1436
|
+
identifiers.add(record[0].Name);
|
|
1437
|
+
}
|
|
1438
|
+
return identifiers;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
const attrMdx = aliasAttributes.map(a =>
|
|
1442
|
+
`[}ElementAttributes_${dimensionName}].[}ElementAttributes_${dimensionName}].[${a}]`
|
|
1443
|
+
).join(',');
|
|
1444
|
+
const mdx = `SELECT ${mdxElements} ON ROWS, {${attrMdx}} ON COLUMNS FROM [}ElementAttributes_${dimensionName}]`;
|
|
1445
|
+
|
|
1446
|
+
return this._retrieveMdxRowsAndCellValuesAsStringSet(mdx);
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
private async _getElementCountWithFilter(
|
|
1450
|
+
dimensionName: string,
|
|
1451
|
+
hierarchyName: string,
|
|
1452
|
+
filter: string
|
|
1453
|
+
): Promise<number> {
|
|
1454
|
+
const baseUrl = formatUrl(
|
|
1455
|
+
"/Dimensions('{}')/Hierarchies('{}')/Elements/$count",
|
|
1456
|
+
dimensionName, hierarchyName
|
|
1457
|
+
);
|
|
1458
|
+
const url = `${baseUrl}?$filter=${filter}`;
|
|
1459
|
+
const response = await this.rest.get(url);
|
|
1460
|
+
return parseInt(response.data) || 0;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
private _buildDrillIntersectionMdx(
|
|
1464
|
+
dimensionName: string,
|
|
1465
|
+
hierarchyName: string,
|
|
1466
|
+
firstElementName: string,
|
|
1467
|
+
secondElementName: string,
|
|
1468
|
+
mdxMethod: string,
|
|
1469
|
+
recursive: boolean
|
|
1470
|
+
): string {
|
|
1471
|
+
const first = `[${dimensionName}].[${hierarchyName}].[${firstElementName}]`;
|
|
1472
|
+
const second = `[${dimensionName}].[${hierarchyName}].[${secondElementName}]`;
|
|
1473
|
+
|
|
1474
|
+
let drillSet: string;
|
|
1475
|
+
if (mdxMethod.toUpperCase() === 'TM1DRILLDOWNMEMBER') {
|
|
1476
|
+
drillSet = recursive
|
|
1477
|
+
? `{TM1DRILLDOWNMEMBER({${first}}, ALL, RECURSIVE)}`
|
|
1478
|
+
: `{TM1DRILLDOWNMEMBER({${first}}, ALL)}`;
|
|
1479
|
+
} else if (mdxMethod.toUpperCase() === 'DESCENDANTS') {
|
|
1480
|
+
drillSet = `{DESCENDANTS(${first}, ${second}.Level, SELF)}`;
|
|
1481
|
+
} else {
|
|
1482
|
+
throw new Error("Invalid MDX Drill Method. Options: 'TM1DrillDownMember' or 'Descendants'");
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
return `INTERSECT(${drillSet}, {${second}})`;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
private async _getMdxSetCardinality(mdx: string): Promise<number> {
|
|
1489
|
+
const url = '/ExecuteMDXSetExpression?$select=Cardinality';
|
|
1490
|
+
const payload = { MDX: mdx };
|
|
1491
|
+
const response = await this.rest.post(url, JSON.stringify(payload));
|
|
1492
|
+
return response.data.Cardinality || 0;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
@requireDataAdmin
|
|
1496
|
+
private async _elementIsAncestorTi(
|
|
1497
|
+
dimensionName: string,
|
|
1498
|
+
hierarchyName: string,
|
|
1499
|
+
elementName: string,
|
|
1500
|
+
ancestorName: string
|
|
1501
|
+
): Promise<boolean> {
|
|
1502
|
+
const processService = new ProcessService(this.rest);
|
|
1503
|
+
const code = `ElementIsAncestor('${escapeODataValue(dimensionName)}', '${escapeODataValue(hierarchyName)}', '${escapeODataValue(ancestorName)}', '${escapeODataValue(elementName)}')=1`;
|
|
1504
|
+
return processService.evaluateBooleanTiExpression(code);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
private async _retrieveMdxRowsAndCellValuesAsStringSet(mdx: string): Promise<CaseAndSpaceInsensitiveSet> {
|
|
1508
|
+
const cellService = new CellService(this.rest);
|
|
1509
|
+
const { rows, values } = await cellService.executeMdxRowsAndValues(mdx);
|
|
1510
|
+
const result = new CaseAndSpaceInsensitiveSet();
|
|
1511
|
+
|
|
1512
|
+
for (const row of rows) {
|
|
1513
|
+
for (const name of row) {
|
|
1514
|
+
if (name) {
|
|
1515
|
+
result.add(name);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
for (const value of values) {
|
|
1521
|
+
if (value && typeof value === 'string' && value.trim() !== '') {
|
|
1522
|
+
result.add(value);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
return result;
|
|
1527
|
+
}
|
|
1207
1528
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
-
import {
|
|
3
|
-
import { RestService } from './RestService';
|
|
2
|
+
import { AxiosResponse } from 'axios';
|
|
3
|
+
import { RequestOptions, RestService } from './RestService';
|
|
4
4
|
import { ObjectService } from './ObjectService';
|
|
5
5
|
import { verifyVersion } from '../utils/Utils';
|
|
6
6
|
|
|
@@ -227,7 +227,7 @@ export class FileService extends ObjectService {
|
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
private async uploadFileContentWithoutMpu(url: string, content: Buffer): Promise<AxiosResponse> {
|
|
230
|
-
const config:
|
|
230
|
+
const config: RequestOptions = {
|
|
231
231
|
headers: this.binaryHttpHeader
|
|
232
232
|
};
|
|
233
233
|
return await this.rest.put(url, content, config);
|