@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,3 +1,7 @@
|
|
|
1
|
+
import { TI_VAR } from "../ti-identifier.js";
|
|
2
|
+
const ASSIGN_RE = new RegExp(`^(${TI_VAR})\\s*=\\s*(.+?)\\s*;?\\s*$`);
|
|
3
|
+
const EMPTY_ASSIGN_RE = new RegExp(`^${TI_VAR}\\s*=\\s*;$`);
|
|
4
|
+
const ASSIGN_LIKE_RE = new RegExp(`^${TI_VAR}\\s*=\\s*.+$`);
|
|
1
5
|
/**
|
|
2
6
|
* Parses TI (TurboIntegrator) source code into an AST.
|
|
3
7
|
*
|
|
@@ -141,8 +145,9 @@ function parseBlock(lines, startIndex, terminator) {
|
|
|
141
145
|
}
|
|
142
146
|
const upper = trimmed.toUpperCase();
|
|
143
147
|
const lineNum = i + 1; // 1-based line numbers
|
|
144
|
-
// Check for double semicolon
|
|
145
|
-
|
|
148
|
+
// Check for double semicolon outside string literals (TM1 accepts
|
|
149
|
+
// `sQ = 'a;;b';`; `''` inside a literal splits it harmlessly)
|
|
150
|
+
if (trimmed.replace(/'[^']*'/g, "''").includes(";;")) {
|
|
146
151
|
throw new ParseError(lineNum, `Double semicolon in line ${lineNum}: each statement needs exactly one semicolon`);
|
|
147
152
|
}
|
|
148
153
|
// Check for block terminators
|
|
@@ -195,12 +200,12 @@ function parseBlock(lines, startIndex, terminator) {
|
|
|
195
200
|
}
|
|
196
201
|
// Assignment: variable = expression;
|
|
197
202
|
// Check for empty right-hand side: y =; or y = ;
|
|
198
|
-
if (
|
|
203
|
+
if (EMPTY_ASSIGN_RE.test(trimmed)) {
|
|
199
204
|
const varName = (trimmed.split(/\s*=/)[0] ?? "").trim();
|
|
200
205
|
throw new ParseError(lineNum, `Empty assignment in line ${lineNum}: "${varName}" has no value (e.g. ${varName} = 1; or ${varName} = 'text';)`);
|
|
201
206
|
}
|
|
202
207
|
// First check if line looks like an assignment but is missing semicolon
|
|
203
|
-
if (
|
|
208
|
+
if (ASSIGN_LIKE_RE.test(trimmed) && !trimmed.endsWith(";")) {
|
|
204
209
|
const upperFirst = (trimmed.split(/[\s=(]/)[0] ?? "").toUpperCase();
|
|
205
210
|
if (!["IF", "ELSEIF", "ELSE", "ENDIF", "WHILE", "END"].includes(upperFirst)) {
|
|
206
211
|
throw new ParseError(lineNum, `Missing semicolon at end of line ${lineNum}`);
|
|
@@ -359,7 +364,7 @@ function tryParseAssignment(line, lineNum) {
|
|
|
359
364
|
// Match: identifier = expression;
|
|
360
365
|
// The variable name can contain letters, digits, underscores
|
|
361
366
|
// We need to be careful not to match == (comparison)
|
|
362
|
-
const match = line.match(
|
|
367
|
+
const match = line.match(ASSIGN_RE);
|
|
363
368
|
if (!match)
|
|
364
369
|
return null;
|
|
365
370
|
const variable = match[1];
|
|
@@ -15,7 +15,11 @@ export interface BuildIndexOpts {
|
|
|
15
15
|
/** Force a fresh fetch and ignore cache. */
|
|
16
16
|
bypassCache?: boolean;
|
|
17
17
|
}
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Drop cached indexes for one connection, or for all of them when
|
|
20
|
+
* `connectionId` is omitted (the explicit tm1_invalidate_callgraph_cache tool).
|
|
21
|
+
*/
|
|
22
|
+
export declare function invalidateCallgraphCache(connectionId?: string): {
|
|
19
23
|
cleared: number;
|
|
20
24
|
};
|
|
21
25
|
export declare function getCallgraphCacheStats(): Array<{
|
|
@@ -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 {
|
package/dist/lib/sample-cells.js
CHANGED
|
@@ -40,7 +40,7 @@ export function buildSampleCellsMdx(args) {
|
|
|
40
40
|
// An unfiltered Sandboxes is pinned to Base instead of crossjoined: TM1's
|
|
41
41
|
// NONEMPTY() returns an empty set whenever Sandboxes is part of the row set,
|
|
42
42
|
// even as {[Sandboxes].[Base]} alone, so every sample of a sandboxed cube
|
|
43
|
-
// came back empty. Measured on
|
|
43
|
+
// came back empty. Measured on a server with EnableSandboxDimension=true.
|
|
44
44
|
// Base is also what every other read through this server sees.
|
|
45
45
|
let sandboxPin;
|
|
46
46
|
for (const dim of dimensions) {
|
|
@@ -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",
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type SecretStore } from "./secrets.js";
|
|
2
|
+
export interface CliIo {
|
|
3
|
+
out: (line: string) => void;
|
|
4
|
+
err: (line: string) => void;
|
|
5
|
+
/** Reads one secret without echoing it. */
|
|
6
|
+
readSecret: (prompt: string) => Promise<string>;
|
|
7
|
+
}
|
|
8
|
+
export declare function runSecretsCli(args: string[], env: NodeJS.ProcessEnv, io?: CliIo, store?: SecretStore): Promise<number>;
|
|
9
|
+
export declare function stripPipedInput(raw: string): string;
|
|
10
|
+
//# sourceMappingURL=secrets-cli.d.ts.map
|