@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.
- package/CHANGELOG.md +102 -1
- package/README.md +5 -5
- package/dist/config.js +34 -17
- package/dist/connections.d.ts +18 -1
- package/dist/connections.js +34 -18
- package/dist/http-transport.js +51 -4
- package/dist/lib/callgraph/referenceIndex.js +2 -1
- package/dist/lib/callgraph/rulesLinter.d.ts +0 -19
- package/dist/lib/callgraph/rulesLinter.js +0 -590
- package/dist/lib/callgraph/tiParser.js +10 -5
- package/dist/lib/callgraph/tm1-adapter.d.ts +5 -1
- package/dist/lib/callgraph/tm1-adapter.js +41 -9
- package/dist/lib/callgraph/variableEnv.js +3 -2
- package/dist/lib/cell-address.d.ts +11 -1
- package/dist/lib/cell-address.js +22 -3
- package/dist/lib/complexity/antipatterns.js +3 -1
- package/dist/lib/complexity/comment-classifier.js +2 -1
- package/dist/lib/coordinate-error.js +5 -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.d.ts +6 -0
- package/dist/lib/sample-cells.js +20 -2
- package/dist/lib/ti-identifier.d.ts +11 -0
- package/dist/lib/ti-identifier.js +11 -0
- package/dist/lib/tm1-name.d.ts +9 -0
- package/dist/lib/tm1-name.js +9 -0
- package/dist/lib/v12-compat/deprecated-ti.js +2 -2
- package/dist/session-manager.d.ts +1 -0
- package/dist/session-manager.js +24 -0
- package/dist/tm1-client/connection/profile.js +2 -4
- package/dist/tm1-client/dispatcher.d.ts +1 -1
- package/dist/tm1-client/dispatcher.js +18 -6
- package/dist/tm1-client/http.d.ts +21 -0
- package/dist/tm1-client/http.js +39 -30
- package/dist/tm1-client/services/cell-service.d.ts +9 -7
- package/dist/tm1-client/services/cell-service.js +54 -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 +43 -25
- package/dist/tools/celldata/execute-mdx.js +19 -23
- package/dist/tools/celldata/get-cell-value.js +1 -1
- 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/sample-cells.js +3 -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/format.d.ts +1 -0
- package/dist/tools/format.js +7 -2
- package/dist/tools/index.js +0 -2
- package/dist/tools/model-building/check-cube-rule.js +3 -2
- package/dist/tools/model-building/clear-cube.js +35 -26
- package/dist/tools/model-building/set-cube-rules.js +13 -20
- package/dist/tools/model-building/unload-cube.js +1 -1
- package/dist/tools/operations/get-cube-stats.js +1 -1
- package/dist/tools/operations/get-transaction-log.js +1 -1
- package/dist/tools/operations/list-error-logs.js +27 -2
- package/dist/tools/schemas/items-fileops.d.ts +4 -0
- package/dist/tools/schemas/items-fileops.js +1 -0
- package/dist/tools/security/list-clients.js +5 -5
- package/dist/tools/security/list-groups.js +1 -1
- package/dist/tools/subsets/create-subset.js +9 -5
- package/dist/tools/subsets/delete-subset.js +8 -3
- package/dist/tools/subsets/update-subset.js +9 -9
- package/dist/tools/ti-development/diff-process-with-file.js +4 -29
- package/dist/tools/ti-development/diff-processes.js +9 -0
- package/dist/tools/ti-development/export-process-to-git.js +9 -7
- package/dist/tools/ti-development/export-process-to-pro.js +15 -8
- package/dist/tools/ti-development/import-pro-file.js +10 -21
- package/dist/tools/ti-development/import-process-from-git.js +8 -18
- package/dist/tools/ti-development/install-pro-bundle.js +2 -2
- package/dist/tools/ti-development/upsert-process.js +26 -5
- package/npm-shrinkwrap.json +465 -500
- package/package.json +14 -10
- package/dist/tools/dimension-management/move-element.d.ts +0 -2
- package/dist/tools/dimension-management/move-element.js +0 -28
|
@@ -17,14 +17,26 @@ import { rethrowIfSystemic } from "../../tm1-client/services/fallback.js";
|
|
|
17
17
|
// Deliberately an allowlist of known-inert paths, not a denylist of
|
|
18
18
|
// code-relevant ones: anything unrecognised keeps invalidating, so a new
|
|
19
19
|
// mutating endpoint is stale-safe by default.
|
|
20
|
-
|
|
20
|
+
//
|
|
21
|
+
// The read-only POST actions are listed too: executing a view, checking rules
|
|
22
|
+
// or feeders, tracing, compiling. Running a PROCESS or CHORE is not — TI can
|
|
23
|
+
// rewrite rules (RuleLoadFromFile) — so tm1.Execute is inert only on Views.
|
|
24
|
+
const INERT_PATHS = [
|
|
25
|
+
/\/ExecuteMDX/i,
|
|
26
|
+
/\/Cellsets/i,
|
|
27
|
+
/tm1\.Update/i,
|
|
28
|
+
/\/Views\([^/]*\)\/tm1\.Execute\b/i,
|
|
29
|
+
/tm1\.(?:CheckRules|CheckFeeders|TraceFeeders|TraceCellCalculation|Compile)\b/i,
|
|
30
|
+
/\/CompileProcess\b/i,
|
|
31
|
+
];
|
|
21
32
|
function affectsReferenceGraph(path) {
|
|
22
33
|
return !INERT_PATHS.some((re) => re.test(path));
|
|
23
34
|
}
|
|
24
35
|
const invalidateOnMutation = (e) => {
|
|
25
36
|
if (!affectsReferenceGraph(e.path))
|
|
26
37
|
return;
|
|
27
|
-
|
|
38
|
+
// Scoped: a write on one connection says nothing about another's code.
|
|
39
|
+
invalidateCallgraphCache(e.connectionId);
|
|
28
40
|
};
|
|
29
41
|
/**
|
|
30
42
|
* Wire the callgraph reference-index cache to tm1Events "mutation" notifications:
|
|
@@ -53,11 +65,31 @@ const inflight = new Map();
|
|
|
53
65
|
// read that started before the mutation, so that result is merely old, not
|
|
54
66
|
// wrong. What must not happen is that snapshot becoming the answer for everyone
|
|
55
67
|
// else for the next 60 seconds.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
68
|
+
//
|
|
69
|
+
// Kept per connection (plus a global one for clear-all), so invalidating one
|
|
70
|
+
// connection does not discard a build in flight for another.
|
|
71
|
+
let globalGeneration = 0;
|
|
72
|
+
const generations = new Map();
|
|
73
|
+
const generationOf = (connectionId) => `${globalGeneration}:${generations.get(connectionId) ?? 0}`;
|
|
74
|
+
/**
|
|
75
|
+
* Drop cached indexes for one connection, or for all of them when
|
|
76
|
+
* `connectionId` is omitted (the explicit tm1_invalidate_callgraph_cache tool).
|
|
77
|
+
*/
|
|
78
|
+
export function invalidateCallgraphCache(connectionId) {
|
|
79
|
+
if (connectionId === undefined) {
|
|
80
|
+
const n = cache.size;
|
|
81
|
+
cache.clear();
|
|
82
|
+
globalGeneration++;
|
|
83
|
+
return { cleared: n };
|
|
84
|
+
}
|
|
85
|
+
let n = 0;
|
|
86
|
+
for (const key of [...cache.keys()]) {
|
|
87
|
+
if (key.startsWith(`${connectionId}|`)) {
|
|
88
|
+
cache.delete(key);
|
|
89
|
+
n++;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
generations.set(connectionId, (generations.get(connectionId) ?? 0) + 1);
|
|
61
93
|
return { cleared: n };
|
|
62
94
|
}
|
|
63
95
|
export function getCallgraphCacheStats() {
|
|
@@ -89,10 +121,10 @@ export async function buildIndexFromTM1(tm1Client, opts = {}) {
|
|
|
89
121
|
}
|
|
90
122
|
const promise = (async () => {
|
|
91
123
|
const start = Date.now();
|
|
92
|
-
const startGeneration =
|
|
124
|
+
const startGeneration = generationOf(tm1Client.connectionId);
|
|
93
125
|
const idx = await buildIndexInternal(tm1Client, includeControl);
|
|
94
126
|
// Only publish if no invalidation happened while we were building.
|
|
95
|
-
if (
|
|
127
|
+
if (generationOf(tm1Client.connectionId) === startGeneration) {
|
|
96
128
|
cache.set(key, {
|
|
97
129
|
index: idx,
|
|
98
130
|
ts: Date.now(),
|
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
* Multi-assignment (a variable is assigned different values) → dynamic,
|
|
11
11
|
* because the value is no longer unambiguous at any given call site.
|
|
12
12
|
*/
|
|
13
|
+
import { TI_VAR } from "../ti-identifier.js";
|
|
13
14
|
const STRING_LITERAL_RE = /^'([^']*)'$/;
|
|
14
15
|
const NUMERIC_LITERAL_RE = /^-?\d+(?:\.\d+)?$/;
|
|
15
|
-
const IDENTIFIER_RE =
|
|
16
|
+
const IDENTIFIER_RE = new RegExp(`^${TI_VAR}$`);
|
|
16
17
|
function neutralizeLine(line) {
|
|
17
18
|
return line
|
|
18
19
|
.replace(/'[^']*'/g, (s) => " ".repeat(s.length))
|
|
@@ -136,7 +137,7 @@ export function buildProcessEnv(text, paramNames, stopAtLineOrOptions) {
|
|
|
136
137
|
};
|
|
137
138
|
const assignedOnce = new Map();
|
|
138
139
|
const seen = new Set();
|
|
139
|
-
const assignRe =
|
|
140
|
+
const assignRe = new RegExp(`^\\s*(${TI_VAR})\\s*=\\s*(.+?)\\s*;?\\s*(?:#.*)?$`);
|
|
140
141
|
const lines = text.split("\n");
|
|
141
142
|
const limit = opts.stopAtLine !== undefined
|
|
142
143
|
? Math.min(opts.stopAtLine, lines.length)
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
export declare const SANDBOX_DIMENSION = "Sandboxes";
|
|
2
2
|
export declare const BASE_SANDBOX = "Base";
|
|
3
|
+
/** Index of the cube's `Sandboxes` dimension, or -1. */
|
|
4
|
+
export declare function sandboxPosition(cubeDimensions: readonly string[]): number;
|
|
5
|
+
/**
|
|
6
|
+
* Positional counterpart of resolveCellAddress, for tools that take one
|
|
7
|
+
* element per cube dimension (get_cell_value, check_writable_coords without
|
|
8
|
+
* `dimensions`). A coordinate exactly one short on a cube with `Sandboxes`
|
|
9
|
+
* is read as "Sandboxes left out" and gets `Base` at its position; anything
|
|
10
|
+
* else returns undefined and the caller's count check refuses it as before.
|
|
11
|
+
*/
|
|
12
|
+
export declare function bindLeftOutSandbox(cubeDimensions: readonly string[], elements: readonly string[]): string[] | undefined;
|
|
3
13
|
export interface ResolvedAddress {
|
|
4
14
|
/** The cube's dimensions, in cube order. */
|
|
5
15
|
dimensions: string[];
|
|
@@ -8,5 +18,5 @@ export interface ResolvedAddress {
|
|
|
8
18
|
/** Set when `Sandboxes` was left out and bound to `Base`. */
|
|
9
19
|
sandboxDefaulted?: typeof BASE_SANDBOX;
|
|
10
20
|
}
|
|
11
|
-
export declare function resolveCellAddress(cubeName: string, cubeDimensions: readonly string[], given: readonly string[]): ResolvedAddress;
|
|
21
|
+
export declare function resolveCellAddress(cubeName: string, cubeDimensions: readonly string[], given: readonly string[], outcome?: string): ResolvedAddress;
|
|
12
22
|
//# sourceMappingURL=cell-address.d.ts.map
|
package/dist/lib/cell-address.js
CHANGED
|
@@ -20,7 +20,26 @@
|
|
|
20
20
|
import { TM1Error, TM1ErrorCode } from "../types.js";
|
|
21
21
|
export const SANDBOX_DIMENSION = "Sandboxes";
|
|
22
22
|
export const BASE_SANDBOX = "Base";
|
|
23
|
-
|
|
23
|
+
/** Index of the cube's `Sandboxes` dimension, or -1. */
|
|
24
|
+
export function sandboxPosition(cubeDimensions) {
|
|
25
|
+
return cubeDimensions.findIndex((d) => d.toLowerCase() === SANDBOX_DIMENSION.toLowerCase());
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Positional counterpart of resolveCellAddress, for tools that take one
|
|
29
|
+
* element per cube dimension (get_cell_value, check_writable_coords without
|
|
30
|
+
* `dimensions`). A coordinate exactly one short on a cube with `Sandboxes`
|
|
31
|
+
* is read as "Sandboxes left out" and gets `Base` at its position; anything
|
|
32
|
+
* else returns undefined and the caller's count check refuses it as before.
|
|
33
|
+
*/
|
|
34
|
+
export function bindLeftOutSandbox(cubeDimensions, elements) {
|
|
35
|
+
if (elements.length !== cubeDimensions.length - 1)
|
|
36
|
+
return undefined;
|
|
37
|
+
const pos = sandboxPosition(cubeDimensions);
|
|
38
|
+
if (pos < 0)
|
|
39
|
+
return undefined;
|
|
40
|
+
return [...elements.slice(0, pos), BASE_SANDBOX, ...elements.slice(pos)];
|
|
41
|
+
}
|
|
42
|
+
export function resolveCellAddress(cubeName, cubeDimensions, given, outcome = "Nothing was written.") {
|
|
24
43
|
const byLower = new Map(cubeDimensions.map((d, i) => [d.toLowerCase(), i]));
|
|
25
44
|
const positions = [];
|
|
26
45
|
const unknown = [];
|
|
@@ -37,7 +56,7 @@ export function resolveCellAddress(cubeName, cubeDimensions, given) {
|
|
|
37
56
|
positions.push(pos ?? -1);
|
|
38
57
|
}
|
|
39
58
|
const missing = cubeDimensions.filter((_, i) => !seen.has(i));
|
|
40
|
-
const sandboxPos = cubeDimensions
|
|
59
|
+
const sandboxPos = sandboxPosition(cubeDimensions);
|
|
41
60
|
const onlySandboxMissing = missing.length === 1 && sandboxPos >= 0 && !seen.has(sandboxPos);
|
|
42
61
|
const problems = [];
|
|
43
62
|
if (unknown.length > 0)
|
|
@@ -49,7 +68,7 @@ export function resolveCellAddress(cubeName, cubeDimensions, given) {
|
|
|
49
68
|
if (problems.length > 0) {
|
|
50
69
|
throw new TM1Error({
|
|
51
70
|
code: TM1ErrorCode.VALIDATION_ERROR,
|
|
52
|
-
message: `Dimensions do not match cube '${cubeName}' — ${problems.join("; ")}.
|
|
71
|
+
message: `Dimensions do not match cube '${cubeName}' — ${problems.join("; ")}. ${outcome}`,
|
|
53
72
|
hint: `Name every dimension of the cube (any order): ${cubeDimensions.join(", ")}. A dimension left out is not ignored — TM1 writes into its default member.${sandboxPos >= 0 ? ` Only ${SANDBOX_DIMENSION} may be left out; it is then bound to ${BASE_SANDBOX}.` : ""}`,
|
|
54
73
|
details: JSON.stringify({ cubeDimensions, given }),
|
|
55
74
|
});
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* to be reusable later from a single-process pre-commit lint entry point.
|
|
8
8
|
*/
|
|
9
9
|
import { parseTiCode } from "../callgraph/tiParser.js";
|
|
10
|
+
import { TI_VAR } from "../ti-identifier.js";
|
|
10
11
|
const TABS = ["prolog", "metadata", "data", "epilog"];
|
|
11
12
|
/**
|
|
12
13
|
* Destructive ops that wipe data/objects. Unconditional use (no enclosing If)
|
|
@@ -84,7 +85,8 @@ function hasSideEffectRhs(expr) {
|
|
|
84
85
|
}
|
|
85
86
|
return false;
|
|
86
87
|
}
|
|
87
|
-
|
|
88
|
+
// Lookarounds instead of \b: \b does not fire after a trailing `.`/`$`/`%`.
|
|
89
|
+
const IDENTIFIER_RE = new RegExp(`(?<![\\w.$%\`])(${TI_VAR})`, "g");
|
|
88
90
|
const EXPAND_VAR_RE = /%([A-Za-z_]\w*)%/g;
|
|
89
91
|
/** Extract all potential variable-read tokens from a raw TI expression/arg/condition string. */
|
|
90
92
|
function collectReads(text, out) {
|
|
@@ -16,13 +16,14 @@
|
|
|
16
16
|
* directives). It is intentionally fuzzy — there is no parser that can tell a
|
|
17
17
|
* disabled statement from prose with certainty.
|
|
18
18
|
*/
|
|
19
|
+
import { TI_VAR } from "../ti-identifier.js";
|
|
19
20
|
const CODE_SIGNALS = [
|
|
20
21
|
// Statement terminator at end of line — the strongest TM1 signal.
|
|
21
22
|
/;\s*$/,
|
|
22
23
|
// Rule-area assignment: `['Sales'] = ...`
|
|
23
24
|
/^\[[^\]]*\]\s*=/,
|
|
24
25
|
// Variable / cube-cell assignment: `vX = ...` or `nResult=...` (not `==`).
|
|
25
|
-
|
|
26
|
+
new RegExp(`^${TI_VAR}\\s*=(?!=)`),
|
|
26
27
|
// Known TM1 function call: `Name(` (case-insensitive — TM1 is).
|
|
27
28
|
/\b(?:CellPutN|CellPutS|CellGetN|CellGetS|CellIsUpdateable|DB|DBRW|DBRA|DBSW|DBSS|AttrPutN|AttrPutS|AttrN|AttrS|ElementName|ElementIndex|DimensionElementInsert|DimensionElementComponentAdd|HierarchyElementInsert|ExecuteProcess|ExecuteCommand|RunProcess|ProcessQuit|ProcessBreak|ItemSkip|ItemReject|ViewZeroOut|CubeClearData|CubeSetLogChanges|ASCIIOutput|TextOutput|SubsetCreate|NumberToString|StringToNumber)\s*\(/i,
|
|
28
29
|
// Control-flow keyword immediately followed by a paren: `IF(`, `WHILE (`.
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// cube's dimension order, names the uncovered positions, and `details` pairs
|
|
10
10
|
// every dimension with the element that landed on it.
|
|
11
11
|
import { TM1Error, TM1ErrorCode } from "../types.js";
|
|
12
|
+
import { BASE_SANDBOX, SANDBOX_DIMENSION, sandboxPosition, } from "./cell-address.js";
|
|
12
13
|
export function dimensionCountMismatch(cubeName, dimensions, elements) {
|
|
13
14
|
const missing = dimensions.slice(elements.length);
|
|
14
15
|
const extra = elements.slice(dimensions.length);
|
|
@@ -19,7 +20,10 @@ export function dimensionCountMismatch(cubeName, dimensions, elements) {
|
|
|
19
20
|
code: TM1ErrorCode.VALIDATION_ERROR,
|
|
20
21
|
message: `Cube '${cubeName}' has ${dimensions.length} dimensions but ${elements.length} element(s) were given.` +
|
|
21
22
|
gap,
|
|
22
|
-
hint: `Pass exactly one element per dimension, in this order: ${dimensions.join(", ")}. A forgotten dimension anywhere in the list shifts every later element onto the wrong one — check the pairing in details
|
|
23
|
+
hint: `Pass exactly one element per dimension, in this order: ${dimensions.join(", ")}. A forgotten dimension anywhere in the list shifts every later element onto the wrong one — check the pairing in details.` +
|
|
24
|
+
(sandboxPosition(dimensions) >= 0
|
|
25
|
+
? ` Only ${SANDBOX_DIMENSION} may be left out; it is then bound to ${BASE_SANDBOX}.`
|
|
26
|
+
: ""),
|
|
23
27
|
details: JSON.stringify(dimensions.map((dimension, i) => ({
|
|
24
28
|
dimension,
|
|
25
29
|
element: elements[i] ?? null,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { tm1NameKey } from "../tm1-name.js";
|
|
1
2
|
const VALID_TYPES = new Set(["Numeric", "Consolidated", "String"]);
|
|
2
3
|
/**
|
|
3
4
|
* Consecutive failed loads of one `(dim, hier)` slot before the cache stops
|
|
@@ -15,9 +16,6 @@ const VALID_TYPES = new Set(["Numeric", "Consolidated", "String"]);
|
|
|
15
16
|
* clock buys nothing and would only drag `Date.now()` into the tests.
|
|
16
17
|
*/
|
|
17
18
|
const MAX_CONSECUTIVE_LOAD_FAILURES = 3;
|
|
18
|
-
function normalize(s) {
|
|
19
|
-
return s.toLowerCase();
|
|
20
|
-
}
|
|
21
19
|
export class ElementTypeCache {
|
|
22
20
|
hierarchy;
|
|
23
21
|
slots = new Map();
|
|
@@ -28,14 +26,14 @@ export class ElementTypeCache {
|
|
|
28
26
|
this.hierarchy = hierarchy;
|
|
29
27
|
}
|
|
30
28
|
async getType(dim, hier, elem) {
|
|
31
|
-
const slotKey = `${
|
|
29
|
+
const slotKey = `${tm1NameKey(dim)}|${tm1NameKey(hier)}`;
|
|
32
30
|
let slot = this.slots.get(slotKey);
|
|
33
31
|
if (slot === undefined) {
|
|
34
32
|
slot = await this.load(slotKey, dim, hier);
|
|
35
33
|
}
|
|
36
34
|
if (slot === null)
|
|
37
35
|
return null;
|
|
38
|
-
return slot.get(
|
|
36
|
+
return slot.get(tm1NameKey(elem)) ?? null;
|
|
39
37
|
}
|
|
40
38
|
async load(slotKey, dim, hier) {
|
|
41
39
|
const inflight = this.pending.get(slotKey);
|
|
@@ -55,7 +53,7 @@ export class ElementTypeCache {
|
|
|
55
53
|
const m = new Map();
|
|
56
54
|
for (const e of elements) {
|
|
57
55
|
if (VALID_TYPES.has(e.type)) {
|
|
58
|
-
m.set(
|
|
56
|
+
m.set(tm1NameKey(e.name), e.type);
|
|
59
57
|
}
|
|
60
58
|
}
|
|
61
59
|
this.slots.set(slotKey, m);
|
|
@@ -21,10 +21,11 @@
|
|
|
21
21
|
// emitted expression in TypeScript so the property can be tested directly;
|
|
22
22
|
// tests/unit/naming-odata-filter.test.ts asserts it over every rule class,
|
|
23
23
|
// and the live suite asserts that TM1 agrees.
|
|
24
|
+
import { escapeOdataLiteral } from "../../tm1-client/services/odata-page.js";
|
|
24
25
|
import { SERVER_RESERVED_CHARS } from "./rules.js";
|
|
25
26
|
/** OData string literal: single quotes double. */
|
|
26
27
|
function lit(value) {
|
|
27
|
-
return `'${value
|
|
28
|
+
return `'${escapeOdataLiteral(value)}'`;
|
|
28
29
|
}
|
|
29
30
|
/**
|
|
30
31
|
* `$filter` expression selecting elements that may violate a naming rule.
|
package/dist/lib/pro-parser.js
CHANGED
|
@@ -70,6 +70,9 @@ function parseSections(lines) {
|
|
|
70
70
|
"574": [],
|
|
71
71
|
"575": [],
|
|
72
72
|
};
|
|
73
|
+
// Sections read by their line count keep trailing blank lines and spaces:
|
|
74
|
+
// TM1 stores and returns them, so trimming made every re-import differ.
|
|
75
|
+
const counted = new Set();
|
|
73
76
|
let i = 0;
|
|
74
77
|
while (i < lines.length) {
|
|
75
78
|
const m = lines[i].match(SECTION_RE);
|
|
@@ -84,6 +87,7 @@ function parseSections(lines) {
|
|
|
84
87
|
// TM1 writes an explicit line count ("572,138"). Trust it, so code lines
|
|
85
88
|
// that look like a header (e.g. a literal "930,0") stay in the section.
|
|
86
89
|
const count = parseInt(rawCount, 10);
|
|
90
|
+
counted.add(code);
|
|
87
91
|
for (let taken = 0; taken < count && i < lines.length; taken++, i++) {
|
|
88
92
|
map[code].push(lines[i]);
|
|
89
93
|
}
|
|
@@ -96,11 +100,16 @@ function parseSections(lines) {
|
|
|
96
100
|
i++;
|
|
97
101
|
}
|
|
98
102
|
}
|
|
103
|
+
// A countless section runs to the next header, so its tail is ambiguous.
|
|
104
|
+
const text = (code) => {
|
|
105
|
+
const joined = map[code].join("\n");
|
|
106
|
+
return counted.has(code) ? joined : joined.trimEnd();
|
|
107
|
+
};
|
|
99
108
|
return {
|
|
100
|
-
prolog:
|
|
101
|
-
metadata:
|
|
102
|
-
data:
|
|
103
|
-
epilog:
|
|
109
|
+
prolog: text("572"),
|
|
110
|
+
metadata: text("573"),
|
|
111
|
+
data: text("574"),
|
|
112
|
+
epilog: text("575"),
|
|
104
113
|
};
|
|
105
114
|
}
|
|
106
115
|
function parseParameters(blocks) {
|
package/dist/lib/safe-regex.d.ts
CHANGED
|
@@ -1,15 +1,2 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Compile a user-supplied regex behind a ReDoS guard.
|
|
3
|
-
*
|
|
4
|
-
* Every `new RegExp(<user/LLM input>)` site must route through this helper:
|
|
5
|
-
* `safe-regex` rejects catastrophic-backtracking patterns (e.g. `(a+)+$`) before
|
|
6
|
-
* the pattern is ever constructed, so a malicious caller cannot freeze the Node
|
|
7
|
-
* event loop and DoS the server. Invalid patterns are normalized to a
|
|
8
|
-
* VALIDATION_ERROR instead of surfacing as a raw SyntaxError.
|
|
9
|
-
*
|
|
10
|
-
* @param pattern user-supplied regex source
|
|
11
|
-
* @param flags optional RegExp flags (e.g. "i", "gi")
|
|
12
|
-
* @param label human label for error messages (e.g. "nameRegex"); defaults to "regex"
|
|
13
|
-
*/
|
|
14
1
|
export declare function compileUserRegex(pattern: string, flags?: string, label?: string): RegExp;
|
|
15
2
|
//# sourceMappingURL=safe-regex.d.ts.map
|
package/dist/lib/safe-regex.js
CHANGED
|
@@ -1,25 +1,52 @@
|
|
|
1
1
|
import safeRegex from "safe-regex";
|
|
2
|
+
import regexpTree from "regexp-tree";
|
|
2
3
|
import { TM1Error, TM1ErrorCode } from "../types.js";
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* VALIDATION_ERROR instead of surfacing as a raw SyntaxError.
|
|
11
|
-
*
|
|
12
|
-
* @param pattern user-supplied regex source
|
|
13
|
-
* @param flags optional RegExp flags (e.g. "i", "gi")
|
|
14
|
-
* @param label human label for error messages (e.g. "nameRegex"); defaults to "regex"
|
|
5
|
+
* Unbounded repetition over a group that contains `|`, e.g. `(\w|\w)*` or
|
|
6
|
+
* `(a|ab)+`. safe-regex measures only star height, so it passes these, yet
|
|
7
|
+
* overlapping branches backtrack exponentially: `^(\w|\w)*!$` took 23.5 s on
|
|
8
|
+
* 30 characters and froze the event loop. Rejected as a class, since telling
|
|
9
|
+
* overlapping from disjoint branches is itself a hard problem; a character
|
|
10
|
+
* class (`[ab]+`) expresses the common intent without the risk.
|
|
15
11
|
*/
|
|
12
|
+
function hasRepeatedAlternation(pattern) {
|
|
13
|
+
let ast;
|
|
14
|
+
try {
|
|
15
|
+
// Parsing a RegExp object avoids re-escaping `/`; compiling never runs it.
|
|
16
|
+
ast = regexpTree.parse(new RegExp(pattern));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false; // new RegExp() below reports the syntax error
|
|
20
|
+
}
|
|
21
|
+
const containsDisjunction = (n) => {
|
|
22
|
+
if (!n || typeof n !== "object")
|
|
23
|
+
return false;
|
|
24
|
+
if (n.type === "Disjunction")
|
|
25
|
+
return true;
|
|
26
|
+
return Object.values(n).some(containsDisjunction);
|
|
27
|
+
};
|
|
28
|
+
const walk = (n) => {
|
|
29
|
+
if (!n || typeof n !== "object")
|
|
30
|
+
return false;
|
|
31
|
+
const node = n;
|
|
32
|
+
if (node.type === "Repetition" &&
|
|
33
|
+
node.quantifier &&
|
|
34
|
+
(node.quantifier.kind !== "Range" || node.quantifier.to === undefined) &&
|
|
35
|
+
node.quantifier.kind !== "?" &&
|
|
36
|
+
containsDisjunction(node.expression)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
return Object.values(node).some(walk);
|
|
40
|
+
};
|
|
41
|
+
return walk(ast);
|
|
42
|
+
}
|
|
16
43
|
export function compileUserRegex(pattern, flags, label = "regex") {
|
|
17
|
-
if (!safeRegex(pattern)) {
|
|
44
|
+
if (!safeRegex(pattern) || hasRepeatedAlternation(pattern)) {
|
|
18
45
|
throw new TM1Error({
|
|
19
46
|
code: TM1ErrorCode.VALIDATION_ERROR,
|
|
20
47
|
message: `${label} rejected: pattern risks catastrophic backtracking (ReDoS).`,
|
|
21
48
|
details: pattern,
|
|
22
|
-
hint: "Avoid nested unbounded quantifiers like (a+)+ or (.*)* —
|
|
49
|
+
hint: "Avoid nested unbounded quantifiers like (a+)+ or (.*)*, and repeating an alternation like (a|b)* — use a character class ([ab]*) or simplify the pattern.",
|
|
23
50
|
});
|
|
24
51
|
}
|
|
25
52
|
try {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MdxResult, CellValue } from "../types.js";
|
|
2
|
+
import { BASE_SANDBOX } from "./cell-address.js";
|
|
2
3
|
export type SampleCellFilter = string | string[];
|
|
3
4
|
export interface SampleCellsBuildArgs {
|
|
4
5
|
cubeName: string;
|
|
@@ -29,6 +30,11 @@ export interface SampleCellsBuildResult {
|
|
|
29
30
|
rowDims: string[];
|
|
30
31
|
/** Dimension placed on COLUMNS. */
|
|
31
32
|
columnDim: string;
|
|
33
|
+
/** Set when an unfiltered `Sandboxes` was pinned to `Base` in WHERE. */
|
|
34
|
+
sandboxPin?: {
|
|
35
|
+
dimension: string;
|
|
36
|
+
element: typeof BASE_SANDBOX;
|
|
37
|
+
};
|
|
32
38
|
}
|
|
33
39
|
export interface SampleCell {
|
|
34
40
|
coordinates: Record<string, string>;
|
package/dist/lib/sample-cells.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// No I/O — kept pure for unit-testability.
|
|
3
3
|
import { TM1Error, TM1ErrorCode } from "../types.js";
|
|
4
4
|
import { escapeMdxName } from "./mdx.js";
|
|
5
|
+
import { BASE_SANDBOX, SANDBOX_DIMENSION } from "./cell-address.js";
|
|
5
6
|
const memberRef = (dim, elem) => {
|
|
6
7
|
// Pre-qualified pass-through (e.g. caller passes "[Dim].[Elem]").
|
|
7
8
|
if (elem.startsWith("[") && elem.includes("].["))
|
|
@@ -36,10 +37,21 @@ export function buildSampleCellsMdx(args) {
|
|
|
36
37
|
const whereParts = [];
|
|
37
38
|
const rowDims = [];
|
|
38
39
|
const rowSets = [];
|
|
40
|
+
// An unfiltered Sandboxes is pinned to Base instead of crossjoined: TM1's
|
|
41
|
+
// NONEMPTY() returns an empty set whenever Sandboxes is part of the row set,
|
|
42
|
+
// even as {[Sandboxes].[Base]} alone, so every sample of a sandboxed cube
|
|
43
|
+
// came back empty. Measured on plapp-franz (EnableSandboxDimension=true).
|
|
44
|
+
// Base is also what every other read through this server sees.
|
|
45
|
+
let sandboxPin;
|
|
39
46
|
for (const dim of dimensions) {
|
|
40
47
|
if (dim === columnDim)
|
|
41
48
|
continue;
|
|
42
|
-
|
|
49
|
+
let f = filters[dim];
|
|
50
|
+
if (f === undefined &&
|
|
51
|
+
dim.toLowerCase() === SANDBOX_DIMENSION.toLowerCase()) {
|
|
52
|
+
f = BASE_SANDBOX;
|
|
53
|
+
sandboxPin = { dimension: dim, element: BASE_SANDBOX };
|
|
54
|
+
}
|
|
43
55
|
if (typeof f === "string") {
|
|
44
56
|
whereDims.push(dim);
|
|
45
57
|
whereParts.push(memberRef(dim, f));
|
|
@@ -103,7 +115,13 @@ export function buildSampleCellsMdx(args) {
|
|
|
103
115
|
}
|
|
104
116
|
const whereClause = whereParts.length > 0 ? ` WHERE (${whereParts.join(",")})` : "";
|
|
105
117
|
const mdx = `SELECT ${columnSet} ON COLUMNS, ${rowAxisExpr} ON ROWS FROM [${escapeMdxName(cubeName)}]${whereClause}`;
|
|
106
|
-
return {
|
|
118
|
+
return {
|
|
119
|
+
mdx,
|
|
120
|
+
whereDims,
|
|
121
|
+
rowDims,
|
|
122
|
+
columnDim,
|
|
123
|
+
...(sandboxPin ? { sandboxPin } : {}),
|
|
124
|
+
};
|
|
107
125
|
}
|
|
108
126
|
/**
|
|
109
127
|
* Convert flat MdxResult cells into self-describing {coordinates,value,formattedValue}.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A TI variable name as TM1 accepts it. Besides letters, digits and `_`,
|
|
3
|
+
* the compiler takes `.`, `$`, `%` and backtick (measured on 11.8 and 12.5:
|
|
4
|
+
* `v.A = 1;`, `v$A`, `v%A` and `` v`A `` all compile), and ODBC columns
|
|
5
|
+
* carry such names into data-source variables. A plain `\w` identifier made
|
|
6
|
+
* the parser reject those lines, and every analysis then saw an empty tab.
|
|
7
|
+
*
|
|
8
|
+
* Function names stay `\w`-only; this is for variables.
|
|
9
|
+
*/
|
|
10
|
+
export declare const TI_VAR = "[A-Za-z_][\\w.$%`]*";
|
|
11
|
+
//# sourceMappingURL=ti-identifier.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A TI variable name as TM1 accepts it. Besides letters, digits and `_`,
|
|
3
|
+
* the compiler takes `.`, `$`, `%` and backtick (measured on 11.8 and 12.5:
|
|
4
|
+
* `v.A = 1;`, `v$A`, `v%A` and `` v`A `` all compile), and ODBC columns
|
|
5
|
+
* carry such names into data-source variables. A plain `\w` identifier made
|
|
6
|
+
* the parser reject those lines, and every analysis then saw an empty tab.
|
|
7
|
+
*
|
|
8
|
+
* Function names stay `\w`-only; this is for variables.
|
|
9
|
+
*/
|
|
10
|
+
export const TI_VAR = "[A-Za-z_][\\w.$%`]*";
|
|
11
|
+
//# sourceMappingURL=ti-identifier.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TM1 name identity: the server treats `Total Year`, `totalyear` and
|
|
3
|
+
* `TOTAL YEAR` as the same object (case- and space-insensitive), so any
|
|
4
|
+
* client-side lookup against server names must key on this, not on
|
|
5
|
+
* `toLowerCase()` alone. Mirrors tm1py's `lower_and_drop_spaces`.
|
|
6
|
+
*/
|
|
7
|
+
export declare const tm1NameKey: (name: string) => string;
|
|
8
|
+
export declare const tm1NameEquals: (a: string, b: string) => boolean;
|
|
9
|
+
//# sourceMappingURL=tm1-name.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TM1 name identity: the server treats `Total Year`, `totalyear` and
|
|
3
|
+
* `TOTAL YEAR` as the same object (case- and space-insensitive), so any
|
|
4
|
+
* client-side lookup against server names must key on this, not on
|
|
5
|
+
* `toLowerCase()` alone. Mirrors tm1py's `lower_and_drop_spaces`.
|
|
6
|
+
*/
|
|
7
|
+
export const tm1NameKey = (name) => name.toLowerCase().replace(/ /g, "");
|
|
8
|
+
export const tm1NameEquals = (a, b) => tm1NameKey(a) === tm1NameKey(b);
|
|
9
|
+
//# sourceMappingURL=tm1-name.js.map
|
|
@@ -230,8 +230,8 @@ export const V12_DEPRECATED_TI = new Map([
|
|
|
230
230
|
{
|
|
231
231
|
name: "SetOdbcUnicodeInterface",
|
|
232
232
|
severity: "error",
|
|
233
|
-
issue: "
|
|
234
|
-
suggestion: "
|
|
233
|
+
issue: "SetODBCUnicodeInterface does not exist in v12 (compile fails); ODBCOpen/ODBCOutput/ODBCClose still compile",
|
|
234
|
+
suggestion: "Remove the call",
|
|
235
235
|
},
|
|
236
236
|
{
|
|
237
237
|
name: "SwapAliasWithPrincipalName",
|
package/dist/session-manager.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createLogger } from "./logger.js";
|
|
|
2
2
|
import { createConnectionProfile, } from "./tm1-client/connection/profile.js";
|
|
3
3
|
import { getTm1Dispatcher, tm1Fetch } from "./tm1-client/dispatcher.js";
|
|
4
4
|
import { PRODUCT, VERSION } from "./version.js";
|
|
5
|
+
import { TM1Error, TM1ErrorCode } from "./types.js";
|
|
5
6
|
const USER_AGENT = `${PRODUCT}/${VERSION}`;
|
|
6
7
|
export class TimeoutError extends Error {
|
|
7
8
|
timeoutMs;
|
|
@@ -31,6 +32,13 @@ export class SessionManager {
|
|
|
31
32
|
sessionCookie = null;
|
|
32
33
|
authInFlight = null;
|
|
33
34
|
keepAliveTimer = null;
|
|
35
|
+
// Set once TM1 rejects the configured credentials (401/403 on login).
|
|
36
|
+
// Credentials come from env and cannot change without a restart, so every
|
|
37
|
+
// later attempt would be rejected the same way — and each one counts
|
|
38
|
+
// against MaximumLoginAttempts. Latching here keeps one failed login from
|
|
39
|
+
// becoming dozens (fan-out tools, the 401 retry, the keep-alive timer)
|
|
40
|
+
// and locking the account.
|
|
41
|
+
rejectedLogin = null;
|
|
34
42
|
config;
|
|
35
43
|
logger;
|
|
36
44
|
profile;
|
|
@@ -61,6 +69,9 @@ export class SessionManager {
|
|
|
61
69
|
* requests are using — the "staggered-401 re-auth churn" cascade.
|
|
62
70
|
*/
|
|
63
71
|
async authenticate(staleCookie) {
|
|
72
|
+
if (this.rejectedLogin) {
|
|
73
|
+
throw this.rejectedLogin;
|
|
74
|
+
}
|
|
64
75
|
if (staleCookie !== undefined &&
|
|
65
76
|
this.sessionCookie !== null &&
|
|
66
77
|
this.sessionCookie !== staleCookie) {
|
|
@@ -105,6 +116,19 @@ export class SessionManager {
|
|
|
105
116
|
}));
|
|
106
117
|
// Always consume body to release connection
|
|
107
118
|
await response.text();
|
|
119
|
+
if (response.status === 401 || response.status === 403) {
|
|
120
|
+
this.rejectedLogin = new TM1Error({
|
|
121
|
+
code: TM1ErrorCode.AUTH_FAILED,
|
|
122
|
+
message: `Authentication failed with status ${response.status}: ${response.statusText}. ` +
|
|
123
|
+
"No further login attempts are made, so the account is not locked out; " +
|
|
124
|
+
"fix the credentials and restart the MCP server.",
|
|
125
|
+
httpStatus: response.status,
|
|
126
|
+
endpoint: loginReq.url,
|
|
127
|
+
});
|
|
128
|
+
this.logger.error({ endpoint: loginReq.url, status: response.status }, "Authentication rejected; login attempts stopped until restart");
|
|
129
|
+
this.stopKeepAlive();
|
|
130
|
+
throw this.rejectedLogin;
|
|
131
|
+
}
|
|
108
132
|
if (!response.ok) {
|
|
109
133
|
this.logger.error({ endpoint: loginReq.url, status: response.status }, "Authentication failed");
|
|
110
134
|
throw new Error(`Authentication failed with status ${response.status}: ${response.statusText}`);
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
// then URL-encode. Mirrors the `enc` helper used across the service layer.
|
|
3
|
-
const enc = (s) => encodeURIComponent(String(s).replace(/'/g, "''"));
|
|
1
|
+
import { odataKey } from "../services/odata-page.js";
|
|
4
2
|
function buildBasicToken(user, password) {
|
|
5
3
|
return "Basic " + Buffer.from(`${user}:${password}`).toString("base64");
|
|
6
4
|
}
|
|
@@ -79,7 +77,7 @@ function createV12Profile(config) {
|
|
|
79
77
|
const database = config.database ?? "";
|
|
80
78
|
// encodeURIComponent (not `enc`) for the instance: it's a bare path segment,
|
|
81
79
|
// not an OData quoted key — no apostrophe-doubling needed, just percent-encoding.
|
|
82
|
-
const dbRoot = `/${encodeURIComponent(instance)}/api/v1/Databases('${
|
|
80
|
+
const dbRoot = `/${encodeURIComponent(instance)}/api/v1/Databases('${odataKey(database)}')`;
|
|
83
81
|
return {
|
|
84
82
|
// Replacement FUNCTION, not a string: String.replace treats "$&"/"$$"/"$1"
|
|
85
83
|
// in a string replacement specially, which would corrupt dbRoot if instance
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Agent } from "undici";
|
|
2
2
|
import type { TM1Config } from "../config.js";
|
|
3
|
-
export declare function getTm1Dispatcher(config: TM1Config): Agent
|
|
3
|
+
export declare function getTm1Dispatcher(config: TM1Config): Agent;
|
|
4
4
|
export declare function tm1Fetch(url: string, init: RequestInit): Promise<Response>;
|
|
5
5
|
//# sourceMappingURL=dispatcher.d.ts.map
|