@ffschrattenecker/tm1-mcp-server 7.0.2 → 8.1.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 +101 -1
- package/README.md +9 -6
- package/dist/config.d.ts +10 -1
- package/dist/config.js +40 -22
- package/dist/connections.d.ts +30 -2
- package/dist/connections.js +94 -21
- package/dist/http-transport.js +51 -4
- package/dist/index.js +6 -0
- 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/sample-cells.js +1 -1
- 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/secrets-cli.d.ts +10 -0
- package/dist/secrets-cli.js +218 -0
- package/dist/secrets.d.ts +39 -0
- package/dist/secrets.js +128 -0
- 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 +16 -27
- 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 +245 -9
- package/package.json +10 -5
- package/dist/tools/dimension-management/move-element.d.ts +0 -2
- package/dist/tools/dimension-management/move-element.js +0 -28
|
@@ -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
|
-
|
|
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.
|
|
46
|
-
*
|
|
47
|
-
*
|
|
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.
|
|
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.
|
|
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.
|
|
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,16 +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
|
|
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
10
|
import { bindLeftOutSandbox } from "../../lib/cell-address.js";
|
|
11
|
+
import { mapSettledWithConcurrency } from "../../lib/concurrency.js";
|
|
11
12
|
import { freeCellset, transformCellsetResponse } from "./cellset-transform.js";
|
|
12
|
-
|
|
13
|
-
|
|
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
|
+
}
|
|
14
36
|
// Build a fully-qualified MDX member reference for a write coordinate.
|
|
15
37
|
// A caller may pass a pre-qualified ref to target an ALTERNATE hierarchy
|
|
16
38
|
// (`[Dim].[AltHier].[Elem]`, or `[Dim].[Elem]` for the default) — passed
|
|
@@ -27,8 +49,10 @@ function qualifyWriteMember(dim, element) {
|
|
|
27
49
|
}
|
|
28
50
|
export class CellService {
|
|
29
51
|
http;
|
|
30
|
-
|
|
52
|
+
dimOrder;
|
|
53
|
+
constructor(http, dimOrder = new DimensionOrderCache(http)) {
|
|
31
54
|
this.http = http;
|
|
55
|
+
this.dimOrder = dimOrder;
|
|
32
56
|
}
|
|
33
57
|
/**
|
|
34
58
|
* Get a single cell value via a 1-tuple MDX query.
|
|
@@ -42,9 +66,7 @@ export class CellService {
|
|
|
42
66
|
if (elements.length === 0) {
|
|
43
67
|
return null;
|
|
44
68
|
}
|
|
45
|
-
const
|
|
46
|
-
const cubeMeta = await this.http.request("GET", cubePath);
|
|
47
|
-
const dims = cubeMeta.Dimensions.map((d) => d.Name);
|
|
69
|
+
const dims = await this.dimOrder.get(cubeName);
|
|
48
70
|
// Sandboxes left out → Base, as tm1_write_cells does.
|
|
49
71
|
elements = bindLeftOutSandbox(dims, elements) ?? elements;
|
|
50
72
|
if (elements.length !== dims.length) {
|
|
@@ -164,11 +186,11 @@ export class CellService {
|
|
|
164
186
|
const cellset = await this.http.request("POST", "/api/v1/ExecuteMDX", { MDX: mdx });
|
|
165
187
|
const id = cellset.ID;
|
|
166
188
|
try {
|
|
167
|
-
await this.http.request("PATCH", `/api/v1/Cellsets('${
|
|
189
|
+
await this.http.request("PATCH", `/api/v1/Cellsets('${odataKey(id)}')/Cells(0)`, { Value: c.value });
|
|
168
190
|
}
|
|
169
191
|
finally {
|
|
170
192
|
try {
|
|
171
|
-
await this.http.request("DELETE", `/api/v1/Cellsets('${
|
|
193
|
+
await this.http.request("DELETE", `/api/v1/Cellsets('${odataKey(id)}')`);
|
|
172
194
|
}
|
|
173
195
|
catch {
|
|
174
196
|
// cleanup best-effort
|
|
@@ -186,7 +208,7 @@ export class CellService {
|
|
|
186
208
|
let bulkFailed = false;
|
|
187
209
|
const cellset = await this.http.request("POST", "/api/v1/ExecuteMDX", { MDX: mdx });
|
|
188
210
|
try {
|
|
189
|
-
await this.http.request("PATCH", `/api/v1/Cellsets('${
|
|
211
|
+
await this.http.request("PATCH", `/api/v1/Cellsets('${odataKey(cellset.ID)}')/Cells`, chunk.map((c, ordinal) => ({ Ordinal: ordinal, Value: c.value })));
|
|
190
212
|
written += chunk.length;
|
|
191
213
|
}
|
|
192
214
|
catch {
|
|
@@ -200,7 +222,7 @@ export class CellService {
|
|
|
200
222
|
}
|
|
201
223
|
finally {
|
|
202
224
|
try {
|
|
203
|
-
await this.http.request("DELETE", `/api/v1/Cellsets('${
|
|
225
|
+
await this.http.request("DELETE", `/api/v1/Cellsets('${odataKey(cellset.ID)}')`);
|
|
204
226
|
}
|
|
205
227
|
catch {
|
|
206
228
|
// cleanup best-effort
|
|
@@ -208,7 +230,9 @@ export class CellService {
|
|
|
208
230
|
}
|
|
209
231
|
if (!bulkFailed)
|
|
210
232
|
continue;
|
|
211
|
-
|
|
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));
|
|
212
236
|
const failed = [];
|
|
213
237
|
results.forEach((r, j) => {
|
|
214
238
|
if (r.status === "fulfilled") {
|
|
@@ -238,39 +262,41 @@ export class CellService {
|
|
|
238
262
|
}
|
|
239
263
|
/**
|
|
240
264
|
* Resolve the cube's dimension order and build Tuple@odata.bind paths for
|
|
241
|
-
* the cell-bound trace actions.
|
|
242
|
-
*
|
|
243
|
-
*
|
|
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().
|
|
244
268
|
*/
|
|
245
269
|
async tupleBinds(cubeName, elements) {
|
|
246
|
-
const
|
|
247
|
-
const dims = cubeMeta.Dimensions.map((d) => d.Name);
|
|
270
|
+
const dims = await this.dimOrder.get(cubeName);
|
|
248
271
|
if (elements.length !== dims.length) {
|
|
249
272
|
throw new TM1Error({
|
|
250
273
|
code: TM1ErrorCode.VALIDATION_ERROR,
|
|
251
274
|
message: `Cube '${cubeName}' has ${dims.length} dimension(s) (${dims.join(", ")}) but ${elements.length} element(s) were given`,
|
|
252
275
|
});
|
|
253
276
|
}
|
|
254
|
-
return dims.map((d, 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
|
+
});
|
|
255
281
|
}
|
|
256
282
|
/**
|
|
257
283
|
* Check the feeders of a cell: returns the cells fed by this cell with a
|
|
258
|
-
* Fed flag per target — Fed=false marks a broken/missing feeder.
|
|
284
|
+
* Fed flag per target — Fed=false marks a broken/missing feeder.
|
|
259
285
|
* POST /api/v1/Cubes('{cube}')/tm1.CheckFeeders
|
|
260
286
|
*/
|
|
261
287
|
async checkFeeders(cubeName, elements, opts) {
|
|
262
288
|
const binds = await this.tupleBinds(cubeName, elements);
|
|
263
|
-
const response = await this.http.request("POST", `/api/v1/Cubes('${
|
|
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);
|
|
264
290
|
return (response.value ?? []).map(mapFedCell);
|
|
265
291
|
}
|
|
266
292
|
/**
|
|
267
293
|
* Trace the feeders of a cell: returns the cells this cell feeds plus the
|
|
268
|
-
* feeder statements involved.
|
|
294
|
+
* feeder statements involved.
|
|
269
295
|
* POST /api/v1/Cubes('{cube}')/tm1.TraceFeeders
|
|
270
296
|
*/
|
|
271
297
|
async traceFeeders(cubeName, elements, opts) {
|
|
272
298
|
const binds = await this.tupleBinds(cubeName, elements);
|
|
273
|
-
const response = await this.http.request("POST", `/api/v1/Cubes('${
|
|
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);
|
|
274
300
|
return {
|
|
275
301
|
fedCells: (response.FedCells ?? []).map(mapFedCell),
|
|
276
302
|
statements: response.Statements ?? [],
|
|
@@ -280,7 +306,7 @@ export class CellService {
|
|
|
280
306
|
* Trace how a cell value is calculated: recursive component tree with
|
|
281
307
|
* per-component type (consolidation/rule), status, value, and rule
|
|
282
308
|
* statements. The server returns the full tree; maxDepth/maxComponents
|
|
283
|
-
* truncate client-side to keep responses bounded.
|
|
309
|
+
* truncate client-side to keep responses bounded.
|
|
284
310
|
* POST /api/v1/Cubes('{cube}')/tm1.TraceCellCalculation
|
|
285
311
|
*/
|
|
286
312
|
async traceCellCalculation(cubeName, elements, maxDepth = 3, maxComponents = 20, opts) {
|
|
@@ -296,7 +322,7 @@ export class CellService {
|
|
|
296
322
|
const prefix = "Components/".repeat(level);
|
|
297
323
|
expandParts.push(`${prefix}Tuple($select=Name)`, `${prefix}Cube($select=Name)`);
|
|
298
324
|
}
|
|
299
|
-
const response = await this.http.request("POST", `/api/v1/Cubes('${
|
|
325
|
+
const response = await this.http.request("POST", `/api/v1/Cubes('${odataKey(cubeName)}')/tm1.TraceCellCalculation?$expand=${expandParts.join(",")}`, { "Tuple@odata.bind": binds }, opts);
|
|
300
326
|
return mapCalcComponent(response, maxDepth, maxComponents);
|
|
301
327
|
}
|
|
302
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
|
-
|
|
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('${
|
|
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('${
|
|
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('${
|
|
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('${
|
|
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('${
|
|
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('${
|
|
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('${
|
|
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
|
-
|
|
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
|
|
110
|
+
* Clear every cell in a cube.
|
|
108
111
|
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* to
|
|
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,
|
|
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
|
|
@@ -5,14 +5,15 @@
|
|
|
5
5
|
// as deprecated wrappers during the migration period (Phase 1 of the
|
|
6
6
|
// god-class split — see docs/ARCHITECTURE.md).
|
|
7
7
|
import { TM1Error, TM1ErrorCode } from "../../types.js";
|
|
8
|
-
import { filterClause, nameFilterPredicates, pageClauses, readCount, } from "./odata-page.js";
|
|
8
|
+
import { filterClause, nameFilterPredicates, pageClauses, readCount, odataKey, } from "./odata-page.js";
|
|
9
9
|
import { classifyExecution } from "./process-status.js";
|
|
10
|
-
|
|
11
|
-
const enc = (s) => encodeURIComponent(String(s).replace(/'/g, "''"));
|
|
10
|
+
import { DimensionOrderCache } from "./dimension-order.js";
|
|
12
11
|
export class CubeService {
|
|
13
12
|
http;
|
|
14
|
-
|
|
13
|
+
dimOrder;
|
|
14
|
+
constructor(http, dimOrder = new DimensionOrderCache(http)) {
|
|
15
15
|
this.http = http;
|
|
16
|
+
this.dimOrder = dimOrder;
|
|
16
17
|
}
|
|
17
18
|
async list(opts = {}) {
|
|
18
19
|
// Without a top-level $select TM1 returns every cube property — including
|
|
@@ -41,13 +42,12 @@ export class CubeService {
|
|
|
41
42
|
: { items, total: readCount(response) };
|
|
42
43
|
}
|
|
43
44
|
/**
|
|
44
|
-
* Return ordered dimension-name list of a cube.
|
|
45
|
+
* Return ordered dimension-name list of a cube. Cached per connection —
|
|
46
|
+
* see dimension-order.ts.
|
|
45
47
|
* GET /api/v1/Cubes('{name}')?$expand=Dimensions($select=Name)
|
|
46
48
|
*/
|
|
47
49
|
async getDimensionNames(cubeName) {
|
|
48
|
-
|
|
49
|
-
const response = await this.http.request("GET", path);
|
|
50
|
-
return response.Dimensions.map((d) => d.Name);
|
|
50
|
+
return this.dimOrder.get(cubeName);
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
53
|
* Cheap existence probe: a single GET with $select=Name (404 → false)
|
|
@@ -62,7 +62,7 @@ export class CubeService {
|
|
|
62
62
|
*/
|
|
63
63
|
async exists(cubeName) {
|
|
64
64
|
try {
|
|
65
|
-
await this.http.request("GET", `/api/v1/Cubes('${
|
|
65
|
+
await this.http.request("GET", `/api/v1/Cubes('${odataKey(cubeName)}')?$select=Name`);
|
|
66
66
|
return true;
|
|
67
67
|
}
|
|
68
68
|
catch (e) {
|
|
@@ -79,7 +79,7 @@ export class CubeService {
|
|
|
79
79
|
await this.http.request("POST", "/api/v1/Cubes", {
|
|
80
80
|
Name: name,
|
|
81
81
|
Dimensions: dimensionNames.map((d) => ({
|
|
82
|
-
"@odata.id": `Dimensions('${
|
|
82
|
+
"@odata.id": `Dimensions('${odataKey(d)}')`,
|
|
83
83
|
})),
|
|
84
84
|
});
|
|
85
85
|
}
|
|
@@ -88,21 +88,21 @@ export class CubeService {
|
|
|
88
88
|
* DELETE /api/v1/Cubes('{name}')
|
|
89
89
|
*/
|
|
90
90
|
async delete(name) {
|
|
91
|
-
await this.http.request("DELETE", `/api/v1/Cubes('${
|
|
91
|
+
await this.http.request("DELETE", `/api/v1/Cubes('${odataKey(name)}')`);
|
|
92
92
|
}
|
|
93
93
|
/**
|
|
94
94
|
* Get the rules text for a cube.
|
|
95
95
|
* GET /api/v1/Cubes('{name}')/Rules
|
|
96
96
|
*/
|
|
97
97
|
async getRules(cubeName) {
|
|
98
|
-
const path = `/api/v1/Cubes('${
|
|
98
|
+
const path = `/api/v1/Cubes('${odataKey(cubeName)}')/Rules`;
|
|
99
99
|
// TM1 returns 404/204 both for "cube missing" and "cube has no rules";
|
|
100
100
|
// an empty 200 body (response undefined) also means "no rules". For any
|
|
101
101
|
// of those, probe `/Cubes('X')?$select=Name` to disambiguate so callers
|
|
102
102
|
// get a clear NOT_FOUND for typos instead of a silently-empty rulesText.
|
|
103
103
|
const empty = async () => {
|
|
104
104
|
try {
|
|
105
|
-
await this.http.request("GET", `/api/v1/Cubes('${
|
|
105
|
+
await this.http.request("GET", `/api/v1/Cubes('${odataKey(cubeName)}')?$select=Name`);
|
|
106
106
|
}
|
|
107
107
|
catch (probeErr) {
|
|
108
108
|
if (probeErr instanceof TM1Error && probeErr.httpStatus === 404) {
|
|
@@ -173,7 +173,7 @@ export class CubeService {
|
|
|
173
173
|
* text, so it travels in `rulesText` like every other statement.
|
|
174
174
|
*/
|
|
175
175
|
async updateRules(cubeName, rulesText) {
|
|
176
|
-
const cubePath = `/api/v1/Cubes('${
|
|
176
|
+
const cubePath = `/api/v1/Cubes('${odataKey(cubeName)}')`;
|
|
177
177
|
await this.http.request("PATCH", cubePath, { Rules: rulesText });
|
|
178
178
|
}
|
|
179
179
|
/**
|
|
@@ -181,7 +181,7 @@ export class CubeService {
|
|
|
181
181
|
* POST /api/v1/Cubes('{name}')/tm1.CheckRules with { Rules: "..." }
|
|
182
182
|
*/
|
|
183
183
|
async checkRule(cubeName, ruleText) {
|
|
184
|
-
const path = `/api/v1/Cubes('${
|
|
184
|
+
const path = `/api/v1/Cubes('${odataKey(cubeName)}')/tm1.CheckRules`;
|
|
185
185
|
const response = await this.http.request("POST", path, { Rules: ruleText });
|
|
186
186
|
return (response.value ?? []).map((e) => ({
|
|
187
187
|
message: e.Message,
|
|
@@ -189,33 +189,17 @@ export class CubeService {
|
|
|
189
189
|
}));
|
|
190
190
|
}
|
|
191
191
|
/**
|
|
192
|
-
* Clear cube
|
|
193
|
-
*
|
|
194
|
-
* A **full** clear runs an ephemeral TI with CubeClearData(). tm1.Clear is
|
|
195
|
-
* not an alternative on either build: 11.8 and 12.5 both answer
|
|
196
|
-
* "'tm1.Clear' resource can not be resolved on type 'Cube'" for a cube that
|
|
197
|
-
* exists, and neither declares a clear action anywhere in $metadata — Cube
|
|
198
|
-
* carries only Lock and Unlock. CubeClearData() is the only route measured
|
|
199
|
-
* to work.
|
|
192
|
+
* Clear every cell in a cube.
|
|
200
193
|
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
194
|
+
* The clear runs an ephemeral TI with CubeClearData(). tm1.Clear is not an
|
|
195
|
+
* alternative on either build: 11.8 and 12.5 both answer "'tm1.Clear'
|
|
196
|
+
* resource can not be resolved on type 'Cube'" for a cube that exists, and
|
|
197
|
+
* neither declares a clear action anywhere in $metadata — Cube carries only
|
|
198
|
+
* Lock and Unlock. CubeClearData() is the only route measured to work, and
|
|
199
|
+
* it takes a cube name and nothing else, so there is no region to scope.
|
|
205
200
|
*/
|
|
206
|
-
async clear(cubeName,
|
|
207
|
-
|
|
208
|
-
if (!isFullClear)
|
|
209
|
-
throw this.partialClearUnsupported(cubeName);
|
|
210
|
-
await this.clearViaTI(cubeName);
|
|
211
|
-
}
|
|
212
|
-
/** Same verdict for both versions: no tuple-selective clear on this server. */
|
|
213
|
-
partialClearUnsupported(cubeName) {
|
|
214
|
-
return new TM1Error({
|
|
215
|
-
code: TM1ErrorCode.UNSUPPORTED_OPERATION,
|
|
216
|
-
message: `Partial clearCube is not supported on TM1 ${this.http.tm1Version} (no tm1.Clear endpoint). Implement a TI process with bedrock '}bedrock.cube.data.clear' or custom CellPutN loop and call via tm1_execute_process.`,
|
|
217
|
-
endpoint: `/api/v1/Cubes('${cubeName}')/tm1.Clear`,
|
|
218
|
-
});
|
|
201
|
+
async clear(cubeName, opts) {
|
|
202
|
+
await this.clearViaTI(cubeName, opts);
|
|
219
203
|
}
|
|
220
204
|
/**
|
|
221
205
|
* Unload a cube from memory. Forces TM1 to discard the in-memory fed-cell
|
|
@@ -225,10 +209,10 @@ export class CubeService {
|
|
|
225
209
|
* POST /api/v1/Cubes('{cube}')/tm1.Unload
|
|
226
210
|
*/
|
|
227
211
|
async unload(cubeName) {
|
|
228
|
-
await this.http.request("POST", `/api/v1/Cubes('${
|
|
212
|
+
await this.http.request("POST", `/api/v1/Cubes('${odataKey(cubeName)}')/tm1.Unload`);
|
|
229
213
|
}
|
|
230
214
|
// 11.x fallback: deploy ephemeral TI with CubeClearData(), execute, delete.
|
|
231
|
-
async clearViaTI(cubeName) {
|
|
215
|
+
async clearViaTI(cubeName, opts) {
|
|
232
216
|
// Cap the sanitized cube name so the temp process name stays under TM1's
|
|
233
217
|
// ~256-char process-name limit (prefix + timestamp suffix add ~25 chars).
|
|
234
218
|
const safeName = cubeName.replace(/[^A-Za-z0-9_]/g, "_").slice(0, 200);
|
|
@@ -244,8 +228,29 @@ export class CubeService {
|
|
|
244
228
|
EpilogProcedure: "",
|
|
245
229
|
DataSource: { Type: "None" },
|
|
246
230
|
});
|
|
231
|
+
let timedOut = false;
|
|
247
232
|
try {
|
|
248
|
-
const result = await this.http
|
|
233
|
+
const result = await this.http
|
|
234
|
+
.request("POST", `/api/v1/Processes('${odataKey(procName)}')/tm1.ExecuteWithReturn`, {}, opts)
|
|
235
|
+
.catch((err) => {
|
|
236
|
+
// A client-side timeout does not stop the TI — but deleting the
|
|
237
|
+
// process does, when it is still waiting on a lock (measured on
|
|
238
|
+
// 12.5: the cube kept its data). So on a timeout the process is left
|
|
239
|
+
// in place to finish; 11.8 held the DELETE until the TI was done
|
|
240
|
+
// anyway. Reporting a bare timeout would tell the model the cube
|
|
241
|
+
// still holds its data — the one reading this must not give.
|
|
242
|
+
if (err instanceof TM1Error &&
|
|
243
|
+
err.code === TM1ErrorCode.LOCK_TIMEOUT) {
|
|
244
|
+
timedOut = true;
|
|
245
|
+
throw new TM1Error({
|
|
246
|
+
code: TM1ErrorCode.LOCK_TIMEOUT,
|
|
247
|
+
message: `Cube clear for '${cubeName}' did not answer within the request timeout, but the clear keeps running on the server — expect the cube to end up empty. Do not retry; check it with tm1_get_cube_stats once the server is idle. The temporary process '${procName}' was left in place so the clear can finish; delete it afterwards with tm1_delete_process.`,
|
|
248
|
+
endpoint: err.endpoint,
|
|
249
|
+
hint: "Pass a larger timeoutMs for big cubes or a busy server so the call waits for the clear to finish.",
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
throw err;
|
|
253
|
+
});
|
|
249
254
|
// ExecuteWithReturn returns HTTP 200 even when the process aborts; the real
|
|
250
255
|
// outcome is in ProcessExecuteStatusCode. Without this check an aborted
|
|
251
256
|
// clear (e.g. lock, security) would be reported as a successful clear.
|
|
@@ -277,13 +282,14 @@ export class CubeService {
|
|
|
277
282
|
message: verdict.outcome === "rolled_back"
|
|
278
283
|
? `Cube clear via TI was rolled back for cube '${cubeName}' (status: ${verdict.processErrorStatus}). Nothing was cleared — the cube is unchanged.`
|
|
279
284
|
: `Cube clear via TI could not be confirmed for cube '${cubeName}': ${verdict.processErrorStatus} The clear may or may not have run — check the cube before retrying.`,
|
|
280
|
-
endpoint: `/api/v1/Processes('${
|
|
285
|
+
endpoint: `/api/v1/Processes('${odataKey(procName)}')/tm1.ExecuteWithReturn`,
|
|
281
286
|
});
|
|
282
287
|
}
|
|
283
288
|
}
|
|
284
289
|
finally {
|
|
285
290
|
try {
|
|
286
|
-
|
|
291
|
+
if (!timedOut)
|
|
292
|
+
await this.http.request("DELETE", `/api/v1/Processes('${odataKey(procName)}')`);
|
|
287
293
|
}
|
|
288
294
|
catch (cleanupErr) {
|
|
289
295
|
this.http.logger.warn({ proc: procName, err: String(cleanupErr) }, "Failed to delete ephemeral clearCube TI process — manual cleanup needed");
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { TM1HttpClient } from "../http.js";
|
|
2
|
+
export declare class DimensionOrderCache {
|
|
3
|
+
private readonly http;
|
|
4
|
+
private readonly now;
|
|
5
|
+
private readonly entries;
|
|
6
|
+
constructor(http: TM1HttpClient, now?: () => number);
|
|
7
|
+
/**
|
|
8
|
+
* Ordered dimension names of `cubeName`.
|
|
9
|
+
* GET /api/v1/Cubes('{name}')?$expand=Dimensions($select=Name)
|
|
10
|
+
*/
|
|
11
|
+
get(cubeName: string): Promise<string[]>;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=dimension-order.d.ts.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { tm1NameKey } from "../../lib/tm1-name.js";
|
|
2
|
+
import { odataKey as enc } from "./odata-page.js";
|
|
3
|
+
const TTL_MS = 60_000;
|
|
4
|
+
const INERT_PATHS = [/\/ExecuteMDX/i, /\/Cellsets/i];
|
|
5
|
+
export class DimensionOrderCache {
|
|
6
|
+
http;
|
|
7
|
+
now;
|
|
8
|
+
entries = new Map();
|
|
9
|
+
constructor(http, now = Date.now) {
|
|
10
|
+
this.http = http;
|
|
11
|
+
this.now = now;
|
|
12
|
+
// Test doubles stub only request(); a real client always has the hook.
|
|
13
|
+
if (typeof http.onMutation === "function") {
|
|
14
|
+
http.onMutation((e) => {
|
|
15
|
+
if (!INERT_PATHS.some((re) => re.test(e.path)))
|
|
16
|
+
this.entries.clear();
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Ordered dimension names of `cubeName`.
|
|
22
|
+
* GET /api/v1/Cubes('{name}')?$expand=Dimensions($select=Name)
|
|
23
|
+
*/
|
|
24
|
+
get(cubeName) {
|
|
25
|
+
const key = tm1NameKey(cubeName);
|
|
26
|
+
const hit = this.entries.get(key);
|
|
27
|
+
if (hit && this.now() - hit.at < TTL_MS)
|
|
28
|
+
return hit.dims;
|
|
29
|
+
// The promise is cached, so concurrent callers share one request; a
|
|
30
|
+
// failure is evicted so the next call retries instead of replaying it.
|
|
31
|
+
const dims = this.http
|
|
32
|
+
.request("GET", `/api/v1/Cubes('${enc(cubeName)}')?$expand=Dimensions($select=Name)`)
|
|
33
|
+
.then((r) => r.Dimensions.map((d) => d.Name));
|
|
34
|
+
this.entries.set(key, { dims, at: this.now() });
|
|
35
|
+
dims.catch(() => {
|
|
36
|
+
if (this.entries.get(key)?.dims === dims)
|
|
37
|
+
this.entries.delete(key);
|
|
38
|
+
});
|
|
39
|
+
return dims;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=dimension-order.js.map
|