@ffschrattenecker/tm1-mcp-server 7.0.2 → 8.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 +83 -1
- package/README.md +5 -5
- package/dist/config.js +34 -17
- package/dist/connections.d.ts +18 -1
- package/dist/connections.js +34 -18
- package/dist/http-transport.js +51 -4
- package/dist/lib/callgraph/referenceIndex.js +2 -1
- package/dist/lib/callgraph/rulesLinter.d.ts +0 -19
- package/dist/lib/callgraph/rulesLinter.js +0 -590
- package/dist/lib/callgraph/tiParser.js +10 -5
- package/dist/lib/callgraph/tm1-adapter.d.ts +5 -1
- package/dist/lib/callgraph/tm1-adapter.js +41 -9
- package/dist/lib/callgraph/variableEnv.js +3 -2
- package/dist/lib/cell-address.d.ts +1 -1
- package/dist/lib/cell-address.js +2 -2
- package/dist/lib/complexity/antipatterns.js +3 -1
- package/dist/lib/complexity/comment-classifier.js +2 -1
- package/dist/lib/feeders/element-type-cache.js +4 -6
- package/dist/lib/naming/odata-filter.js +2 -1
- package/dist/lib/pro-parser.js +13 -4
- package/dist/lib/safe-regex.d.ts +0 -13
- package/dist/lib/safe-regex.js +40 -13
- package/dist/lib/ti-identifier.d.ts +11 -0
- package/dist/lib/ti-identifier.js +11 -0
- package/dist/lib/tm1-name.d.ts +9 -0
- package/dist/lib/tm1-name.js +9 -0
- package/dist/lib/v12-compat/deprecated-ti.js +2 -2
- package/dist/session-manager.d.ts +1 -0
- package/dist/session-manager.js +24 -0
- package/dist/tm1-client/connection/profile.js +2 -4
- package/dist/tm1-client/dispatcher.d.ts +1 -1
- package/dist/tm1-client/dispatcher.js +18 -6
- package/dist/tm1-client/http.d.ts +21 -0
- package/dist/tm1-client/http.js +39 -30
- package/dist/tm1-client/services/cell-service.d.ts +9 -7
- package/dist/tm1-client/services/cell-service.js +51 -25
- package/dist/tm1-client/services/chore-service.js +8 -9
- package/dist/tm1-client/services/cube-service.d.ts +14 -18
- package/dist/tm1-client/services/cube-service.js +51 -45
- package/dist/tm1-client/services/dimension-order.d.ts +13 -0
- package/dist/tm1-client/services/dimension-order.js +42 -0
- package/dist/tm1-client/services/dimension-service.js +4 -6
- package/dist/tm1-client/services/element-service.d.ts +35 -11
- package/dist/tm1-client/services/element-service.js +124 -60
- package/dist/tm1-client/services/file-service.d.ts +42 -6
- package/dist/tm1-client/services/file-service.js +225 -28
- package/dist/tm1-client/services/hierarchy-service.d.ts +19 -9
- package/dist/tm1-client/services/hierarchy-service.js +118 -12
- package/dist/tm1-client/services/monitoring-service.js +2 -5
- package/dist/tm1-client/services/odata-page.d.ts +6 -0
- package/dist/tm1-client/services/odata-page.js +8 -0
- package/dist/tm1-client/services/process-service.d.ts +1 -1
- package/dist/tm1-client/services/process-service.js +26 -26
- package/dist/tm1-client/services/security-service.js +8 -9
- package/dist/tm1-client/services/server-service.js +14 -15
- package/dist/tm1-client/services/subset-service.d.ts +23 -10
- package/dist/tm1-client/services/subset-service.js +82 -26
- package/dist/tm1-client/services/view-service.js +12 -13
- package/dist/tm1-client.js +5 -2
- package/dist/tools/analysis/check-v12-readiness.js +3 -3
- package/dist/tools/celldata/check-feeders.js +6 -6
- package/dist/tools/celldata/check-writable-coords.js +33 -22
- package/dist/tools/celldata/execute-mdx.js +10 -9
- package/dist/tools/celldata/get-view.js +10 -9
- package/dist/tools/celldata/member-ref.d.ts +15 -0
- package/dist/tools/celldata/member-ref.js +67 -0
- package/dist/tools/celldata/trace-cell-calculation.js +6 -6
- package/dist/tools/celldata/trace-feeders.js +6 -6
- package/dist/tools/celldata/write-cells.js +62 -12
- package/dist/tools/dimension-management/create-element-attribute.js +1 -1
- package/dist/tools/dimension-management/delete-hierarchy.js +3 -4
- package/dist/tools/dimension-management/get-element-attribute-values.js +7 -2
- package/dist/tools/dimension-management/list-element-attributes.js +1 -1
- package/dist/tools/dimension-management/update-element-attribute-value.js +8 -3
- package/dist/tools/dimension-management/update-element.js +14 -7
- package/dist/tools/fileops/container.d.ts +8 -0
- package/dist/tools/fileops/container.js +11 -0
- package/dist/tools/fileops/delete-file.js +4 -2
- package/dist/tools/fileops/get-file-content.js +45 -7
- package/dist/tools/fileops/list-files.js +5 -2
- package/dist/tools/fileops/search-files.js +4 -1
- package/dist/tools/fileops/upload-file.js +5 -2
- package/dist/tools/index.js +0 -2
- package/dist/tools/model-building/check-cube-rule.js +3 -2
- package/dist/tools/model-building/clear-cube.js +35 -26
- package/dist/tools/model-building/set-cube-rules.js +13 -20
- package/dist/tools/model-building/unload-cube.js +1 -1
- package/dist/tools/operations/get-cube-stats.js +1 -1
- package/dist/tools/operations/get-transaction-log.js +1 -1
- package/dist/tools/operations/list-error-logs.js +27 -2
- package/dist/tools/schemas/items-fileops.d.ts +4 -0
- package/dist/tools/schemas/items-fileops.js +1 -0
- package/dist/tools/security/list-clients.js +5 -5
- package/dist/tools/security/list-groups.js +1 -1
- package/dist/tools/subsets/create-subset.js +9 -5
- package/dist/tools/subsets/delete-subset.js +8 -3
- package/dist/tools/subsets/update-subset.js +9 -9
- package/dist/tools/ti-development/diff-process-with-file.js +4 -29
- package/dist/tools/ti-development/diff-processes.js +9 -0
- package/dist/tools/ti-development/export-process-to-git.js +9 -7
- package/dist/tools/ti-development/export-process-to-pro.js +15 -8
- package/dist/tools/ti-development/import-pro-file.js +10 -21
- package/dist/tools/ti-development/import-process-from-git.js +8 -18
- package/dist/tools/ti-development/install-pro-bundle.js +2 -2
- package/dist/tools/ti-development/upsert-process.js +26 -5
- package/npm-shrinkwrap.json +10 -9
- package/package.json +9 -5
- package/dist/tools/dimension-management/move-element.d.ts +0 -2
- package/dist/tools/dimension-management/move-element.js +0 -28
|
@@ -55,6 +55,14 @@ export function readNestedCount(response, navigationProperty) {
|
|
|
55
55
|
export function escapeOdataLiteral(value) {
|
|
56
56
|
return value.replace(/'/g, "''");
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Entity-key segment for a URL path: `Cubes('${odataKey(name)}')`. Doubles `'`,
|
|
60
|
+
* then percent-encodes, so `#`, `?`, `%`, `&` and `/` in a TM1 name stay inside
|
|
61
|
+
* the key. The one encoder for every service — see scripts/check-no-local-enc.mjs.
|
|
62
|
+
*/
|
|
63
|
+
export function odataKey(value) {
|
|
64
|
+
return encodeURIComponent(escapeOdataLiteral(String(value)));
|
|
65
|
+
}
|
|
58
66
|
/** Join filter predicates with `and` into a `&$filter=…` fragment. Empty string when none. */
|
|
59
67
|
export function filterClause(predicates) {
|
|
60
68
|
return predicates.length === 0 ? "" : `&$filter=${predicates.join(" and ")}`;
|
|
@@ -257,7 +257,7 @@ export declare class ProcessService {
|
|
|
257
257
|
* Required after setting an ASCII DataSource.
|
|
258
258
|
* PATCH /api/v1/Processes('{name}') with Variables array.
|
|
259
259
|
*/
|
|
260
|
-
updateVariables(processName: string, vars: ProcessVariable[], variablesUIData?: string[]): Promise<void>;
|
|
260
|
+
updateVariables(processName: string, vars: ProcessVariable[] | undefined, variablesUIData?: string[]): Promise<void>;
|
|
261
261
|
/**
|
|
262
262
|
* Update the parameters of a TI process.
|
|
263
263
|
* PATCH /api/v1/Processes('{name}') with Parameters array.
|
|
@@ -7,9 +7,7 @@ import { TM1Error, TM1ErrorCode } from "../../types.js";
|
|
|
7
7
|
import { isUnsupportedQueryShape, rethrowIfSystemic, rethrowIfSystemicOrDenied, } from "./fallback.js";
|
|
8
8
|
import { classifyExecution } from "./process-status.js";
|
|
9
9
|
import { ignoredColumnsOf } from "../../lib/variables-ui-data.js";
|
|
10
|
-
import { filterClause, nameFilterPredicates, pageClauses, readCount, } from "./odata-page.js";
|
|
11
|
-
// OData key encoder: double ' per OData literal rules, then percent-encode.
|
|
12
|
-
const enc = (s) => encodeURIComponent(String(s).replace(/'/g, "''"));
|
|
10
|
+
import { filterClause, nameFilterPredicates, pageClauses, readCount, odataKey, } from "./odata-page.js";
|
|
13
11
|
function decodeVariable(v) {
|
|
14
12
|
return {
|
|
15
13
|
name: v.Name,
|
|
@@ -120,7 +118,7 @@ export class ProcessService {
|
|
|
120
118
|
// on 11.8.02900.8 and 12.5.9 — both omit the key entirely without it, and both
|
|
121
119
|
// return {"Filename": ...} with it (v11 `TM1ProcessError_*.log`, v12
|
|
122
120
|
// `ProcessLog_*.jsonl`).
|
|
123
|
-
const path = `/api/v1/Processes('${
|
|
121
|
+
const path = `/api/v1/Processes('${odataKey(processName)}')/tm1.ExecuteWithReturn?$expand=ErrorLogFile`;
|
|
124
122
|
const body = {};
|
|
125
123
|
if (params && Object.keys(params).length > 0) {
|
|
126
124
|
body.Parameters = Object.entries(params).map(([name, value]) => ({
|
|
@@ -214,7 +212,7 @@ export class ProcessService {
|
|
|
214
212
|
* GET /api/v1/Processes('{name}')/Parameters
|
|
215
213
|
*/
|
|
216
214
|
async getParameters(processName) {
|
|
217
|
-
const path = `/api/v1/Processes('${
|
|
215
|
+
const path = `/api/v1/Processes('${odataKey(processName)}')/Parameters`;
|
|
218
216
|
// TM1 v11 returns Type as the decoded string "Numeric" / "String"
|
|
219
217
|
// (not the legacy int code 1 / 2). The old `=== 1` check silently
|
|
220
218
|
// classified every Numeric parameter as String.
|
|
@@ -241,7 +239,7 @@ export class ProcessService {
|
|
|
241
239
|
*/
|
|
242
240
|
async exists(name) {
|
|
243
241
|
try {
|
|
244
|
-
await this.http.request("GET", `/api/v1/Processes('${
|
|
242
|
+
await this.http.request("GET", `/api/v1/Processes('${odataKey(name)}')?$select=Name`);
|
|
245
243
|
return true;
|
|
246
244
|
}
|
|
247
245
|
catch (e) {
|
|
@@ -255,7 +253,7 @@ export class ProcessService {
|
|
|
255
253
|
* before re-POSTing to avoid TM1 rejecting the body.
|
|
256
254
|
*/
|
|
257
255
|
async copy(sourceName, targetName) {
|
|
258
|
-
const path = `/api/v1/Processes('${
|
|
256
|
+
const path = `/api/v1/Processes('${odataKey(sourceName)}')`;
|
|
259
257
|
const source = await this.http.request("GET", path);
|
|
260
258
|
delete source["@odata.context"];
|
|
261
259
|
delete source["@odata.etag"];
|
|
@@ -282,7 +280,7 @@ export class ProcessService {
|
|
|
282
280
|
* rather than an error, since neither is required to use the process.
|
|
283
281
|
*/
|
|
284
282
|
async getUIData(processName) {
|
|
285
|
-
const path = `/api/v1/Processes('${
|
|
283
|
+
const path = `/api/v1/Processes('${odataKey(processName)}')` +
|
|
286
284
|
`?$select=UIData,VariablesUIData`;
|
|
287
285
|
try {
|
|
288
286
|
const r = await this.http.request("GET", path);
|
|
@@ -413,7 +411,7 @@ export class ProcessService {
|
|
|
413
411
|
* GET /api/v1/Processes('{name}')
|
|
414
412
|
*/
|
|
415
413
|
async getCode(processName) {
|
|
416
|
-
const path = `/api/v1/Processes('${
|
|
414
|
+
const path = `/api/v1/Processes('${odataKey(processName)}')`;
|
|
417
415
|
const response = await this.http.request("GET", path);
|
|
418
416
|
return {
|
|
419
417
|
prolog: response.PrologProcedure,
|
|
@@ -428,7 +426,7 @@ export class ProcessService {
|
|
|
428
426
|
* GET /api/v1/Processes('{name}')?$select=HasSecurityAccess
|
|
429
427
|
*/
|
|
430
428
|
async getDeployMeta(processName) {
|
|
431
|
-
const path = `/api/v1/Processes('${
|
|
429
|
+
const path = `/api/v1/Processes('${odataKey(processName)}')?$select=HasSecurityAccess`;
|
|
432
430
|
const response = await this.http.request("GET", path);
|
|
433
431
|
return { hasSecurityAccess: response.HasSecurityAccess === true };
|
|
434
432
|
}
|
|
@@ -437,7 +435,7 @@ export class ProcessService {
|
|
|
437
435
|
* PATCH /api/v1/Processes('{name}') with only the tabs to update.
|
|
438
436
|
*/
|
|
439
437
|
async updateCode(processName, code) {
|
|
440
|
-
const path = `/api/v1/Processes('${
|
|
438
|
+
const path = `/api/v1/Processes('${odataKey(processName)}')`;
|
|
441
439
|
const body = {};
|
|
442
440
|
if (code.prolog !== undefined)
|
|
443
441
|
body.PrologProcedure = code.prolog;
|
|
@@ -455,7 +453,7 @@ export class ProcessService {
|
|
|
455
453
|
* are omitted by the server; newlines are CRLF.
|
|
456
454
|
*/
|
|
457
455
|
async getCodeBlob(processName) {
|
|
458
|
-
const path = `/api/v1/Processes('${
|
|
456
|
+
const path = `/api/v1/Processes('${odataKey(processName)}')/Code/$value`;
|
|
459
457
|
return this.http.requestRaw("GET", path);
|
|
460
458
|
}
|
|
461
459
|
/**
|
|
@@ -466,7 +464,7 @@ export class ProcessService {
|
|
|
466
464
|
* only write path.
|
|
467
465
|
*/
|
|
468
466
|
async updateCodeBlob(processName, blob) {
|
|
469
|
-
const path = `/api/v1/Processes('${
|
|
467
|
+
const path = `/api/v1/Processes('${odataKey(processName)}')`;
|
|
470
468
|
await this.http.request("PATCH", path, { Code: blob });
|
|
471
469
|
}
|
|
472
470
|
/**
|
|
@@ -474,7 +472,7 @@ export class ProcessService {
|
|
|
474
472
|
* PATCH /api/v1/Processes('{name}') { HasSecurityAccess }.
|
|
475
473
|
*/
|
|
476
474
|
async updateSecurityAccess(processName, hasSecurityAccess) {
|
|
477
|
-
const path = `/api/v1/Processes('${
|
|
475
|
+
const path = `/api/v1/Processes('${odataKey(processName)}')`;
|
|
478
476
|
await this.http.request("PATCH", path, {
|
|
479
477
|
HasSecurityAccess: hasSecurityAccess,
|
|
480
478
|
});
|
|
@@ -494,7 +492,7 @@ export class ProcessService {
|
|
|
494
492
|
* TM1 v12 returns the password in plain text.
|
|
495
493
|
*/
|
|
496
494
|
async getDataSource(processName, opts) {
|
|
497
|
-
const path = `/api/v1/Processes('${
|
|
495
|
+
const path = `/api/v1/Processes('${odataKey(processName)}')`;
|
|
498
496
|
const response = await this.http.request("GET", path);
|
|
499
497
|
const ds = response.DataSource;
|
|
500
498
|
// ds.Type is a raw string from the OData response. Surface (don't silently
|
|
@@ -588,7 +586,7 @@ export class ProcessService {
|
|
|
588
586
|
* PATCH /api/v1/Processes('{name}') with DataSource object.
|
|
589
587
|
*/
|
|
590
588
|
async updateDataSource(processName, dataSource) {
|
|
591
|
-
const path = `/api/v1/Processes('${
|
|
589
|
+
const path = `/api/v1/Processes('${odataKey(processName)}')`;
|
|
592
590
|
const dsBody = { Type: dataSource.type };
|
|
593
591
|
if (dataSource.dataSourceNameForServer !== undefined)
|
|
594
592
|
dsBody.dataSourceNameForServer = dataSource.dataSourceNameForServer;
|
|
@@ -645,7 +643,7 @@ export class ProcessService {
|
|
|
645
643
|
* GET /api/v1/Processes('{name}')?$select=Variables,VariablesUIData
|
|
646
644
|
*/
|
|
647
645
|
async getVariableLayout(processName) {
|
|
648
|
-
const path = `/api/v1/Processes('${
|
|
646
|
+
const path = `/api/v1/Processes('${odataKey(processName)}')` +
|
|
649
647
|
`?$select=Variables,VariablesUIData`;
|
|
650
648
|
try {
|
|
651
649
|
const r = await this.http.request("GET", path);
|
|
@@ -663,7 +661,7 @@ export class ProcessService {
|
|
|
663
661
|
throw e;
|
|
664
662
|
// A server that will not answer the $select still answers the plain
|
|
665
663
|
// collection — variables without the ignore information beat no answer.
|
|
666
|
-
const response = await this.http.request("GET", `/api/v1/Processes('${
|
|
664
|
+
const response = await this.http.request("GET", `/api/v1/Processes('${odataKey(processName)}')/Variables`);
|
|
667
665
|
return {
|
|
668
666
|
variables: response.value.map(decodeVariable),
|
|
669
667
|
ignoredColumns: [],
|
|
@@ -702,16 +700,18 @@ export class ProcessService {
|
|
|
702
700
|
* PATCH /api/v1/Processes('{name}') with Variables array.
|
|
703
701
|
*/
|
|
704
702
|
async updateVariables(processName, vars, variablesUIData) {
|
|
705
|
-
const path = `/api/v1/Processes('${
|
|
706
|
-
const body = {
|
|
707
|
-
|
|
703
|
+
const path = `/api/v1/Processes('${odataKey(processName)}')`;
|
|
704
|
+
const body = {};
|
|
705
|
+
// undefined = leave the variables alone and patch only the column layout.
|
|
706
|
+
if (vars !== undefined) {
|
|
707
|
+
body.Variables = vars.map((v) => ({
|
|
708
708
|
Name: v.name,
|
|
709
709
|
Type: v.type,
|
|
710
710
|
Position: v.position,
|
|
711
711
|
StartByte: v.startByte ?? 0,
|
|
712
712
|
EndByte: v.endByte ?? 0,
|
|
713
|
-
}))
|
|
714
|
-
}
|
|
713
|
+
}));
|
|
714
|
+
}
|
|
715
715
|
// Patching Variables alone leaves whatever VariablesUIData the process
|
|
716
716
|
// already has (measured on 12.5), which is right for an edit that keeps the
|
|
717
717
|
// column layout and wrong for one that changes it — hence the explicit
|
|
@@ -725,7 +725,7 @@ export class ProcessService {
|
|
|
725
725
|
* PATCH /api/v1/Processes('{name}') with Parameters array.
|
|
726
726
|
*/
|
|
727
727
|
async updateParameters(processName, params) {
|
|
728
|
-
const path = `/api/v1/Processes('${
|
|
728
|
+
const path = `/api/v1/Processes('${odataKey(processName)}')`;
|
|
729
729
|
const body = {
|
|
730
730
|
Parameters: params.map(encodeParameter),
|
|
731
731
|
};
|
|
@@ -736,7 +736,7 @@ export class ProcessService {
|
|
|
736
736
|
* DELETE /api/v1/Processes('{name}')
|
|
737
737
|
*/
|
|
738
738
|
async delete(processName) {
|
|
739
|
-
const path = `/api/v1/Processes('${
|
|
739
|
+
const path = `/api/v1/Processes('${odataKey(processName)}')`;
|
|
740
740
|
await this.http.request("DELETE", path);
|
|
741
741
|
}
|
|
742
742
|
/**
|
|
@@ -744,7 +744,7 @@ export class ProcessService {
|
|
|
744
744
|
* POST /api/v1/Processes('{name}')/tm1.Compile
|
|
745
745
|
*/
|
|
746
746
|
async compile(processName) {
|
|
747
|
-
const path = `/api/v1/Processes('${
|
|
747
|
+
const path = `/api/v1/Processes('${odataKey(processName)}')/tm1.Compile`;
|
|
748
748
|
try {
|
|
749
749
|
const response = await this.http.request("POST", path, {});
|
|
750
750
|
const errors = (response?.value ?? []).map((e) => ({
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
const enc = (s) => encodeURIComponent(String(s).replace(/'/g, "''"));
|
|
1
|
+
import { odataKey } from "./odata-page.js";
|
|
3
2
|
export class SecurityService {
|
|
4
3
|
http;
|
|
5
4
|
constructor(http) {
|
|
@@ -18,7 +17,7 @@ export class SecurityService {
|
|
|
18
17
|
* GET /api/v1/Users('{name}')
|
|
19
18
|
*/
|
|
20
19
|
async getClient(name) {
|
|
21
|
-
return this.http.request("GET", `/api/v1/Users('${
|
|
20
|
+
return this.http.request("GET", `/api/v1/Users('${odataKey(name)}')?$select=Name,FriendlyName,Type,Enabled&$expand=Groups`);
|
|
22
21
|
}
|
|
23
22
|
/**
|
|
24
23
|
* Create a new user (client). Optionally seeds groups.
|
|
@@ -33,7 +32,7 @@ export class SecurityService {
|
|
|
33
32
|
if (payload.friendlyName !== undefined)
|
|
34
33
|
body.FriendlyName = payload.friendlyName;
|
|
35
34
|
if (payload.groups !== undefined) {
|
|
36
|
-
body["Groups@odata.bind"] = payload.groups.map((g) => `Groups('${
|
|
35
|
+
body["Groups@odata.bind"] = payload.groups.map((g) => `Groups('${odataKey(g)}')`);
|
|
37
36
|
}
|
|
38
37
|
await this.http.request("POST", "/api/v1/Users", body);
|
|
39
38
|
}
|
|
@@ -49,14 +48,14 @@ export class SecurityService {
|
|
|
49
48
|
body.FriendlyName = payload.friendlyName;
|
|
50
49
|
if (payload.enabled !== undefined)
|
|
51
50
|
body.Enabled = payload.enabled;
|
|
52
|
-
await this.http.request("PATCH", `/api/v1/Users('${
|
|
51
|
+
await this.http.request("PATCH", `/api/v1/Users('${odataKey(name)}')`, body);
|
|
53
52
|
}
|
|
54
53
|
/**
|
|
55
54
|
* Delete a user (client).
|
|
56
55
|
* DELETE /api/v1/Users('{name}')
|
|
57
56
|
*/
|
|
58
57
|
async deleteClient(name) {
|
|
59
|
-
await this.http.request("DELETE", `/api/v1/Users('${
|
|
58
|
+
await this.http.request("DELETE", `/api/v1/Users('${odataKey(name)}')`);
|
|
60
59
|
}
|
|
61
60
|
/**
|
|
62
61
|
* List all security groups with member users.
|
|
@@ -77,9 +76,9 @@ export class SecurityService {
|
|
|
77
76
|
* tm1py pattern: PATCH /Users('x') with Name + Groups@odata.bind.
|
|
78
77
|
*/
|
|
79
78
|
async assignClientGroup(clientName, groupName) {
|
|
80
|
-
await this.http.request("PATCH", `/api/v1/Users('${
|
|
79
|
+
await this.http.request("PATCH", `/api/v1/Users('${odataKey(clientName)}')`, {
|
|
81
80
|
Name: clientName,
|
|
82
|
-
"Groups@odata.bind": [`Groups('${
|
|
81
|
+
"Groups@odata.bind": [`Groups('${odataKey(groupName)}')`],
|
|
83
82
|
});
|
|
84
83
|
}
|
|
85
84
|
/**
|
|
@@ -87,7 +86,7 @@ export class SecurityService {
|
|
|
87
86
|
* tm1py pattern: DELETE /Users('x')/Groups?$id=Groups('y').
|
|
88
87
|
*/
|
|
89
88
|
async removeClientGroup(clientName, groupName) {
|
|
90
|
-
await this.http.request("DELETE", `/api/v1/Users('${
|
|
89
|
+
await this.http.request("DELETE", `/api/v1/Users('${odataKey(clientName)}')/Groups?$id=Groups('${odataKey(groupName)}')`);
|
|
91
90
|
}
|
|
92
91
|
}
|
|
93
92
|
//# sourceMappingURL=security-service.js.map
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// See docs/ARCHITECTURE.md for the layering.
|
|
6
6
|
import { TM1Error, TM1ErrorCode } from "../../types.js";
|
|
7
7
|
import { rethrowIfSystemicOrDenied } from "./fallback.js";
|
|
8
|
+
import { escapeOdataLiteral, odataKey } from "./odata-page.js";
|
|
8
9
|
// TM1 references the per-run TI error file inside the free-text message, either
|
|
9
10
|
// wrapped in angle brackets (e.g. German `Fehlerdatei: <…log>`) or bare
|
|
10
11
|
// (`see TM1ProcessError_…log`). Both regexes are linear (no nested quantifiers,
|
|
@@ -85,10 +86,8 @@ export function toOdataDateTime(input) {
|
|
|
85
86
|
return t;
|
|
86
87
|
}
|
|
87
88
|
// enc stays plain — it also wraps whole $filter/$orderby clauses below, where
|
|
88
|
-
// the inner string literals are already single-quote-escaped via
|
|
89
|
+
// the inner string literals are already single-quote-escaped via escapeOdataLiteral().
|
|
89
90
|
const enc = encodeURIComponent;
|
|
90
|
-
// OData entity-key encoder: double ' per OData literal rules, then percent-encode.
|
|
91
|
-
const encKey = (s) => encodeURIComponent(String(s).replace(/'/g, "''"));
|
|
92
91
|
export class ServerService {
|
|
93
92
|
http;
|
|
94
93
|
constructor(http) {
|
|
@@ -163,15 +162,14 @@ export class ServerService {
|
|
|
163
162
|
async getMessageLog(opts = {}) {
|
|
164
163
|
const top = opts.top ?? 100;
|
|
165
164
|
// Inner-literal single-quote escaping per OData; the whole clause is then
|
|
166
|
-
// URL-encoded with enc() below (mirrors getAuditLog()'s
|
|
167
|
-
const esc = (s) => s.replace(/'/g, "''");
|
|
165
|
+
// URL-encoded with enc() below (mirrors getAuditLog()'s escapeOdataLiteral()+enc() pattern).
|
|
168
166
|
const filters = [];
|
|
169
167
|
if (opts.filter)
|
|
170
|
-
filters.push(`contains(tolower(Message),'${
|
|
168
|
+
filters.push(`contains(tolower(Message),'${escapeOdataLiteral(opts.filter.toLowerCase())}')`);
|
|
171
169
|
if (opts.level)
|
|
172
|
-
filters.push(`toupper(Level) eq '${
|
|
170
|
+
filters.push(`toupper(Level) eq '${escapeOdataLiteral(opts.level.toUpperCase())}'`);
|
|
173
171
|
if (opts.logger)
|
|
174
|
-
filters.push(`Logger eq '${
|
|
172
|
+
filters.push(`Logger eq '${escapeOdataLiteral(opts.logger)}'`);
|
|
175
173
|
if (opts.since)
|
|
176
174
|
filters.push(`TimeStamp ge ${toOdataDateTime(opts.since)}`);
|
|
177
175
|
if (opts.until)
|
|
@@ -247,14 +245,13 @@ export class ServerService {
|
|
|
247
245
|
* GET /api/v1/AuditLogEntries
|
|
248
246
|
*/
|
|
249
247
|
async getAuditLog(opts) {
|
|
250
|
-
const esc = (s) => s.replace(/'/g, "''");
|
|
251
248
|
const filters = [];
|
|
252
249
|
if (opts.user)
|
|
253
|
-
filters.push(`UserName eq '${
|
|
250
|
+
filters.push(`UserName eq '${escapeOdataLiteral(opts.user)}'`);
|
|
254
251
|
if (opts.objectType)
|
|
255
|
-
filters.push(`ObjectType eq '${
|
|
252
|
+
filters.push(`ObjectType eq '${escapeOdataLiteral(opts.objectType)}'`);
|
|
256
253
|
if (opts.objectName)
|
|
257
|
-
filters.push(`ObjectName eq '${
|
|
254
|
+
filters.push(`ObjectName eq '${escapeOdataLiteral(opts.objectName)}'`);
|
|
258
255
|
if (opts.since)
|
|
259
256
|
filters.push(`TimeStamp ge ${toOdataDateTime(opts.since)}`);
|
|
260
257
|
if (opts.until)
|
|
@@ -370,9 +367,9 @@ export class ServerService {
|
|
|
370
367
|
async queryTransactionLog(q) {
|
|
371
368
|
const filters = [];
|
|
372
369
|
if (q.cubeName)
|
|
373
|
-
filters.push(`Cube eq '${q.cubeName
|
|
370
|
+
filters.push(`Cube eq '${escapeOdataLiteral(q.cubeName)}'`);
|
|
374
371
|
if (q.user)
|
|
375
|
-
filters.push(`User eq '${q.user
|
|
372
|
+
filters.push(`User eq '${escapeOdataLiteral(q.user)}'`);
|
|
376
373
|
if (q.since)
|
|
377
374
|
filters.push(`TimeStamp ge ${toOdataDateTime(q.since)}`);
|
|
378
375
|
if (q.until)
|
|
@@ -470,12 +467,14 @@ export class ServerService {
|
|
|
470
467
|
const proc = opts.processName.toLowerCase();
|
|
471
468
|
// TM1 v11+ pattern with session hash: TM1ProcessError_<ts>_<id>_<proc>_<hash>.log
|
|
472
469
|
// TM1 pattern without hash: TM1ProcessError_<ts>_<id>_<proc>.log
|
|
470
|
+
// TM1 v12 pattern: ProcessLog_<ts>_<id>_<proc>.jsonl
|
|
473
471
|
// Legacy/manual pattern: <proc>_<ts>.log
|
|
474
472
|
entries = entries.filter((e) => {
|
|
475
473
|
const f = e.filename.toLowerCase();
|
|
476
474
|
return (f === proc ||
|
|
477
475
|
f.startsWith(`${proc}_`) ||
|
|
478
476
|
f.endsWith(`_${proc}.log`) ||
|
|
477
|
+
f.endsWith(`_${proc}.jsonl`) ||
|
|
479
478
|
f.includes(`_${proc}_`));
|
|
480
479
|
});
|
|
481
480
|
}
|
|
@@ -497,7 +496,7 @@ export class ServerService {
|
|
|
497
496
|
* GET /api/v1/ErrorLogFiles('<filename>')/Content
|
|
498
497
|
*/
|
|
499
498
|
async getErrorLogContent(filename) {
|
|
500
|
-
const path = `/api/v1/ErrorLogFiles('${
|
|
499
|
+
const path = `/api/v1/ErrorLogFiles('${odataKey(filename)}')/Content`;
|
|
501
500
|
return await this.http.requestRaw("GET", path);
|
|
502
501
|
}
|
|
503
502
|
}
|
|
@@ -3,6 +3,8 @@ import type { TM1HttpClient } from "../http.js";
|
|
|
3
3
|
export declare class SubsetService {
|
|
4
4
|
private readonly http;
|
|
5
5
|
constructor(http: TM1HttpClient);
|
|
6
|
+
private base;
|
|
7
|
+
private bind;
|
|
6
8
|
/**
|
|
7
9
|
* List public + private subsets of a hierarchy.
|
|
8
10
|
* GET /api/v1/Dimensions('{d}')/Hierarchies('{h}')/Subsets|PrivateSubsets
|
|
@@ -14,24 +16,35 @@ export declare class SubsetService {
|
|
|
14
16
|
*/
|
|
15
17
|
get(dimensionName: string, hierarchyName: string, subsetName: string, isPrivate?: boolean): Promise<Subset>;
|
|
16
18
|
/**
|
|
17
|
-
* Create a public
|
|
18
|
-
* (elements). Mixed/empty inputs throw
|
|
19
|
-
*
|
|
19
|
+
* Create a subset, public or private (owned by the signed-in user). Either
|
|
20
|
+
* MDX-based (expression) or static (elements). Mixed/empty inputs throw
|
|
21
|
+
* VALIDATION_ERROR.
|
|
22
|
+
* POST /api/v1/Dimensions('{d}')/Hierarchies('{h}')/Subsets|PrivateSubsets
|
|
20
23
|
*/
|
|
21
|
-
create(dimensionName: string, hierarchyName: string, subset: SubsetCreate): Promise<void>;
|
|
24
|
+
create(dimensionName: string, hierarchyName: string, subset: SubsetCreate, isPrivate?: boolean): Promise<void>;
|
|
22
25
|
/**
|
|
23
|
-
* Update an existing
|
|
24
|
-
*
|
|
26
|
+
* Update an existing subset: its MDX, its static element list, or its alias.
|
|
27
|
+
*
|
|
28
|
+
* Measured on 11.8 and 12.5: a PATCH that binds Elements APPENDS to the
|
|
29
|
+
* list (and freezes an MDX subset's result first), and one that also sends
|
|
30
|
+
* `Expression: ""` is refused as "both a list of Elements and an
|
|
31
|
+
* Expression". Replacing the list takes two calls: drop every element
|
|
32
|
+
* reference, then bind the new ones. That pair is not atomic, and a bind
|
|
33
|
+
* naming an unknown element fails with 404 after the list is already gone,
|
|
34
|
+
* so on failure the old definition is written back.
|
|
35
|
+
*
|
|
36
|
+
* PATCH /api/v1/Dimensions('{d}')/Hierarchies('{h}')/Subsets|PrivateSubsets('{s}')
|
|
37
|
+
* DELETE …/Subsets('{s}')/Elements/$ref
|
|
25
38
|
*/
|
|
26
39
|
update(dimensionName: string, hierarchyName: string, subsetName: string, update: {
|
|
27
40
|
expression?: string | undefined;
|
|
28
41
|
elements?: string[] | undefined;
|
|
29
42
|
alias?: string | undefined;
|
|
30
|
-
}): Promise<void>;
|
|
43
|
+
}, isPrivate?: boolean): Promise<void>;
|
|
31
44
|
/**
|
|
32
|
-
* Delete a public
|
|
33
|
-
* DELETE /api/v1/Dimensions('{d}')/Hierarchies('{h}')/Subsets('{s}')
|
|
45
|
+
* Delete a subset, public or private.
|
|
46
|
+
* DELETE /api/v1/Dimensions('{d}')/Hierarchies('{h}')/Subsets|PrivateSubsets('{s}')
|
|
34
47
|
*/
|
|
35
|
-
delete(dimensionName: string, hierarchyName: string, subsetName: string): Promise<void>;
|
|
48
|
+
delete(dimensionName: string, hierarchyName: string, subsetName: string, isPrivate?: boolean): Promise<void>;
|
|
36
49
|
}
|
|
37
50
|
//# sourceMappingURL=subset-service.d.ts.map
|
|
@@ -6,13 +6,18 @@
|
|
|
6
6
|
// See docs/ARCHITECTURE.md for the layering.
|
|
7
7
|
import { TM1Error, TM1ErrorCode } from "../../types.js";
|
|
8
8
|
import { rethrowIfSystemic } from "./fallback.js";
|
|
9
|
-
|
|
10
|
-
const enc = (s) => encodeURIComponent(String(s).replace(/'/g, "''"));
|
|
9
|
+
import { odataKey } from "./odata-page.js";
|
|
11
10
|
export class SubsetService {
|
|
12
11
|
http;
|
|
13
12
|
constructor(http) {
|
|
14
13
|
this.http = http;
|
|
15
14
|
}
|
|
15
|
+
base(dimensionName, hierarchyName) {
|
|
16
|
+
return `/api/v1/Dimensions('${odataKey(dimensionName)}')/Hierarchies('${odataKey(hierarchyName)}')`;
|
|
17
|
+
}
|
|
18
|
+
bind(dimensionName, hierarchyName, elements) {
|
|
19
|
+
return elements.map((e) => `Dimensions('${odataKey(dimensionName)}')/Hierarchies('${odataKey(hierarchyName)}')/Elements('${odataKey(e)}')`);
|
|
20
|
+
}
|
|
16
21
|
/**
|
|
17
22
|
* List public + private subsets of a hierarchy.
|
|
18
23
|
* GET /api/v1/Dimensions('{d}')/Hierarchies('{h}')/Subsets|PrivateSubsets
|
|
@@ -21,7 +26,7 @@ export class SubsetService {
|
|
|
21
26
|
const result = [];
|
|
22
27
|
const fetchScope = async (segment, isPrivate) => {
|
|
23
28
|
try {
|
|
24
|
-
const path = `/api/v1/Dimensions('${
|
|
29
|
+
const path = `/api/v1/Dimensions('${odataKey(dimensionName)}')/Hierarchies('${odataKey(hierarchyName)}')/${segment}?$select=Name,Expression,Alias`;
|
|
25
30
|
const response = await this.http.request("GET", path);
|
|
26
31
|
for (const s of response.value) {
|
|
27
32
|
result.push({
|
|
@@ -50,7 +55,7 @@ export class SubsetService {
|
|
|
50
55
|
*/
|
|
51
56
|
async get(dimensionName, hierarchyName, subsetName, isPrivate = false) {
|
|
52
57
|
const segment = isPrivate ? "PrivateSubsets" : "Subsets";
|
|
53
|
-
const path = `/api/v1/Dimensions('${
|
|
58
|
+
const path = `/api/v1/Dimensions('${odataKey(dimensionName)}')/Hierarchies('${odataKey(hierarchyName)}')/${segment}('${odataKey(subsetName)}')?$expand=Elements($select=Name)&$select=Name,Expression,Alias`;
|
|
54
59
|
const response = await this.http.request("GET", path);
|
|
55
60
|
return {
|
|
56
61
|
name: response.Name,
|
|
@@ -63,12 +68,13 @@ export class SubsetService {
|
|
|
63
68
|
};
|
|
64
69
|
}
|
|
65
70
|
/**
|
|
66
|
-
* Create a public
|
|
67
|
-
* (elements). Mixed/empty inputs throw
|
|
68
|
-
*
|
|
71
|
+
* Create a subset, public or private (owned by the signed-in user). Either
|
|
72
|
+
* MDX-based (expression) or static (elements). Mixed/empty inputs throw
|
|
73
|
+
* VALIDATION_ERROR.
|
|
74
|
+
* POST /api/v1/Dimensions('{d}')/Hierarchies('{h}')/Subsets|PrivateSubsets
|
|
69
75
|
*/
|
|
70
|
-
async create(dimensionName, hierarchyName, subset) {
|
|
71
|
-
const path =
|
|
76
|
+
async create(dimensionName, hierarchyName, subset, isPrivate = false) {
|
|
77
|
+
const path = `${this.base(dimensionName, hierarchyName)}/${isPrivate ? "PrivateSubsets" : "Subsets"}`;
|
|
72
78
|
if (subset.expression && subset.elements && subset.elements.length > 0) {
|
|
73
79
|
throw new TM1Error({
|
|
74
80
|
code: TM1ErrorCode.VALIDATION_ERROR,
|
|
@@ -89,34 +95,84 @@ export class SubsetService {
|
|
|
89
95
|
body.Expression = subset.expression;
|
|
90
96
|
}
|
|
91
97
|
else {
|
|
92
|
-
body["Elements@odata.bind"] =
|
|
98
|
+
body["Elements@odata.bind"] = this.bind(dimensionName, hierarchyName, subset.elements);
|
|
93
99
|
}
|
|
94
100
|
await this.http.request("POST", path, body);
|
|
95
101
|
}
|
|
96
102
|
/**
|
|
97
|
-
* Update an existing
|
|
98
|
-
*
|
|
103
|
+
* Update an existing subset: its MDX, its static element list, or its alias.
|
|
104
|
+
*
|
|
105
|
+
* Measured on 11.8 and 12.5: a PATCH that binds Elements APPENDS to the
|
|
106
|
+
* list (and freezes an MDX subset's result first), and one that also sends
|
|
107
|
+
* `Expression: ""` is refused as "both a list of Elements and an
|
|
108
|
+
* Expression". Replacing the list takes two calls: drop every element
|
|
109
|
+
* reference, then bind the new ones. That pair is not atomic, and a bind
|
|
110
|
+
* naming an unknown element fails with 404 after the list is already gone,
|
|
111
|
+
* so on failure the old definition is written back.
|
|
112
|
+
*
|
|
113
|
+
* PATCH /api/v1/Dimensions('{d}')/Hierarchies('{h}')/Subsets|PrivateSubsets('{s}')
|
|
114
|
+
* DELETE …/Subsets('{s}')/Elements/$ref
|
|
99
115
|
*/
|
|
100
|
-
async update(dimensionName, hierarchyName, subsetName, update) {
|
|
101
|
-
const
|
|
116
|
+
async update(dimensionName, hierarchyName, subsetName, update, isPrivate = false) {
|
|
117
|
+
const { expression, elements, alias } = update;
|
|
118
|
+
if (expression !== undefined && elements !== undefined) {
|
|
119
|
+
throw new TM1Error({
|
|
120
|
+
code: TM1ErrorCode.VALIDATION_ERROR,
|
|
121
|
+
message: "Pass either expression (MDX) or elements (static list), not both.",
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (expression === undefined &&
|
|
125
|
+
elements === undefined &&
|
|
126
|
+
alias === undefined) {
|
|
127
|
+
throw new TM1Error({
|
|
128
|
+
code: TM1ErrorCode.VALIDATION_ERROR,
|
|
129
|
+
message: "Nothing to update: pass expression, elements or alias.",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
const path = `${this.base(dimensionName, hierarchyName)}/${isPrivate ? "PrivateSubsets" : "Subsets"}('${odataKey(subsetName)}')`;
|
|
102
133
|
const body = {};
|
|
103
|
-
if (
|
|
104
|
-
body.Alias =
|
|
105
|
-
if (
|
|
106
|
-
|
|
134
|
+
if (alias !== undefined)
|
|
135
|
+
body.Alias = alias;
|
|
136
|
+
if (elements === undefined) {
|
|
137
|
+
if (expression !== undefined)
|
|
138
|
+
body.Expression = expression;
|
|
139
|
+
await this.http.request("PATCH", path, body);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const before = await this.get(dimensionName, hierarchyName, subsetName, isPrivate);
|
|
143
|
+
await this.http.request("DELETE", `${path}/Elements/$ref`);
|
|
144
|
+
if (elements.length > 0)
|
|
145
|
+
body["Elements@odata.bind"] = this.bind(dimensionName, hierarchyName, elements);
|
|
146
|
+
if (Object.keys(body).length === 0)
|
|
147
|
+
return;
|
|
148
|
+
try {
|
|
149
|
+
await this.http.request("PATCH", path, body);
|
|
107
150
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
151
|
+
catch (e) {
|
|
152
|
+
const restore = before.expression
|
|
153
|
+
? { Expression: before.expression }
|
|
154
|
+
: {
|
|
155
|
+
"Elements@odata.bind": this.bind(dimensionName, hierarchyName, before.elements),
|
|
156
|
+
};
|
|
157
|
+
try {
|
|
158
|
+
await this.http.request("PATCH", path, restore);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
throw new TM1Error({
|
|
162
|
+
code: TM1ErrorCode.TM1_ERROR,
|
|
163
|
+
message: `Subset '${subsetName}' was emptied and could not be restored after the update failed: ` +
|
|
164
|
+
(e instanceof Error ? e.message : String(e)),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
throw e;
|
|
111
168
|
}
|
|
112
|
-
await this.http.request("PATCH", path, body);
|
|
113
169
|
}
|
|
114
170
|
/**
|
|
115
|
-
* Delete a public
|
|
116
|
-
* DELETE /api/v1/Dimensions('{d}')/Hierarchies('{h}')/Subsets('{s}')
|
|
171
|
+
* Delete a subset, public or private.
|
|
172
|
+
* DELETE /api/v1/Dimensions('{d}')/Hierarchies('{h}')/Subsets|PrivateSubsets('{s}')
|
|
117
173
|
*/
|
|
118
|
-
async delete(dimensionName, hierarchyName, subsetName) {
|
|
119
|
-
await this.http.request("DELETE",
|
|
174
|
+
async delete(dimensionName, hierarchyName, subsetName, isPrivate = false) {
|
|
175
|
+
await this.http.request("DELETE", `${this.base(dimensionName, hierarchyName)}/${isPrivate ? "PrivateSubsets" : "Subsets"}('${odataKey(subsetName)}')`);
|
|
120
176
|
}
|
|
121
177
|
}
|
|
122
178
|
//# sourceMappingURL=subset-service.js.map
|