@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
@@ -6,14 +6,26 @@
6
6
  //
7
7
  // The Agent is cached so connection-pooling stays effective across requests.
8
8
  import { Agent, fetch as undiciFetch } from "undici";
9
- let cachedAgent;
9
+ //
10
+ // headersTimeout/bodyTimeout are switched off on purpose. undici's defaults
11
+ // (300 s each) end any request that has not answered after five minutes with
12
+ // UND_ERR_HEADERS_TIMEOUT — measured: a 400 s budget died at 300.8 s on both
13
+ // fetch paths. A TI process runs as long as it runs and TM1 has no timeout
14
+ // for it, so the only limit is our own per-request AbortSignal (timeoutMs),
15
+ // which http.ts maps to LOCK_TIMEOUT.
16
+ const agents = new Map();
10
17
  export function getTm1Dispatcher(config) {
11
- if (config.ssl.rejectUnauthorized)
12
- return undefined;
13
- if (!cachedAgent) {
14
- cachedAgent = new Agent({ connect: { rejectUnauthorized: false } });
18
+ const verify = config.ssl.rejectUnauthorized;
19
+ let agent = agents.get(verify);
20
+ if (!agent) {
21
+ agent = new Agent({
22
+ connect: { rejectUnauthorized: verify },
23
+ headersTimeout: 0,
24
+ bodyTimeout: 0,
25
+ });
26
+ agents.set(verify, agent);
15
27
  }
16
- return cachedAgent;
28
+ return agent;
17
29
  }
18
30
  // Node's BUILT-IN fetch silently drops Set-Cookie headers when handed a
19
31
  // dispatcher from the npm undici package (cross-instance Headers handling;
@@ -2,6 +2,7 @@ import type pino from "pino";
2
2
  import { type TM1Config } from "../config.js";
3
3
  import type { SessionManager } from "../session-manager.js";
4
4
  import { TM1Error } from "../types.js";
5
+ import { type Tm1MutationEvent } from "../lib/tm1-events.js";
5
6
  export interface RequestOptions {
6
7
  timeoutMs?: number;
7
8
  signal?: AbortSignal;
@@ -13,6 +14,15 @@ export declare class TM1HttpClient {
13
14
  protected readonly sessionManager: SessionManager;
14
15
  private readonly profile;
15
16
  constructor(config: TM1Config, sessionManager: SessionManager, logger: pino.Logger);
17
+ private readonly mutationListeners;
18
+ /**
19
+ * @internal — subscribe to THIS connection's successful mutations. Unlike
20
+ * the process-global `tm1Events` bus, listeners die with the client, so a
21
+ * per-connection cache needs no unsubscribe and never sees another
22
+ * connection's writes.
23
+ */
24
+ onMutation(listener: (e: Tm1MutationEvent) => void): void;
25
+ private emitMutation;
16
26
  /**
17
27
  * TM1 major version (11 | 12) — the single numeric source of truth for
18
28
  * version-conditional service code. Branch on this, never on the display
@@ -45,6 +55,17 @@ export declare class TM1HttpClient {
45
55
  */
46
56
  /** @internal — for Service-layer use; not part of the public consumer API. */
47
57
  requestRaw(method: string, path: string, opts?: RequestOptions): Promise<string>;
58
+ /**
59
+ * Same request as requestRaw, but hands back the bytes instead of a string.
60
+ *
61
+ * requestRaw decodes the body as UTF-8, which silently destroys anything
62
+ * that is not text — a spreadsheet read that way comes back as replacement
63
+ * characters. Callers that may face binary content use this and decide how
64
+ * to present it.
65
+ */
66
+ /** @internal — for Service-layer use; not part of the public consumer API. */
67
+ requestRawBytes(method: string, path: string, opts?: RequestOptions): Promise<Buffer>;
68
+ private fetchRaw;
48
69
  /**
49
70
  * Make an authenticated HTTP request with a binary body (Buffer/Uint8Array).
50
71
  * Used for blob/file uploads where the body is raw bytes (not JSON).
@@ -51,6 +51,24 @@ export class TM1HttpClient {
51
51
  this.logger = logger;
52
52
  this.profile = createConnectionProfile(config);
53
53
  }
54
+ mutationListeners = [];
55
+ /**
56
+ * @internal — subscribe to THIS connection's successful mutations. Unlike
57
+ * the process-global `tm1Events` bus, listeners die with the client, so a
58
+ * per-connection cache needs no unsubscribe and never sees another
59
+ * connection's writes.
60
+ */
61
+ onMutation(listener) {
62
+ this.mutationListeners.push(listener);
63
+ }
64
+ emitMutation(method, path) {
65
+ if (isSafeHttpMethod(method))
66
+ return;
67
+ const e = { method, path, connectionId: connectionIdOf(this.config) };
68
+ tm1Events.emit("mutation", e);
69
+ for (const listener of this.mutationListeners)
70
+ listener(e);
71
+ }
54
72
  /**
55
73
  * TM1 major version (11 | 12) — the single numeric source of truth for
56
74
  * version-conditional service code. Branch on this, never on the display
@@ -110,23 +128,11 @@ export class TM1HttpClient {
110
128
  });
111
129
  }
112
130
  const retryResult = await this.handleResponse(retryResponse, path);
113
- if (!isSafeMethod) {
114
- tm1Events.emit("mutation", {
115
- method,
116
- path,
117
- connectionId: connectionIdOf(this.config),
118
- });
119
- }
131
+ this.emitMutation(method, path);
120
132
  return retryResult;
121
133
  }
122
134
  const result = await this.handleResponse(response, path);
123
- if (!isSafeMethod) {
124
- tm1Events.emit("mutation", {
125
- method,
126
- path,
127
- connectionId: connectionIdOf(this.config),
128
- });
129
- }
135
+ this.emitMutation(method, path);
130
136
  return result;
131
137
  }
132
138
  catch (error) {
@@ -174,6 +180,22 @@ export class TM1HttpClient {
174
180
  */
175
181
  /** @internal — for Service-layer use; not part of the public consumer API. */
176
182
  async requestRaw(method, path, opts) {
183
+ return (await this.fetchRaw(method, path, opts)).text();
184
+ }
185
+ /**
186
+ * Same request as requestRaw, but hands back the bytes instead of a string.
187
+ *
188
+ * requestRaw decodes the body as UTF-8, which silently destroys anything
189
+ * that is not text — a spreadsheet read that way comes back as replacement
190
+ * characters. Callers that may face binary content use this and decide how
191
+ * to present it.
192
+ */
193
+ /** @internal — for Service-layer use; not part of the public consumer API. */
194
+ async requestRawBytes(method, path, opts) {
195
+ const res = await this.fetchRaw(method, path, opts);
196
+ return Buffer.from(await res.arrayBuffer());
197
+ }
198
+ async fetchRaw(method, path, opts) {
177
199
  const url = `${this.config.baseUrl}${this.profile.resolveApiPath(path)}`;
178
200
  const effectiveTimeout = opts?.timeoutMs ?? this.config.requestTimeoutMs;
179
201
  const cookie = await this.sessionManager.ensureSession();
@@ -198,15 +220,8 @@ export class TM1HttpClient {
198
220
  }
199
221
  throw this.classifyHttpError(response.status, path, body || undefined);
200
222
  }
201
- const text = await response.text();
202
- if (!isSafeHttpMethod(method)) {
203
- tm1Events.emit("mutation", {
204
- method,
205
- path,
206
- connectionId: connectionIdOf(this.config),
207
- });
208
- }
209
- return text;
223
+ this.emitMutation(method, path);
224
+ return response;
210
225
  }
211
226
  /**
212
227
  * Make an authenticated HTTP request with a binary body (Buffer/Uint8Array).
@@ -237,13 +252,7 @@ export class TM1HttpClient {
237
252
  }
238
253
  throw this.classifyHttpError(response.status, path, errBody || undefined);
239
254
  }
240
- if (!isSafeHttpMethod(method)) {
241
- tm1Events.emit("mutation", {
242
- method,
243
- path,
244
- connectionId: connectionIdOf(this.config),
245
- });
246
- }
255
+ this.emitMutation(method, path);
247
256
  }
248
257
  /**
249
258
  * Low-level single fetch with timeout + AbortSignal wiring.
@@ -1,8 +1,10 @@
1
1
  import type { CalculationTraceNode, CellValue, FedCellDescriptor, FeederTraceResult, MdxResult } from "../../types.js";
2
2
  import type { RequestOptions, TM1HttpClient } from "../http.js";
3
+ import { DimensionOrderCache } from "./dimension-order.js";
3
4
  export declare class CellService {
4
5
  private readonly http;
5
- constructor(http: TM1HttpClient);
6
+ private readonly dimOrder;
7
+ constructor(http: TM1HttpClient, dimOrder?: DimensionOrderCache);
6
8
  /**
7
9
  * Get a single cell value via a 1-tuple MDX query.
8
10
  *
@@ -42,20 +44,20 @@ export declare class CellService {
42
44
  }>): Promise<void>;
43
45
  /**
44
46
  * Resolve the cube's dimension order and build Tuple@odata.bind paths for
45
- * the cell-bound trace actions. Elements address the default hierarchy
46
- * (same name as the dimension) — alternate hierarchies are not supported
47
- * by these diagnostics tools.
47
+ * the cell-bound trace actions. Each entry addresses the dimension's default
48
+ * hierarchy, or an alternate one when written `Hier:Elem` — see
49
+ * splitHierarchyQualified().
48
50
  */
49
51
  private tupleBinds;
50
52
  /**
51
53
  * Check the feeders of a cell: returns the cells fed by this cell with a
52
- * Fed flag per target — Fed=false marks a broken/missing feeder. v11 only.
54
+ * Fed flag per target — Fed=false marks a broken/missing feeder.
53
55
  * POST /api/v1/Cubes('{cube}')/tm1.CheckFeeders
54
56
  */
55
57
  checkFeeders(cubeName: string, elements: string[], opts?: RequestOptions): Promise<FedCellDescriptor[]>;
56
58
  /**
57
59
  * Trace the feeders of a cell: returns the cells this cell feeds plus the
58
- * feeder statements involved. v11 only.
60
+ * feeder statements involved.
59
61
  * POST /api/v1/Cubes('{cube}')/tm1.TraceFeeders
60
62
  */
61
63
  traceFeeders(cubeName: string, elements: string[], opts?: RequestOptions): Promise<FeederTraceResult>;
@@ -63,7 +65,7 @@ export declare class CellService {
63
65
  * Trace how a cell value is calculated: recursive component tree with
64
66
  * per-component type (consolidation/rule), status, value, and rule
65
67
  * statements. The server returns the full tree; maxDepth/maxComponents
66
- * truncate client-side to keep responses bounded. v11 only.
68
+ * truncate client-side to keep responses bounded.
67
69
  * POST /api/v1/Cubes('{cube}')/tm1.TraceCellCalculation
68
70
  */
69
71
  traceCellCalculation(cubeName: string, elements: string[], maxDepth?: number, maxComponents?: number, opts?: RequestOptions): Promise<CalculationTraceNode>;
@@ -1,15 +1,38 @@
1
1
  // Cell domain service. Owns single-cell reads, MDX cellset execution, and
2
2
  // cell writes via the cellset PATCH path. Cube-shape lookups (resolving
3
- // dimension order for getValue) go through the request layer directly rather
4
- // than CubeService to avoid a CubeService → CellService cycle later.
3
+ // dimension order for getValue) go through the shared DimensionOrderCache
4
+ // rather than CubeService to avoid a CubeService → CellService cycle later.
5
5
  //
6
6
  // See docs/ARCHITECTURE.md for the layering.
7
7
  import { TM1Error, TM1ErrorCode } from "../../types.js";
8
8
  import { escapeMdxName } from "../../lib/mdx.js";
9
9
  import { dimensionCountMismatch } from "../../lib/coordinate-error.js";
10
+ import { bindLeftOutSandbox } from "../../lib/cell-address.js";
11
+ import { mapSettledWithConcurrency } from "../../lib/concurrency.js";
10
12
  import { freeCellset, transformCellsetResponse } from "./cellset-transform.js";
11
- // OData key encoder: double ' per OData literal rules, then percent-encode.
12
- const enc = (s) => encodeURIComponent(String(s).replace(/'/g, "''"));
13
+ import { odataKey } from "./odata-page.js";
14
+ import { DimensionOrderCache } from "./dimension-order.js";
15
+ // In-flight cap for the per-cell re-walk after a refused bulk write.
16
+ const FALLBACK_CONCURRENCY = 8;
17
+ /**
18
+ * Split one entry of a cell coordinate into (hierarchy, element).
19
+ *
20
+ * A bare name addresses the dimension's DEFAULT hierarchy, which carries the
21
+ * dimension's own name. `Hier:Elem` addresses an alternate hierarchy — the
22
+ * same `Dim:Hier` idiom TM1 rules use, one slot down.
23
+ *
24
+ * The split takes the FIRST colon, and the dimension's default hierarchy can
25
+ * always be named explicitly, so an element whose own name contains a colon
26
+ * stays reachable: in dimension `Region`, `Region:A:B` is element `A:B` in
27
+ * the default hierarchy. A leading or trailing colon cannot separate anything,
28
+ * so such an entry is taken as a plain element name.
29
+ */
30
+ function splitHierarchyQualified(entry, dimension) {
31
+ const i = entry.indexOf(":");
32
+ if (i <= 0 || i === entry.length - 1)
33
+ return { hierarchy: dimension, element: entry };
34
+ return { hierarchy: entry.slice(0, i), element: entry.slice(i + 1) };
35
+ }
13
36
  // Build a fully-qualified MDX member reference for a write coordinate.
14
37
  // A caller may pass a pre-qualified ref to target an ALTERNATE hierarchy
15
38
  // (`[Dim].[AltHier].[Elem]`, or `[Dim].[Elem]` for the default) — passed
@@ -26,8 +49,10 @@ function qualifyWriteMember(dim, element) {
26
49
  }
27
50
  export class CellService {
28
51
  http;
29
- constructor(http) {
52
+ dimOrder;
53
+ constructor(http, dimOrder = new DimensionOrderCache(http)) {
30
54
  this.http = http;
55
+ this.dimOrder = dimOrder;
31
56
  }
32
57
  /**
33
58
  * Get a single cell value via a 1-tuple MDX query.
@@ -41,9 +66,9 @@ export class CellService {
41
66
  if (elements.length === 0) {
42
67
  return null;
43
68
  }
44
- const cubePath = `/api/v1/Cubes('${enc(cubeName)}')?$expand=Dimensions($select=Name)`;
45
- const cubeMeta = await this.http.request("GET", cubePath);
46
- const dims = cubeMeta.Dimensions.map((d) => d.Name);
69
+ const dims = await this.dimOrder.get(cubeName);
70
+ // Sandboxes left out → Base, as tm1_write_cells does.
71
+ elements = bindLeftOutSandbox(dims, elements) ?? elements;
47
72
  if (elements.length !== dims.length) {
48
73
  throw dimensionCountMismatch(cubeName, dims, elements);
49
74
  }
@@ -161,11 +186,11 @@ export class CellService {
161
186
  const cellset = await this.http.request("POST", "/api/v1/ExecuteMDX", { MDX: mdx });
162
187
  const id = cellset.ID;
163
188
  try {
164
- await this.http.request("PATCH", `/api/v1/Cellsets('${enc(id)}')/Cells(0)`, { Value: c.value });
189
+ await this.http.request("PATCH", `/api/v1/Cellsets('${odataKey(id)}')/Cells(0)`, { Value: c.value });
165
190
  }
166
191
  finally {
167
192
  try {
168
- await this.http.request("DELETE", `/api/v1/Cellsets('${enc(id)}')`);
193
+ await this.http.request("DELETE", `/api/v1/Cellsets('${odataKey(id)}')`);
169
194
  }
170
195
  catch {
171
196
  // cleanup best-effort
@@ -183,7 +208,7 @@ export class CellService {
183
208
  let bulkFailed = false;
184
209
  const cellset = await this.http.request("POST", "/api/v1/ExecuteMDX", { MDX: mdx });
185
210
  try {
186
- await this.http.request("PATCH", `/api/v1/Cellsets('${enc(cellset.ID)}')/Cells`, chunk.map((c, ordinal) => ({ Ordinal: ordinal, Value: c.value })));
211
+ await this.http.request("PATCH", `/api/v1/Cellsets('${odataKey(cellset.ID)}')/Cells`, chunk.map((c, ordinal) => ({ Ordinal: ordinal, Value: c.value })));
187
212
  written += chunk.length;
188
213
  }
189
214
  catch {
@@ -197,7 +222,7 @@ export class CellService {
197
222
  }
198
223
  finally {
199
224
  try {
200
- await this.http.request("DELETE", `/api/v1/Cellsets('${enc(cellset.ID)}')`);
225
+ await this.http.request("DELETE", `/api/v1/Cellsets('${odataKey(cellset.ID)}')`);
201
226
  }
202
227
  catch {
203
228
  // cleanup best-effort
@@ -205,7 +230,9 @@ export class CellService {
205
230
  }
206
231
  if (!bulkFailed)
207
232
  continue;
208
- const results = await Promise.allSettled(chunk.map(writeOne));
233
+ // Bounded: re-walking a 500-cell chunk unbounded put ~1500 requests
234
+ // (cellset, PATCH, delete per cell) in flight against the worker pool.
235
+ const results = await mapSettledWithConcurrency(chunk, FALLBACK_CONCURRENCY, (c) => writeOne(c));
209
236
  const failed = [];
210
237
  results.forEach((r, j) => {
211
238
  if (r.status === "fulfilled") {
@@ -235,39 +262,41 @@ export class CellService {
235
262
  }
236
263
  /**
237
264
  * Resolve the cube's dimension order and build Tuple@odata.bind paths for
238
- * the cell-bound trace actions. Elements address the default hierarchy
239
- * (same name as the dimension) — alternate hierarchies are not supported
240
- * by these diagnostics tools.
265
+ * the cell-bound trace actions. Each entry addresses the dimension's default
266
+ * hierarchy, or an alternate one when written `Hier:Elem` — see
267
+ * splitHierarchyQualified().
241
268
  */
242
269
  async tupleBinds(cubeName, elements) {
243
- const cubeMeta = await this.http.request("GET", `/api/v1/Cubes('${enc(cubeName)}')?$expand=Dimensions($select=Name)`);
244
- const dims = cubeMeta.Dimensions.map((d) => d.Name);
270
+ const dims = await this.dimOrder.get(cubeName);
245
271
  if (elements.length !== dims.length) {
246
272
  throw new TM1Error({
247
273
  code: TM1ErrorCode.VALIDATION_ERROR,
248
274
  message: `Cube '${cubeName}' has ${dims.length} dimension(s) (${dims.join(", ")}) but ${elements.length} element(s) were given`,
249
275
  });
250
276
  }
251
- return dims.map((d, i) => `Dimensions('${enc(d)}')/Hierarchies('${enc(d)}')/Elements('${enc(elements[i])}')`);
277
+ return dims.map((d, i) => {
278
+ const { hierarchy, element } = splitHierarchyQualified(elements[i], d);
279
+ return `Dimensions('${odataKey(d)}')/Hierarchies('${odataKey(hierarchy)}')/Elements('${odataKey(element)}')`;
280
+ });
252
281
  }
253
282
  /**
254
283
  * Check the feeders of a cell: returns the cells fed by this cell with a
255
- * Fed flag per target — Fed=false marks a broken/missing feeder. v11 only.
284
+ * Fed flag per target — Fed=false marks a broken/missing feeder.
256
285
  * POST /api/v1/Cubes('{cube}')/tm1.CheckFeeders
257
286
  */
258
287
  async checkFeeders(cubeName, elements, opts) {
259
288
  const binds = await this.tupleBinds(cubeName, elements);
260
- const response = await this.http.request("POST", `/api/v1/Cubes('${enc(cubeName)}')/tm1.CheckFeeders?$expand=Cube($select=Name),Tuple($select=Name)`, { "Tuple@odata.bind": binds }, opts);
289
+ const response = await this.http.request("POST", `/api/v1/Cubes('${odataKey(cubeName)}')/tm1.CheckFeeders?$expand=Cube($select=Name),Tuple($select=Name)`, { "Tuple@odata.bind": binds }, opts);
261
290
  return (response.value ?? []).map(mapFedCell);
262
291
  }
263
292
  /**
264
293
  * Trace the feeders of a cell: returns the cells this cell feeds plus the
265
- * feeder statements involved. v11 only.
294
+ * feeder statements involved.
266
295
  * POST /api/v1/Cubes('{cube}')/tm1.TraceFeeders
267
296
  */
268
297
  async traceFeeders(cubeName, elements, opts) {
269
298
  const binds = await this.tupleBinds(cubeName, elements);
270
- const response = await this.http.request("POST", `/api/v1/Cubes('${enc(cubeName)}')/tm1.TraceFeeders?$expand=FedCells/Cube($select=Name),FedCells/Tuple($select=Name)`, { "Tuple@odata.bind": binds }, opts);
299
+ const response = await this.http.request("POST", `/api/v1/Cubes('${odataKey(cubeName)}')/tm1.TraceFeeders?$expand=FedCells/Cube($select=Name),FedCells/Tuple($select=Name)`, { "Tuple@odata.bind": binds }, opts);
271
300
  return {
272
301
  fedCells: (response.FedCells ?? []).map(mapFedCell),
273
302
  statements: response.Statements ?? [],
@@ -277,7 +306,7 @@ export class CellService {
277
306
  * Trace how a cell value is calculated: recursive component tree with
278
307
  * per-component type (consolidation/rule), status, value, and rule
279
308
  * statements. The server returns the full tree; maxDepth/maxComponents
280
- * truncate client-side to keep responses bounded. v11 only.
309
+ * truncate client-side to keep responses bounded.
281
310
  * POST /api/v1/Cubes('{cube}')/tm1.TraceCellCalculation
282
311
  */
283
312
  async traceCellCalculation(cubeName, elements, maxDepth = 3, maxComponents = 20, opts) {
@@ -293,7 +322,7 @@ export class CellService {
293
322
  const prefix = "Components/".repeat(level);
294
323
  expandParts.push(`${prefix}Tuple($select=Name)`, `${prefix}Cube($select=Name)`);
295
324
  }
296
- const response = await this.http.request("POST", `/api/v1/Cubes('${enc(cubeName)}')/tm1.TraceCellCalculation?$expand=${expandParts.join(",")}`, { "Tuple@odata.bind": binds }, opts);
325
+ const response = await this.http.request("POST", `/api/v1/Cubes('${odataKey(cubeName)}')/tm1.TraceCellCalculation?$expand=${expandParts.join(",")}`, { "Tuple@odata.bind": binds }, opts);
297
326
  return mapCalcComponent(response, maxDepth, maxComponents);
298
327
  }
299
328
  }
@@ -5,8 +5,7 @@
5
5
  import { CHORE_STATUS_UNAVAILABLE, TM1Error, TM1ErrorCode, } from "../../types.js";
6
6
  import { classifyChoreExecution } from "./chore-status.js";
7
7
  import { rethrowIfSystemic } from "./fallback.js";
8
- // OData key encoder: double ' per OData literal rules, then percent-encode.
9
- const enc = (s) => encodeURIComponent(String(s).replace(/'/g, "''"));
8
+ import { odataKey } from "./odata-page.js";
10
9
  function frequencyDuration(f) {
11
10
  return `P${f.days}DT${String(f.hours).padStart(2, "0")}H${String(f.minutes).padStart(2, "0")}M${String(f.seconds).padStart(2, "0")}S`;
12
11
  }
@@ -45,7 +44,7 @@ export class ChoreService {
45
44
  * PATCH /api/v1/Chores('{name}') with { Active: bool }
46
45
  */
47
46
  async toggleActive(choreName, active) {
48
- const path = `/api/v1/Chores('${enc(choreName)}')`;
47
+ const path = `/api/v1/Chores('${odataKey(choreName)}')`;
49
48
  await this.http.request("PATCH", path, { Active: active });
50
49
  }
51
50
  /**
@@ -65,11 +64,11 @@ export class ChoreService {
65
64
  * — everywhere else. Returns 204, so no status exists to report.
66
65
  */
67
66
  async execute(choreName, opts) {
68
- const plain = `/api/v1/Chores('${enc(choreName)}')/tm1.Execute`;
67
+ const plain = `/api/v1/Chores('${odataKey(choreName)}')/tm1.Execute`;
69
68
  if (this.withReturnSupported === false) {
70
69
  return this.executeWithoutStatus(plain, opts);
71
70
  }
72
- const withReturn = `/api/v1/Chores('${enc(choreName)}')/tm1.ExecuteWithReturn?$expand=ErrorLogFile`;
71
+ const withReturn = `/api/v1/Chores('${odataKey(choreName)}')/tm1.ExecuteWithReturn?$expand=ErrorLogFile`;
73
72
  try {
74
73
  const response = await this.http.request("POST", withReturn, {}, opts);
75
74
  this.withReturnSupported = true;
@@ -129,7 +128,7 @@ export class ChoreService {
129
128
  Frequency: frequencyDuration(chore.frequency),
130
129
  Tasks: chore.steps.map((step, idx) => ({
131
130
  Step: idx,
132
- "Process@odata.bind": `Processes('${enc(step.process)}')`,
131
+ "Process@odata.bind": `Processes('${odataKey(step.process)}')`,
133
132
  Parameters: step.parameters.map((p) => ({
134
133
  Name: p.name,
135
134
  Value: p.value,
@@ -143,7 +142,7 @@ export class ChoreService {
143
142
  * PATCH /api/v1/Chores('{name}')
144
143
  */
145
144
  async update(choreName, updates) {
146
- const path = `/api/v1/Chores('${enc(choreName)}')`;
145
+ const path = `/api/v1/Chores('${odataKey(choreName)}')`;
147
146
  const body = {};
148
147
  if (updates.startTime !== undefined)
149
148
  body.StartTime = updates.startTime;
@@ -159,7 +158,7 @@ export class ChoreService {
159
158
  if (updates.steps !== undefined) {
160
159
  body.Tasks = updates.steps.map((step, idx) => ({
161
160
  Step: idx,
162
- "Process@odata.bind": `Processes('${enc(step.process)}')`,
161
+ "Process@odata.bind": `Processes('${odataKey(step.process)}')`,
163
162
  Parameters: step.parameters.map((p) => ({
164
163
  Name: p.name,
165
164
  Value: p.value,
@@ -173,7 +172,7 @@ export class ChoreService {
173
172
  * DELETE /api/v1/Chores('{name}')
174
173
  */
175
174
  async delete(choreName) {
176
- await this.http.request("DELETE", `/api/v1/Chores('${enc(choreName)}')`);
175
+ await this.http.request("DELETE", `/api/v1/Chores('${odataKey(choreName)}')`);
177
176
  }
178
177
  }
179
178
  //# sourceMappingURL=chore-service.js.map
@@ -1,6 +1,7 @@
1
1
  import type { Cube, CubeRules, RuleSyntaxError } from "../../types.js";
2
- import type { TM1HttpClient } from "../http.js";
2
+ import type { RequestOptions, TM1HttpClient } from "../http.js";
3
3
  import { type NameFilterOpts, type Paged, type PageOpts } from "./odata-page.js";
4
+ import { DimensionOrderCache } from "./dimension-order.js";
4
5
  export interface AllRulesOpts {
5
6
  /** Include control cubes (`}`-prefixed). Default false. */
6
7
  includeControl?: boolean;
@@ -14,7 +15,8 @@ export interface CubeListOpts extends NameFilterOpts {
14
15
  }
15
16
  export declare class CubeService {
16
17
  private readonly http;
17
- constructor(http: TM1HttpClient);
18
+ private readonly dimOrder;
19
+ constructor(http: TM1HttpClient, dimOrder?: DimensionOrderCache);
18
20
  /**
19
21
  * List all cubes with their dimension names.
20
22
  * GET /api/v1/Cubes?$expand=Dimensions($select=Name)
@@ -36,7 +38,8 @@ export declare class CubeService {
36
38
  page: PageOpts;
37
39
  }): Promise<Paged<Cube>>;
38
40
  /**
39
- * Return ordered dimension-name list of a cube.
41
+ * Return ordered dimension-name list of a cube. Cached per connection —
42
+ * see dimension-order.ts.
40
43
  * GET /api/v1/Cubes('{name}')?$expand=Dimensions($select=Name)
41
44
  */
42
45
  getDimensionNames(cubeName: string): Promise<string[]>;
@@ -104,23 +107,16 @@ export declare class CubeService {
104
107
  */
105
108
  checkRule(cubeName: string, ruleText: string): Promise<RuleSyntaxError[]>;
106
109
  /**
107
- * Clear cube cells.
110
+ * Clear every cell in a cube.
108
111
  *
109
- * A **full** clear runs an ephemeral TI with CubeClearData(). tm1.Clear is
110
- * not an alternative on either build: 11.8 and 12.5 both answer
111
- * "'tm1.Clear' resource can not be resolved on type 'Cube'" for a cube that
112
- * exists, and neither declares a clear action anywhere in $metadata — Cube
113
- * carries only Lock and Unlock. CubeClearData() is the only route measured
114
- * to work.
115
- *
116
- * A **partial** clear would need tuple selectors, which only tm1.Clear
117
- * offers, so no server reachable here can do one. Callers get
118
- * UNSUPPORTED_OPERATION pointing at bedrock `}bedrock.cube.data.clear`
119
- * rather than a request that is known to 404.
112
+ * The clear runs an ephemeral TI with CubeClearData(). tm1.Clear is not an
113
+ * alternative on either build: 11.8 and 12.5 both answer "'tm1.Clear'
114
+ * resource can not be resolved on type 'Cube'" for a cube that exists, and
115
+ * neither declares a clear action anywhere in $metadata — Cube carries only
116
+ * Lock and Unlock. CubeClearData() is the only route measured to work, and
117
+ * it takes a cube name and nothing else, so there is no region to scope.
120
118
  */
121
- clear(cubeName: string, dimensions: string[], tuples: string[][]): Promise<void>;
122
- /** Same verdict for both versions: no tuple-selective clear on this server. */
123
- private partialClearUnsupported;
119
+ clear(cubeName: string, opts?: Pick<RequestOptions, "timeoutMs">): Promise<void>;
124
120
  /**
125
121
  * Unload a cube from memory. Forces TM1 to discard the in-memory fed-cell
126
122
  * index and reload from disk on next access. Required for feeder corrections