@ffschrattenecker/tm1-mcp-server 7.0.2 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +83 -1
- package/README.md +5 -5
- package/dist/config.js +34 -17
- package/dist/connections.d.ts +18 -1
- package/dist/connections.js +34 -18
- package/dist/http-transport.js +51 -4
- package/dist/lib/callgraph/referenceIndex.js +2 -1
- package/dist/lib/callgraph/rulesLinter.d.ts +0 -19
- package/dist/lib/callgraph/rulesLinter.js +0 -590
- package/dist/lib/callgraph/tiParser.js +10 -5
- package/dist/lib/callgraph/tm1-adapter.d.ts +5 -1
- package/dist/lib/callgraph/tm1-adapter.js +41 -9
- package/dist/lib/callgraph/variableEnv.js +3 -2
- package/dist/lib/cell-address.d.ts +1 -1
- package/dist/lib/cell-address.js +2 -2
- package/dist/lib/complexity/antipatterns.js +3 -1
- package/dist/lib/complexity/comment-classifier.js +2 -1
- package/dist/lib/feeders/element-type-cache.js +4 -6
- package/dist/lib/naming/odata-filter.js +2 -1
- package/dist/lib/pro-parser.js +13 -4
- package/dist/lib/safe-regex.d.ts +0 -13
- package/dist/lib/safe-regex.js +40 -13
- package/dist/lib/ti-identifier.d.ts +11 -0
- package/dist/lib/ti-identifier.js +11 -0
- package/dist/lib/tm1-name.d.ts +9 -0
- package/dist/lib/tm1-name.js +9 -0
- package/dist/lib/v12-compat/deprecated-ti.js +2 -2
- package/dist/session-manager.d.ts +1 -0
- package/dist/session-manager.js +24 -0
- package/dist/tm1-client/connection/profile.js +2 -4
- package/dist/tm1-client/dispatcher.d.ts +1 -1
- package/dist/tm1-client/dispatcher.js +18 -6
- package/dist/tm1-client/http.d.ts +21 -0
- package/dist/tm1-client/http.js +39 -30
- package/dist/tm1-client/services/cell-service.d.ts +9 -7
- package/dist/tm1-client/services/cell-service.js +51 -25
- package/dist/tm1-client/services/chore-service.js +8 -9
- package/dist/tm1-client/services/cube-service.d.ts +14 -18
- package/dist/tm1-client/services/cube-service.js +51 -45
- package/dist/tm1-client/services/dimension-order.d.ts +13 -0
- package/dist/tm1-client/services/dimension-order.js +42 -0
- package/dist/tm1-client/services/dimension-service.js +4 -6
- package/dist/tm1-client/services/element-service.d.ts +35 -11
- package/dist/tm1-client/services/element-service.js +124 -60
- package/dist/tm1-client/services/file-service.d.ts +42 -6
- package/dist/tm1-client/services/file-service.js +225 -28
- package/dist/tm1-client/services/hierarchy-service.d.ts +19 -9
- package/dist/tm1-client/services/hierarchy-service.js +118 -12
- package/dist/tm1-client/services/monitoring-service.js +2 -5
- package/dist/tm1-client/services/odata-page.d.ts +6 -0
- package/dist/tm1-client/services/odata-page.js +8 -0
- package/dist/tm1-client/services/process-service.d.ts +1 -1
- package/dist/tm1-client/services/process-service.js +26 -26
- package/dist/tm1-client/services/security-service.js +8 -9
- package/dist/tm1-client/services/server-service.js +14 -15
- package/dist/tm1-client/services/subset-service.d.ts +23 -10
- package/dist/tm1-client/services/subset-service.js +82 -26
- package/dist/tm1-client/services/view-service.js +12 -13
- package/dist/tm1-client.js +5 -2
- package/dist/tools/analysis/check-v12-readiness.js +3 -3
- package/dist/tools/celldata/check-feeders.js +6 -6
- package/dist/tools/celldata/check-writable-coords.js +33 -22
- package/dist/tools/celldata/execute-mdx.js +10 -9
- package/dist/tools/celldata/get-view.js +10 -9
- package/dist/tools/celldata/member-ref.d.ts +15 -0
- package/dist/tools/celldata/member-ref.js +67 -0
- package/dist/tools/celldata/trace-cell-calculation.js +6 -6
- package/dist/tools/celldata/trace-feeders.js +6 -6
- package/dist/tools/celldata/write-cells.js +62 -12
- package/dist/tools/dimension-management/create-element-attribute.js +1 -1
- package/dist/tools/dimension-management/delete-hierarchy.js +3 -4
- package/dist/tools/dimension-management/get-element-attribute-values.js +7 -2
- package/dist/tools/dimension-management/list-element-attributes.js +1 -1
- package/dist/tools/dimension-management/update-element-attribute-value.js +8 -3
- package/dist/tools/dimension-management/update-element.js +14 -7
- package/dist/tools/fileops/container.d.ts +8 -0
- package/dist/tools/fileops/container.js +11 -0
- package/dist/tools/fileops/delete-file.js +4 -2
- package/dist/tools/fileops/get-file-content.js +45 -7
- package/dist/tools/fileops/list-files.js +5 -2
- package/dist/tools/fileops/search-files.js +4 -1
- package/dist/tools/fileops/upload-file.js +5 -2
- package/dist/tools/index.js +0 -2
- package/dist/tools/model-building/check-cube-rule.js +3 -2
- package/dist/tools/model-building/clear-cube.js +35 -26
- package/dist/tools/model-building/set-cube-rules.js +13 -20
- package/dist/tools/model-building/unload-cube.js +1 -1
- package/dist/tools/operations/get-cube-stats.js +1 -1
- package/dist/tools/operations/get-transaction-log.js +1 -1
- package/dist/tools/operations/list-error-logs.js +27 -2
- package/dist/tools/schemas/items-fileops.d.ts +4 -0
- package/dist/tools/schemas/items-fileops.js +1 -0
- package/dist/tools/security/list-clients.js +5 -5
- package/dist/tools/security/list-groups.js +1 -1
- package/dist/tools/subsets/create-subset.js +9 -5
- package/dist/tools/subsets/delete-subset.js +8 -3
- package/dist/tools/subsets/update-subset.js +9 -9
- package/dist/tools/ti-development/diff-process-with-file.js +4 -29
- package/dist/tools/ti-development/diff-processes.js +9 -0
- package/dist/tools/ti-development/export-process-to-git.js +9 -7
- package/dist/tools/ti-development/export-process-to-pro.js +15 -8
- package/dist/tools/ti-development/import-pro-file.js +10 -21
- package/dist/tools/ti-development/import-process-from-git.js +8 -18
- package/dist/tools/ti-development/install-pro-bundle.js +2 -2
- package/dist/tools/ti-development/upsert-process.js +26 -5
- package/npm-shrinkwrap.json +10 -9
- package/package.json +9 -5
- package/dist/tools/dimension-management/move-element.d.ts +0 -2
- package/dist/tools/dimension-management/move-element.js +0 -28
|
@@ -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)
|
|
@@ -18,5 +18,5 @@ export interface ResolvedAddress {
|
|
|
18
18
|
/** Set when `Sandboxes` was left out and bound to `Base`. */
|
|
19
19
|
sandboxDefaulted?: typeof BASE_SANDBOX;
|
|
20
20
|
}
|
|
21
|
-
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;
|
|
22
22
|
//# sourceMappingURL=cell-address.d.ts.map
|
package/dist/lib/cell-address.js
CHANGED
|
@@ -39,7 +39,7 @@ export function bindLeftOutSandbox(cubeDimensions, elements) {
|
|
|
39
39
|
return undefined;
|
|
40
40
|
return [...elements.slice(0, pos), BASE_SANDBOX, ...elements.slice(pos)];
|
|
41
41
|
}
|
|
42
|
-
export function resolveCellAddress(cubeName, cubeDimensions, given) {
|
|
42
|
+
export function resolveCellAddress(cubeName, cubeDimensions, given, outcome = "Nothing was written.") {
|
|
43
43
|
const byLower = new Map(cubeDimensions.map((d, i) => [d.toLowerCase(), i]));
|
|
44
44
|
const positions = [];
|
|
45
45
|
const unknown = [];
|
|
@@ -68,7 +68,7 @@ export function resolveCellAddress(cubeName, cubeDimensions, given) {
|
|
|
68
68
|
if (problems.length > 0) {
|
|
69
69
|
throw new TM1Error({
|
|
70
70
|
code: TM1ErrorCode.VALIDATION_ERROR,
|
|
71
|
-
message: `Dimensions do not match cube '${cubeName}' — ${problems.join("; ")}.
|
|
71
|
+
message: `Dimensions do not match cube '${cubeName}' — ${problems.join("; ")}. ${outcome}`,
|
|
72
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}.` : ""}`,
|
|
73
73
|
details: JSON.stringify({ cubeDimensions, given }),
|
|
74
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 (`.
|
|
@@ -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 {
|
|
@@ -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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
if (!
|
|
14
|
-
|
|
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
|
|
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).
|