@ffschrattenecker/tm1-mcp-server 7.0.1 → 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.
Files changed (116) hide show
  1. package/CHANGELOG.md +102 -1
  2. package/README.md +5 -5
  3. package/dist/config.js +34 -17
  4. package/dist/connections.d.ts +18 -1
  5. package/dist/connections.js +34 -18
  6. package/dist/http-transport.js +51 -4
  7. package/dist/lib/callgraph/referenceIndex.js +2 -1
  8. package/dist/lib/callgraph/rulesLinter.d.ts +0 -19
  9. package/dist/lib/callgraph/rulesLinter.js +0 -590
  10. package/dist/lib/callgraph/tiParser.js +10 -5
  11. package/dist/lib/callgraph/tm1-adapter.d.ts +5 -1
  12. package/dist/lib/callgraph/tm1-adapter.js +41 -9
  13. package/dist/lib/callgraph/variableEnv.js +3 -2
  14. package/dist/lib/cell-address.d.ts +11 -1
  15. package/dist/lib/cell-address.js +22 -3
  16. package/dist/lib/complexity/antipatterns.js +3 -1
  17. package/dist/lib/complexity/comment-classifier.js +2 -1
  18. package/dist/lib/coordinate-error.js +5 -1
  19. package/dist/lib/feeders/element-type-cache.js +4 -6
  20. package/dist/lib/naming/odata-filter.js +2 -1
  21. package/dist/lib/pro-parser.js +13 -4
  22. package/dist/lib/safe-regex.d.ts +0 -13
  23. package/dist/lib/safe-regex.js +40 -13
  24. package/dist/lib/sample-cells.d.ts +6 -0
  25. package/dist/lib/sample-cells.js +20 -2
  26. package/dist/lib/ti-identifier.d.ts +11 -0
  27. package/dist/lib/ti-identifier.js +11 -0
  28. package/dist/lib/tm1-name.d.ts +9 -0
  29. package/dist/lib/tm1-name.js +9 -0
  30. package/dist/lib/v12-compat/deprecated-ti.js +2 -2
  31. package/dist/session-manager.d.ts +1 -0
  32. package/dist/session-manager.js +24 -0
  33. package/dist/tm1-client/connection/profile.js +2 -4
  34. package/dist/tm1-client/dispatcher.d.ts +1 -1
  35. package/dist/tm1-client/dispatcher.js +18 -6
  36. package/dist/tm1-client/http.d.ts +21 -0
  37. package/dist/tm1-client/http.js +39 -30
  38. package/dist/tm1-client/services/cell-service.d.ts +9 -7
  39. package/dist/tm1-client/services/cell-service.js +54 -25
  40. package/dist/tm1-client/services/chore-service.js +8 -9
  41. package/dist/tm1-client/services/cube-service.d.ts +14 -18
  42. package/dist/tm1-client/services/cube-service.js +51 -45
  43. package/dist/tm1-client/services/dimension-order.d.ts +13 -0
  44. package/dist/tm1-client/services/dimension-order.js +42 -0
  45. package/dist/tm1-client/services/dimension-service.js +4 -6
  46. package/dist/tm1-client/services/element-service.d.ts +35 -11
  47. package/dist/tm1-client/services/element-service.js +124 -60
  48. package/dist/tm1-client/services/file-service.d.ts +42 -6
  49. package/dist/tm1-client/services/file-service.js +225 -28
  50. package/dist/tm1-client/services/hierarchy-service.d.ts +19 -9
  51. package/dist/tm1-client/services/hierarchy-service.js +118 -12
  52. package/dist/tm1-client/services/monitoring-service.js +2 -5
  53. package/dist/tm1-client/services/odata-page.d.ts +6 -0
  54. package/dist/tm1-client/services/odata-page.js +8 -0
  55. package/dist/tm1-client/services/process-service.d.ts +1 -1
  56. package/dist/tm1-client/services/process-service.js +26 -26
  57. package/dist/tm1-client/services/security-service.js +8 -9
  58. package/dist/tm1-client/services/server-service.js +14 -15
  59. package/dist/tm1-client/services/subset-service.d.ts +23 -10
  60. package/dist/tm1-client/services/subset-service.js +82 -26
  61. package/dist/tm1-client/services/view-service.js +12 -13
  62. package/dist/tm1-client.js +5 -2
  63. package/dist/tools/analysis/check-v12-readiness.js +3 -3
  64. package/dist/tools/celldata/check-feeders.js +6 -6
  65. package/dist/tools/celldata/check-writable-coords.js +43 -25
  66. package/dist/tools/celldata/execute-mdx.js +19 -23
  67. package/dist/tools/celldata/get-cell-value.js +1 -1
  68. package/dist/tools/celldata/get-view.js +10 -9
  69. package/dist/tools/celldata/member-ref.d.ts +15 -0
  70. package/dist/tools/celldata/member-ref.js +67 -0
  71. package/dist/tools/celldata/sample-cells.js +3 -0
  72. package/dist/tools/celldata/trace-cell-calculation.js +6 -6
  73. package/dist/tools/celldata/trace-feeders.js +6 -6
  74. package/dist/tools/celldata/write-cells.js +62 -12
  75. package/dist/tools/dimension-management/create-element-attribute.js +1 -1
  76. package/dist/tools/dimension-management/delete-hierarchy.js +3 -4
  77. package/dist/tools/dimension-management/get-element-attribute-values.js +7 -2
  78. package/dist/tools/dimension-management/list-element-attributes.js +1 -1
  79. package/dist/tools/dimension-management/update-element-attribute-value.js +8 -3
  80. package/dist/tools/dimension-management/update-element.js +14 -7
  81. package/dist/tools/fileops/container.d.ts +8 -0
  82. package/dist/tools/fileops/container.js +11 -0
  83. package/dist/tools/fileops/delete-file.js +4 -2
  84. package/dist/tools/fileops/get-file-content.js +45 -7
  85. package/dist/tools/fileops/list-files.js +5 -2
  86. package/dist/tools/fileops/search-files.js +4 -1
  87. package/dist/tools/fileops/upload-file.js +5 -2
  88. package/dist/tools/format.d.ts +1 -0
  89. package/dist/tools/format.js +7 -2
  90. package/dist/tools/index.js +0 -2
  91. package/dist/tools/model-building/check-cube-rule.js +3 -2
  92. package/dist/tools/model-building/clear-cube.js +35 -26
  93. package/dist/tools/model-building/set-cube-rules.js +13 -20
  94. package/dist/tools/model-building/unload-cube.js +1 -1
  95. package/dist/tools/operations/get-cube-stats.js +1 -1
  96. package/dist/tools/operations/get-transaction-log.js +1 -1
  97. package/dist/tools/operations/list-error-logs.js +27 -2
  98. package/dist/tools/schemas/items-fileops.d.ts +4 -0
  99. package/dist/tools/schemas/items-fileops.js +1 -0
  100. package/dist/tools/security/list-clients.js +5 -5
  101. package/dist/tools/security/list-groups.js +1 -1
  102. package/dist/tools/subsets/create-subset.js +9 -5
  103. package/dist/tools/subsets/delete-subset.js +8 -3
  104. package/dist/tools/subsets/update-subset.js +9 -9
  105. package/dist/tools/ti-development/diff-process-with-file.js +4 -29
  106. package/dist/tools/ti-development/diff-processes.js +9 -0
  107. package/dist/tools/ti-development/export-process-to-git.js +9 -7
  108. package/dist/tools/ti-development/export-process-to-pro.js +15 -8
  109. package/dist/tools/ti-development/import-pro-file.js +10 -21
  110. package/dist/tools/ti-development/import-process-from-git.js +8 -18
  111. package/dist/tools/ti-development/install-pro-bundle.js +2 -2
  112. package/dist/tools/ti-development/upsert-process.js +26 -5
  113. package/npm-shrinkwrap.json +465 -500
  114. package/package.json +14 -10
  115. package/dist/tools/dimension-management/move-element.d.ts +0 -2
  116. 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('${enc(processName)}')/tm1.ExecuteWithReturn?$expand=ErrorLogFile`;
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('${enc(processName)}')/Parameters`;
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('${enc(name)}')?$select=Name`);
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('${enc(sourceName)}')`;
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('${enc(processName)}')` +
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('${enc(processName)}')`;
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('${enc(processName)}')?$select=HasSecurityAccess`;
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('${enc(processName)}')`;
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('${enc(processName)}')/Code/$value`;
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('${enc(processName)}')`;
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('${enc(processName)}')`;
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('${enc(processName)}')`;
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('${enc(processName)}')`;
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('${enc(processName)}')` +
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('${enc(processName)}')/Variables`);
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('${enc(processName)}')`;
706
- const body = {
707
- Variables: vars.map((v) => ({
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('${enc(processName)}')`;
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('${enc(processName)}')`;
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('${enc(processName)}')/tm1.Compile`;
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
- // OData key encoder: double ' per OData literal rules, then percent-encode.
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('${enc(name)}')?$select=Name,FriendlyName,Type,Enabled&$expand=Groups`);
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('${enc(g)}')`);
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('${enc(name)}')`, body);
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('${enc(name)}')`);
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('${enc(clientName)}')`, {
79
+ await this.http.request("PATCH", `/api/v1/Users('${odataKey(clientName)}')`, {
81
80
  Name: clientName,
82
- "Groups@odata.bind": [`Groups('${enc(groupName)}')`],
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('${enc(clientName)}')/Groups?$id=Groups('${enc(groupName)}')`);
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 esc().
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 esc()+enc() pattern).
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),'${esc(opts.filter.toLowerCase())}')`);
168
+ filters.push(`contains(tolower(Message),'${escapeOdataLiteral(opts.filter.toLowerCase())}')`);
171
169
  if (opts.level)
172
- filters.push(`toupper(Level) eq '${esc(opts.level.toUpperCase())}'`);
170
+ filters.push(`toupper(Level) eq '${escapeOdataLiteral(opts.level.toUpperCase())}'`);
173
171
  if (opts.logger)
174
- filters.push(`Logger eq '${esc(opts.logger)}'`);
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 '${esc(opts.user)}'`);
250
+ filters.push(`UserName eq '${escapeOdataLiteral(opts.user)}'`);
254
251
  if (opts.objectType)
255
- filters.push(`ObjectType eq '${esc(opts.objectType)}'`);
252
+ filters.push(`ObjectType eq '${escapeOdataLiteral(opts.objectType)}'`);
256
253
  if (opts.objectName)
257
- filters.push(`ObjectName eq '${esc(opts.objectName)}'`);
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.replace(/'/g, "''")}'`);
370
+ filters.push(`Cube eq '${escapeOdataLiteral(q.cubeName)}'`);
374
371
  if (q.user)
375
- filters.push(`User eq '${q.user.replace(/'/g, "''")}'`);
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('${encKey(filename)}')/Content`;
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 subset. Either MDX-based (expression) or static
18
- * (elements). Mixed/empty inputs throw VALIDATION_ERROR.
19
- * POST /api/v1/Dimensions('{d}')/Hierarchies('{h}')/Subsets
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 public subset.
24
- * PATCH /api/v1/Dimensions('{d}')/Hierarchies('{h}')/Subsets('{s}')
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 subset.
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
- // OData key encoder: double ' per OData literal rules, then percent-encode.
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('${enc(dimensionName)}')/Hierarchies('${enc(hierarchyName)}')/${segment}?$select=Name,Expression,Alias`;
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('${enc(dimensionName)}')/Hierarchies('${enc(hierarchyName)}')/${segment}('${enc(subsetName)}')?$expand=Elements($select=Name)&$select=Name,Expression,Alias`;
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 subset. Either MDX-based (expression) or static
67
- * (elements). Mixed/empty inputs throw VALIDATION_ERROR.
68
- * POST /api/v1/Dimensions('{d}')/Hierarchies('{h}')/Subsets
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 = `/api/v1/Dimensions('${enc(dimensionName)}')/Hierarchies('${enc(hierarchyName)}')/Subsets`;
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"] = subset.elements.map((e) => `Dimensions('${enc(dimensionName)}')/Hierarchies('${enc(hierarchyName)}')/Elements('${enc(e)}')`);
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 public subset.
98
- * PATCH /api/v1/Dimensions('{d}')/Hierarchies('{h}')/Subsets('{s}')
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 path = `/api/v1/Dimensions('${enc(dimensionName)}')/Hierarchies('${enc(hierarchyName)}')/Subsets('${enc(subsetName)}')`;
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 (update.alias !== undefined)
104
- body.Alias = update.alias;
105
- if (update.expression !== undefined) {
106
- body.Expression = update.expression;
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
- else if (update.elements !== undefined) {
109
- body.Expression = "";
110
- body["Elements@odata.bind"] = update.elements.map((e) => `Dimensions('${enc(dimensionName)}')/Hierarchies('${enc(hierarchyName)}')/Elements('${enc(e)}')`);
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 subset.
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", `/api/v1/Dimensions('${enc(dimensionName)}')/Hierarchies('${enc(hierarchyName)}')/Subsets('${enc(subsetName)}')`);
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