@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,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { invalidateCallgraphCache } from "../../lib/callgraph/tm1-adapter.js";
|
|
3
|
-
import { TM1ErrorCode } from "../../types.js";
|
|
3
|
+
import { TM1Error, TM1ErrorCode } from "../../types.js";
|
|
4
4
|
import { withToolHint } from "../error-format.js";
|
|
5
5
|
import { actionResponse } from "../format.js";
|
|
6
6
|
import { CONFIRM_SCHEMA, requireConfirm } from "../confirm.js";
|
|
@@ -14,7 +14,8 @@ export const registerSetCubeRules = defineTool({
|
|
|
14
14
|
"Create or replace the rules for a TM1 cube.",
|
|
15
15
|
"SKIPCHECK; belongs at the top and FEEDERS; before all feeder definitions — SKIPCHECK is what makes feeders take effect, so rules with feeders need it.",
|
|
16
16
|
"Pass exactly one source: rules (the full text — replaces everything), edits (find/replace patch against the current text; each find must match exactly once, else nothing is written), or filePath (full text from a host file under TM1_LOCAL_FILE_ROOT).",
|
|
17
|
-
"The full resulting text is syntax-checked before anything is written
|
|
17
|
+
"The full resulting text is syntax-checked with tm1.CheckRules before anything is written: TM1 itself stores broken rules without an error, and they then silently compute nothing. Any error aborts the call with VALIDATION_ERROR, the errors with their line numbers in details, and nothing is written; preflight:false skips the check.",
|
|
18
|
+
"The stored text is read back after writing (verified.textMatches), so no separate tm1_get_cube_rules is needed; the callgraph cache is dropped automatically.",
|
|
18
19
|
],
|
|
19
20
|
annotations: IDEMPOTENT_DESTRUCTIVE,
|
|
20
21
|
output: MutationResultSchema,
|
|
@@ -45,23 +46,15 @@ export const registerSetCubeRules = defineTool({
|
|
|
45
46
|
if (preflight) {
|
|
46
47
|
const errors = await tm1Client.cubes.checkRule(cubeName, text);
|
|
47
48
|
if (errors.length > 0) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
message: `Preflight rule check failed: ${errors.length} error(s). Nothing was written.`,
|
|
58
|
-
hint: "Fix the lines in errors[] (lineNumber is in the full resulting text, after edits are applied). preflight:false skips the check; TM1 would store the broken text.",
|
|
59
|
-
errors,
|
|
60
|
-
}),
|
|
61
|
-
},
|
|
62
|
-
],
|
|
63
|
-
isError: true,
|
|
64
|
-
};
|
|
49
|
+
throw new TM1Error({
|
|
50
|
+
code: TM1ErrorCode.VALIDATION_ERROR,
|
|
51
|
+
message: `Rules for '${cubeName}' have ${errors.length} syntax error(s): ${errors
|
|
52
|
+
.slice(0, 5)
|
|
53
|
+
.map((e) => `line ${e.lineNumber ?? "?"}: ${e.message.trim()}`)
|
|
54
|
+
.join("; ")}. Nothing was written.`,
|
|
55
|
+
hint: "Fix the reported lines and retry (line numbers are in the full resulting text, after edits are applied). preflight:false writes the text anyway; TM1 stores it, but the broken statements compute nothing.",
|
|
56
|
+
details: JSON.stringify({ stage: "preflight", errors }),
|
|
57
|
+
});
|
|
65
58
|
}
|
|
66
59
|
}
|
|
67
60
|
await withToolHint(tm1Client.cubes.updateRules(cubeName, text), `Inspect details for the offending line.`);
|
|
@@ -78,7 +71,7 @@ export const registerSetCubeRules = defineTool({
|
|
|
78
71
|
}
|
|
79
72
|
const lineCount = text.split("\n").length;
|
|
80
73
|
// Rule changes shift call edges (DB(), feeders) — drop callgraph TTL early.
|
|
81
|
-
const { cleared: callgraphEntriesCleared } = invalidateCallgraphCache();
|
|
74
|
+
const { cleared: callgraphEntriesCleared } = invalidateCallgraphCache(tm1Client.connectionId);
|
|
82
75
|
return actionResponse({
|
|
83
76
|
success: true,
|
|
84
77
|
cubeName,
|
|
@@ -5,7 +5,7 @@ import { MutationResultSchema } from "../schemas/items.js";
|
|
|
5
5
|
import { defineTool } from "../define-tool.js";
|
|
6
6
|
export const registerUnloadCube = defineTool({
|
|
7
7
|
name: "tm1_unload_cube",
|
|
8
|
-
description: "Unload a cube from memory. TM1 discards the in-memory fed-cell index and reloads from
|
|
8
|
+
description: "Unload a cube from memory. TM1 saves the cube to disk first, then discards the in-memory fed-cell index and reloads from .cub on next access. Two reasons to call it: after feeder corrections, since the fed-cell index is cumulative and changes to existing feeders only take effect once the cube reloads; and to give the memory the cube occupies back to the server. TM1 v11 only.",
|
|
9
9
|
annotations: DESTRUCTIVE,
|
|
10
10
|
// v11 only. v12 answers tm1.Unload with "Demand load, loading and unloading
|
|
11
11
|
// of cubes is no longer supported." — the feature is gone, with no successor
|
|
@@ -12,7 +12,7 @@ export const registerGetCubeStats = defineTool({
|
|
|
12
12
|
"Read }StatsByCube metrics for one or more cubes (memory, populated cells, fed cells, feeder efficiency).",
|
|
13
13
|
"Well-known metrics are mapped to typed fields; the full element-name → value map is also returned under `raw` so server-side renames don't break the tool.",
|
|
14
14
|
"Per-cube errors are reported as items[].error without failing the whole call.",
|
|
15
|
-
"
|
|
15
|
+
"The }Stats* control cubes are not always there: v12 has none at all, and v11 keeps them only while statistics collection is switched on. That case, and an account not allowed to read them, return `statsUnavailable` {reason: absent|denied} instead of a raw error.",
|
|
16
16
|
],
|
|
17
17
|
annotations: READ_ONLY,
|
|
18
18
|
output: CubeStatsResultSchema,
|
|
@@ -5,7 +5,7 @@ import { TransactionLogEntrySchema } from "../schemas/items.js";
|
|
|
5
5
|
import { defineTool } from "../define-tool.js";
|
|
6
6
|
export const registerGetTransactionLog = defineTool({
|
|
7
7
|
name: "tm1_get_transaction_log",
|
|
8
|
-
description: "Fetch recent TM1 transaction log entries (cell writes), newest first. Optional filters: cube, user, and a since/until time range. NOTE: the endpoint scans the log server-side and a full scan can take minutes-to-hours. A cheap preflight probe fails fast on unreachable/no-rights; without `since` the server walks expanding time windows backward (10min→1y) and stops once `top` rows are found, so
|
|
8
|
+
description: "Fetch recent TM1 transaction log entries (cell writes), newest first. Optional filters: cube, user, and a since/until time range. NOTE: the endpoint scans the log server-side and a full scan can take minutes-to-hours. A cheap preflight probe fails fast on unreachable/no-rights; without `since` the server walks expanding time windows backward (10min→1y) and stops once `top` rows are found, so the lookback is bounded at one year rather than open-ended — a sparse log still walks all nine windows before it gives up. Pass since/until (from-to) to bound it explicitly. (v11 only)",
|
|
9
9
|
annotations: withVersion(READ_ONLY, "v11"),
|
|
10
10
|
// v12 deprecated TransactionLogEntry/TransactionLogEntries (and the
|
|
11
11
|
// TransactionLog / TailTransactionLog functions) in 12.0.0; all of them serve
|
|
@@ -8,11 +8,16 @@ import { pageShapeFor } from "../schemas/common.js";
|
|
|
8
8
|
// Best-effort extraction of {process, ts} from an error-log filename.
|
|
9
9
|
// Two known patterns (see server-service.listErrorLogFiles):
|
|
10
10
|
// modern v11: TM1ProcessError_<ts>_<id>_<proc>(_<hash>)?.log
|
|
11
|
+
// v12: ProcessLog_<ts>_<id>_<proc>.jsonl
|
|
11
12
|
// legacy: <proc>_<ts>.log
|
|
12
13
|
// Process names may contain underscores, so the modern parse greedily captures
|
|
13
14
|
// the tail and strips a trailing session-hash token (_<hex6+>). This is a
|
|
14
15
|
// heuristic — good enough for audit triage, not a guaranteed exact split.
|
|
15
16
|
export function parseLogName(filename) {
|
|
17
|
+
// v12 names carry no session hash, so nothing to strip.
|
|
18
|
+
const v12 = filename.match(/^ProcessLog_(\d{14})_\d+_(.+)\.jsonl$/i);
|
|
19
|
+
if (v12)
|
|
20
|
+
return { ts: v12[1], process: v12[2] };
|
|
16
21
|
const modern = filename.match(/^TM1ProcessError_(\d{14})_\d+_(.+)\.log$/i);
|
|
17
22
|
if (modern) {
|
|
18
23
|
// Strip the trailing TM1 session-hash token. Real-world v11 hashes are
|
|
@@ -62,12 +67,32 @@ export const registerListErrorLogs = defineTool({
|
|
|
62
67
|
"groupBy='process' returns a per-process audit summary instead of individual files.",
|
|
63
68
|
],
|
|
64
69
|
annotations: READ_ONLY,
|
|
65
|
-
output:
|
|
70
|
+
output: {
|
|
71
|
+
// Set only on the groupBy='process' branch, which wraps the page in the
|
|
72
|
+
// filters it applied and the totals before paging.
|
|
73
|
+
groupBy: z.literal("process").optional().describe("Echoes groupBy"),
|
|
74
|
+
processName: z
|
|
75
|
+
.string()
|
|
76
|
+
.optional()
|
|
77
|
+
.describe("Echoes the processName filter"),
|
|
78
|
+
since: z.string().optional().describe("Echoes the since filter"),
|
|
79
|
+
totalFiles: z
|
|
80
|
+
.number()
|
|
81
|
+
.int()
|
|
82
|
+
.optional()
|
|
83
|
+
.describe("Log files aggregated into the groups"),
|
|
84
|
+
groupCount: z
|
|
85
|
+
.number()
|
|
86
|
+
.int()
|
|
87
|
+
.optional()
|
|
88
|
+
.describe("Distinct processes before paging"),
|
|
89
|
+
...pageShapeFor(z.union([ErrorLogFileSchema, ErrorLogGroupSchema])),
|
|
90
|
+
},
|
|
66
91
|
input: {
|
|
67
92
|
processName: z
|
|
68
93
|
.string()
|
|
69
94
|
.optional()
|
|
70
|
-
.describe("Optional process-name filter — matches
|
|
95
|
+
.describe("Optional process-name filter — matches modern v11 'TM1ProcessError_<ts>_<id>_<processName>_<hash>.log', v12 'ProcessLog_<ts>_<id>_<processName>.jsonl' and legacy '<processName>_<ts>.log' filename patterns."),
|
|
71
96
|
since: z
|
|
72
97
|
.string()
|
|
73
98
|
.optional()
|
|
@@ -5,6 +5,10 @@ export declare const FileContentResultSchema: z.ZodObject<{
|
|
|
5
5
|
returnedBytes: z.ZodNumber;
|
|
6
6
|
truncated: z.ZodBoolean;
|
|
7
7
|
truncationReason: z.ZodOptional<z.ZodString>;
|
|
8
|
+
encoding: z.ZodEnum<{
|
|
9
|
+
text: "text";
|
|
10
|
+
base64: "base64";
|
|
11
|
+
}>;
|
|
8
12
|
content: z.ZodString;
|
|
9
13
|
}, z.core.$strip>;
|
|
10
14
|
export declare const FilenameItemSchema: z.ZodString;
|
|
@@ -6,6 +6,7 @@ export const FileContentResultSchema = z.object({
|
|
|
6
6
|
returnedBytes: z.number().int(),
|
|
7
7
|
truncated: z.boolean(),
|
|
8
8
|
truncationReason: z.string().optional(),
|
|
9
|
+
encoding: z.enum(["text", "base64"]),
|
|
9
10
|
content: z.string(),
|
|
10
11
|
});
|
|
11
12
|
// listFiles returns bare strings (file/folder names).
|
|
@@ -30,9 +30,9 @@ function project(clients, fields, groupCount) {
|
|
|
30
30
|
want.delete("groups");
|
|
31
31
|
}
|
|
32
32
|
return clients.map((c) => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
// Name is the item key and required by ClientSchema, so every projection
|
|
34
|
+
// carries it — fields=['type'] without it failed the output schema.
|
|
35
|
+
const out = { Name: c.Name };
|
|
36
36
|
if (want.has("friendlyName") && c.FriendlyName !== undefined)
|
|
37
37
|
out.FriendlyName = c.FriendlyName;
|
|
38
38
|
if (want.has("type") && c.Type !== undefined)
|
|
@@ -57,7 +57,7 @@ export const registerListClients = defineTool({
|
|
|
57
57
|
fields: z
|
|
58
58
|
.array(z.enum(FIELD_KEYS))
|
|
59
59
|
.optional()
|
|
60
|
-
.describe("Projection: subset of ['name','friendlyName','type','enabled','groups','groupCount']. Omit for full default payload."),
|
|
60
|
+
.describe("Projection: subset of ['name','friendlyName','type','enabled','groups','groupCount']. Name is always included. Omit for full default payload."),
|
|
61
61
|
groupCount: z
|
|
62
62
|
.boolean()
|
|
63
63
|
.optional()
|
|
@@ -77,7 +77,7 @@ export const registerListClients = defineTool({
|
|
|
77
77
|
header: "Groups",
|
|
78
78
|
get: (c) => c.groupCount !== undefined
|
|
79
79
|
? `${c.groupCount} (count)`
|
|
80
|
-
: (c.Groups ?? []).join(", "),
|
|
80
|
+
: (c.Groups ?? []).map((g) => g.Name).join(", "),
|
|
81
81
|
},
|
|
82
82
|
]);
|
|
83
83
|
return pageResponse(projectedPage, format, { title: "Clients", columns });
|
|
@@ -34,7 +34,7 @@ export const registerListGroups = defineTool({
|
|
|
34
34
|
header: "Clients",
|
|
35
35
|
get: (g) => "clientCount" in g
|
|
36
36
|
? `${g.clientCount} (count)`
|
|
37
|
-
: (g.Clients ?? []).join(", "),
|
|
37
|
+
: (g.Clients ?? []).map((c) => c.Name).join(", "),
|
|
38
38
|
},
|
|
39
39
|
]);
|
|
40
40
|
return pageResponse(projectedPage, format, { title: "Groups", columns });
|
|
@@ -6,7 +6,7 @@ import { defineTool } from "../define-tool.js";
|
|
|
6
6
|
import { HIERARCHY_NAME_OPTIONAL, resolveHierarchy } from "../hierarchy.js";
|
|
7
7
|
export const registerCreateSubset = defineTool({
|
|
8
8
|
name: "tm1_create_subset",
|
|
9
|
-
description: "Create a public
|
|
9
|
+
description: "Create a TM1 subset, public by default or private with isPrivate=true (private subsets belong to the signed-in user and are invisible to others; the same name may exist once public and once private). Provide either expression (MDX-based, dynamic) OR elements (static list) — not both. Optional alias attribute name controls the displayed alias.",
|
|
10
10
|
annotations: WRITE,
|
|
11
11
|
output: MutationResultSchema,
|
|
12
12
|
input: {
|
|
@@ -25,15 +25,19 @@ export const registerCreateSubset = defineTool({
|
|
|
25
25
|
.string()
|
|
26
26
|
.optional()
|
|
27
27
|
.describe("Alias attribute used as display name in the subset"),
|
|
28
|
+
isPrivate: z
|
|
29
|
+
.boolean()
|
|
30
|
+
.optional()
|
|
31
|
+
.default(false)
|
|
32
|
+
.describe("Use PrivateSubsets (the signed-in user's) instead of public Subsets"),
|
|
28
33
|
},
|
|
29
|
-
handler: async ({ dimensionName, hierarchyName, subsetName, expression, elements, alias }, tm1Client) => {
|
|
30
|
-
|
|
31
|
-
await tm1Client.subsets.create(dimensionName, hierarchy, {
|
|
34
|
+
handler: async ({ dimensionName, hierarchyName, subsetName, expression, elements, alias, isPrivate, }, tm1Client) => {
|
|
35
|
+
await tm1Client.subsets.create(dimensionName, resolveHierarchy(dimensionName, hierarchyName), {
|
|
32
36
|
name: subsetName,
|
|
33
37
|
expression,
|
|
34
38
|
elements,
|
|
35
39
|
alias,
|
|
36
|
-
});
|
|
40
|
+
}, isPrivate ?? false);
|
|
37
41
|
return actionResponse({
|
|
38
42
|
success: true,
|
|
39
43
|
subsetName,
|
|
@@ -7,19 +7,24 @@ import { defineTool } from "../define-tool.js";
|
|
|
7
7
|
import { HIERARCHY_NAME_OPTIONAL, resolveHierarchy } from "../hierarchy.js";
|
|
8
8
|
export const registerDeleteSubset = defineTool({
|
|
9
9
|
name: "tm1_delete_subset",
|
|
10
|
-
description: "Delete a public
|
|
10
|
+
description: "Delete a TM1 subset, public by default or private with isPrivate=true. TM1 refuses to delete a subset a view still uses (SubsetIsBeingUsedByView) — change it with tm1_update_subset instead. 404 if not found. Irreversible — pass confirm=<subset name verbatim>.",
|
|
11
11
|
annotations: DESTRUCTIVE,
|
|
12
12
|
output: MutationResultSchema,
|
|
13
13
|
input: {
|
|
14
14
|
dimensionName: z.string().describe("Dimension name"),
|
|
15
15
|
...HIERARCHY_NAME_OPTIONAL,
|
|
16
16
|
subsetName: z.string().describe("Subset to delete"),
|
|
17
|
+
isPrivate: z
|
|
18
|
+
.boolean()
|
|
19
|
+
.optional()
|
|
20
|
+
.default(false)
|
|
21
|
+
.describe("Use PrivateSubsets (the signed-in user's) instead of public Subsets"),
|
|
17
22
|
...CONFIRM_SCHEMA,
|
|
18
23
|
},
|
|
19
|
-
handler: async ({ dimensionName, hierarchyName, subsetName, confirm }, tm1Client) => {
|
|
24
|
+
handler: async ({ dimensionName, hierarchyName, subsetName, isPrivate, confirm }, tm1Client) => {
|
|
20
25
|
const hierarchy = resolveHierarchy(dimensionName, hierarchyName);
|
|
21
26
|
requireConfirm(confirm, subsetName, "subset");
|
|
22
|
-
await tm1Client.subsets.delete(dimensionName, hierarchy, subsetName);
|
|
27
|
+
await tm1Client.subsets.delete(dimensionName, hierarchy, subsetName, isPrivate ?? false);
|
|
23
28
|
return actionResponse({ success: true, subsetName });
|
|
24
29
|
},
|
|
25
30
|
});
|
|
@@ -6,7 +6,7 @@ import { defineTool } from "../define-tool.js";
|
|
|
6
6
|
import { HIERARCHY_NAME_OPTIONAL, resolveHierarchy } from "../hierarchy.js";
|
|
7
7
|
export const registerUpdateSubset = defineTool({
|
|
8
8
|
name: "tm1_update_subset",
|
|
9
|
-
description: "Update
|
|
9
|
+
description: "Update an existing TM1 subset in place, public by default or private with isPrivate=true. The way to change a subset a view uses, since TM1 will not delete that one. Pass expression to replace the MDX, OR elements to replace the static list (order kept; an MDX subset becomes static; [] empties it) — not both. Pass alias to change the alias attribute. If the new list names an unknown element, TM1 refuses it and the old definition is written back.",
|
|
10
10
|
annotations: IDEMPOTENT_WRITE,
|
|
11
11
|
output: MutationResultSchema,
|
|
12
12
|
input: {
|
|
@@ -17,16 +17,16 @@ export const registerUpdateSubset = defineTool({
|
|
|
17
17
|
elements: z
|
|
18
18
|
.array(z.string())
|
|
19
19
|
.optional()
|
|
20
|
-
.describe("New static element list
|
|
20
|
+
.describe("New static element list; replaces the old one and turns an MDX subset static. Mutually exclusive with expression."),
|
|
21
21
|
alias: z.string().optional().describe("New alias attribute"),
|
|
22
|
+
isPrivate: z
|
|
23
|
+
.boolean()
|
|
24
|
+
.optional()
|
|
25
|
+
.default(false)
|
|
26
|
+
.describe("Use PrivateSubsets (the signed-in user's) instead of public Subsets"),
|
|
22
27
|
},
|
|
23
|
-
handler: async ({ dimensionName, hierarchyName, subsetName, expression, elements, alias }, tm1Client) => {
|
|
24
|
-
|
|
25
|
-
await tm1Client.subsets.update(dimensionName, hierarchy, subsetName, {
|
|
26
|
-
expression,
|
|
27
|
-
elements,
|
|
28
|
-
alias,
|
|
29
|
-
});
|
|
28
|
+
handler: async ({ dimensionName, hierarchyName, subsetName, expression, elements, alias, isPrivate, }, tm1Client) => {
|
|
29
|
+
await tm1Client.subsets.update(dimensionName, resolveHierarchy(dimensionName, hierarchyName), subsetName, { expression, elements, alias }, isPrivate ?? false);
|
|
30
30
|
return actionResponse({ success: true, subsetName });
|
|
31
31
|
},
|
|
32
32
|
});
|
|
@@ -5,8 +5,9 @@ import { TM1Error, TM1ErrorCode } from "../../types.js";
|
|
|
5
5
|
import { parseProFile } from "../../lib/pro-parser.js";
|
|
6
6
|
import { ignoredColumnsOf } from "../../lib/variables-ui-data.js";
|
|
7
7
|
import { maskCode, resolveMaskSecrets } from "../../lib/mask-secrets.js";
|
|
8
|
+
import { diffDs } from "./diff-processes.js";
|
|
8
9
|
import { DiffProcessResultSchema } from "../schemas/items.js";
|
|
9
|
-
import { READ_ONLY
|
|
10
|
+
import { READ_ONLY } from "../annotations.js";
|
|
10
11
|
import { defineTool } from "../define-tool.js";
|
|
11
12
|
function tabDiff(name, installed, file) {
|
|
12
13
|
const norm = (s) => s.replace(/\r\n/g, "\n").trimEnd();
|
|
@@ -93,36 +94,10 @@ function diffIgnoredColumns(installed, file) {
|
|
|
93
94
|
renamed,
|
|
94
95
|
};
|
|
95
96
|
}
|
|
96
|
-
function diffDataSource(installed, file) {
|
|
97
|
-
const diffs = [];
|
|
98
|
-
if (installed.type !== file.type)
|
|
99
|
-
diffs.push(`type: ${installed.type} → ${file.type}`);
|
|
100
|
-
const fields = [
|
|
101
|
-
"dataSourceNameForServer",
|
|
102
|
-
"dataSourceNameForClient",
|
|
103
|
-
"asciiDelimiterChar",
|
|
104
|
-
"asciiQuoteCharacter",
|
|
105
|
-
"asciiDecimalSeparator",
|
|
106
|
-
"asciiThousandSeparator",
|
|
107
|
-
"asciiHeaderRecords",
|
|
108
|
-
"view",
|
|
109
|
-
"subset",
|
|
110
|
-
"userName",
|
|
111
|
-
// See diff-processes.ts: the ODBC query is substance, the password is noise.
|
|
112
|
-
"query",
|
|
113
|
-
];
|
|
114
|
-
for (const f of fields) {
|
|
115
|
-
const a = installed[f];
|
|
116
|
-
const b = file[f];
|
|
117
|
-
if ((a ?? "") !== (b ?? ""))
|
|
118
|
-
diffs.push(`${String(f)}: ${JSON.stringify(a)} → ${JSON.stringify(b)}`);
|
|
119
|
-
}
|
|
120
|
-
return { identical: diffs.length === 0, differences: diffs };
|
|
121
|
-
}
|
|
122
97
|
export const registerDiffProcessWithFile = defineTool({
|
|
123
98
|
name: "tm1_diff_process_with_file",
|
|
124
99
|
description: "Compare an installed TI process on the server against a local .pro file. Returns per-tab identical flags + line counts, parameter diff (added/removed/changed), variable diff, and datasource diff. Use before tm1_import_pro_file to preview what will change.",
|
|
125
|
-
annotations:
|
|
100
|
+
annotations: READ_ONLY,
|
|
126
101
|
output: DiffProcessResultSchema,
|
|
127
102
|
input: {
|
|
128
103
|
filePath: z
|
|
@@ -177,7 +152,7 @@ export const registerDiffProcessWithFile = defineTool({
|
|
|
177
152
|
const params = diffParams(installedParams, parsed.parameters);
|
|
178
153
|
const variables = diffVars(installedLayout.variables, parsed.variables);
|
|
179
154
|
const ignoredColumns = diffIgnoredColumns(installedLayout.ignoredColumns, ignoredColumnsOf(parsed.variablesUIData));
|
|
180
|
-
const dataSource =
|
|
155
|
+
const dataSource = diffDs(installedDs, parsed.dataSource);
|
|
181
156
|
const allIdentical = tabs.every((t) => t.identical) &&
|
|
182
157
|
params.added.length === 0 &&
|
|
183
158
|
params.removed.length === 0 &&
|
|
@@ -204,6 +204,7 @@ function diffIgnoredColumns(a, b) {
|
|
|
204
204
|
renamed,
|
|
205
205
|
};
|
|
206
206
|
}
|
|
207
|
+
// Shared with tm1_diff_process_with_file so both diffs check the same fields.
|
|
207
208
|
export function diffDs(a, b) {
|
|
208
209
|
const diffs = [];
|
|
209
210
|
const fields = [
|
|
@@ -227,6 +228,14 @@ export function diffDs(a, b) {
|
|
|
227
228
|
if ((a[f] ?? "") !== (b[f] ?? ""))
|
|
228
229
|
diffs.push(`${String(f)}: ${JSON.stringify(a[f])} → ${JSON.stringify(b[f])}`);
|
|
229
230
|
}
|
|
231
|
+
// Compared with their defaults filled in: the .pro parser sets the
|
|
232
|
+
// delimiter type only on ASCII sources and v12 never returns usesUnicode,
|
|
233
|
+
// so a bare comparison would report a difference nobody made.
|
|
234
|
+
const delimiter = (d) => d.type === "ASCII" ? (d.asciiDelimiterType ?? "Character") : undefined;
|
|
235
|
+
if (delimiter(a) !== delimiter(b))
|
|
236
|
+
diffs.push(`asciiDelimiterType: ${JSON.stringify(delimiter(a))} → ${JSON.stringify(delimiter(b))}`);
|
|
237
|
+
if ((a.usesUnicode ?? false) !== (b.usesUnicode ?? false))
|
|
238
|
+
diffs.push(`usesUnicode: ${JSON.stringify(a.usesUnicode ?? false)} → ${JSON.stringify(b.usesUnicode ?? false)}`);
|
|
230
239
|
return { identical: diffs.length === 0, differences: diffs };
|
|
231
240
|
}
|
|
232
241
|
// ── tool ──────────────────────────────────────────────────────────────────────
|
|
@@ -14,8 +14,8 @@ export const registerExportProcessToGit = defineTool({
|
|
|
14
14
|
description: [
|
|
15
15
|
"Serialize a TM1 process to the tm1-git two-file layout: a '{name}.json' (parameters, variables, ignored datasource columns, datasource) plus a '{name}.ti' (Prolog/Metadata/Data/Epilog as plain code).",
|
|
16
16
|
"The .ti holds the code in TM1's native `Code` representation (#region <Tab> / #endregion, CRLF, empty tabs omitted); the .json holds the structure. Code lives outside the JSON so Git diffs stay readable.",
|
|
17
|
-
"Returns both file bodies (json + ti) inline by default. Pass writeToDir to persist them to disk instead: the code is then written to files and omitted from the response to avoid duplicating it into the context window; only metadata (filenames, counts, writtenTo paths) comes back.
|
|
18
|
-
"Security: the ODBC datasource password is stripped unless includeDataSourcePassword is set (which also requires writeToDir); credential literals in the TI code are masked when maskSecrets is on; credentialsOmitted=true flags when a password was stripped.",
|
|
17
|
+
"Returns both file bodies (json + ti) inline by default. Pass writeToDir to persist them to disk instead: the code is then written to files and omitted from the response to avoid duplicating it into the context window; only metadata (filenames, counts, writtenTo paths) comes back. Files written via writeToDir are round-trip safe with tm1_import_process_from_git; the inline .ti has credential literals masked by default and is for reading, not re-importing.",
|
|
18
|
+
"Security: the ODBC datasource password is stripped unless includeDataSourcePassword is set (which also requires writeToDir); credential literals in the TI code are masked in the inline response when maskSecrets is on, never in written files (those must re-import intact); credentialsOmitted=true flags when a password was stripped.",
|
|
19
19
|
"includeDataSourcePassword is v12-only: what v11 hands out expires with the server run, so exporting it would produce a file that looks complete and fails later.",
|
|
20
20
|
],
|
|
21
21
|
annotations: READ_ONLY,
|
|
@@ -30,8 +30,7 @@ export const registerExportProcessToGit = defineTool({
|
|
|
30
30
|
.boolean()
|
|
31
31
|
.optional()
|
|
32
32
|
.default(true)
|
|
33
|
-
.describe("Redact credential literals in the
|
|
34
|
-
"Masks the password arg of ODBCOpen() and quoted values assigned to credential-named identifiers (pPwd, sToken, …). Default: true. Set false only when explicitly auditing credentials."),
|
|
33
|
+
.describe("Redact credential literals in the .ti code returned INLINE (password arg of ODBCOpen(), quoted values assigned to credential-named identifiers such as pPwd or sToken). Default: true. Files written via writeToDir always hold the code unmasked, so they re-import intact — treat them as containing any password literal the code has."),
|
|
35
34
|
includeDataSourcePassword: z
|
|
36
35
|
.boolean()
|
|
37
36
|
.optional()
|
|
@@ -57,8 +56,11 @@ export const registerExportProcessToGit = defineTool({
|
|
|
57
56
|
const pair = await readProcessAsGit(tm1Client, processName, {
|
|
58
57
|
includePassword: includeDataSourcePassword === true,
|
|
59
58
|
});
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
// The file is the deployable copy and gets the code as it is: a masked
|
|
60
|
+
// literal would re-import as broken code ('***' instead of the password).
|
|
61
|
+
// Masking applies to what goes back to the model — the inline body.
|
|
62
|
+
const ti = pair.ti;
|
|
63
|
+
const inlineTi = resolveMaskSecrets(maskSecrets) ? maskCode(ti) : ti;
|
|
62
64
|
const { json, credentialsOmitted } = pair;
|
|
63
65
|
const jsonFileName = `${processName}.json`;
|
|
64
66
|
const tiFileName = `${processName}.ti`;
|
|
@@ -103,7 +105,7 @@ export const registerExportProcessToGit = defineTool({
|
|
|
103
105
|
// Echo the file bodies inline only when NOT persisting to disk. With
|
|
104
106
|
// writeToDir the caller already has the files, so returning the code
|
|
105
107
|
// would just duplicate thousands of tokens into the context window.
|
|
106
|
-
...(writeToDir ? {} : { json, ti }),
|
|
108
|
+
...(writeToDir ? {} : { json, ti: inlineTi }),
|
|
107
109
|
}),
|
|
108
110
|
},
|
|
109
111
|
],
|
|
@@ -7,7 +7,7 @@ import { maskCode, resolveMaskSecrets } from "../../lib/mask-secrets.js";
|
|
|
7
7
|
import { supportsCredentialExport } from "../../lib/credential-format.js";
|
|
8
8
|
import { TM1Error, TM1ErrorCode } from "../../types.js";
|
|
9
9
|
import { ExportProcessToProResultSchema } from "../schemas/items.js";
|
|
10
|
-
import { READ_ONLY
|
|
10
|
+
import { READ_ONLY } from "../annotations.js";
|
|
11
11
|
import { defineTool } from "../define-tool.js";
|
|
12
12
|
export const registerExportProcessToPro = defineTool({
|
|
13
13
|
name: "tm1_export_process_to_pro",
|
|
@@ -15,10 +15,10 @@ export const registerExportProcessToPro = defineTool({
|
|
|
15
15
|
"Reverse of tm1_import_pro_file: serialize a TM1 process back to a .pro file body.",
|
|
16
16
|
"Fetches code (Prolog/Metadata/Data/Epilog), parameters, variables (including the columns set to Ignore), and datasource in parallel.",
|
|
17
17
|
"Returns the .pro content inline by default; pass writeToFile to also persist to an absolute path on the MCP host.",
|
|
18
|
-
"
|
|
18
|
+
"A file written via writeToFile is round-trip safe with tm1_import_pro_file — useful for syncing live server state into a Git repo. The inline copy has credential literals masked by default and is for reading, not re-importing.",
|
|
19
19
|
"NOT a drop-in replacement for the .pro file in TM1's Datadir: the output omits TM1's BOM, its '601' version header and CRLF line endings. Measured on 11.8: TM1 does load such a file at startup and rewrites it in its own dialect, but it decodes slot 565 with its own scheme — a password written here becomes garbage that TM1 then persists, so the process looks configured and fails at runtime. Deploy via tm1_import_pro_file, not by copying into the Datadir.",
|
|
20
20
|
],
|
|
21
|
-
annotations:
|
|
21
|
+
annotations: READ_ONLY,
|
|
22
22
|
output: ExportProcessToProResultSchema,
|
|
23
23
|
input: {
|
|
24
24
|
processName: z.string().describe("Name of the TI process to export"),
|
|
@@ -30,8 +30,9 @@ export const registerExportProcessToPro = defineTool({
|
|
|
30
30
|
.boolean()
|
|
31
31
|
.optional()
|
|
32
32
|
.default(true)
|
|
33
|
-
.describe("Redact credential literals in the
|
|
34
|
-
"
|
|
33
|
+
.describe("Redact credential literals in the code returned INLINE (password arg of ODBCOpen(), quoted values assigned to credential-named " +
|
|
34
|
+
"identifiers such as pPwd or sToken). Default: true. A file written via writeToFile always holds the code unmasked, so it " +
|
|
35
|
+
"re-imports intact — treat it as containing any password literal the code has."),
|
|
35
36
|
includeDataSourcePassword: z
|
|
36
37
|
.boolean()
|
|
37
38
|
.optional()
|
|
@@ -70,8 +71,10 @@ export const registerExportProcessToPro = defineTool({
|
|
|
70
71
|
if (!includeDataSourcePassword)
|
|
71
72
|
delete dataSource.password;
|
|
72
73
|
const credentialsIncluded = Boolean(includeDataSourcePassword && dataSource.password);
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
// The file is the deployable copy and gets the code as it is: a masked
|
|
75
|
+
// literal would re-import as broken code ('***' instead of the password).
|
|
76
|
+
// Masking applies to what goes back to the model — the inline body.
|
|
77
|
+
const build = (mask) => serializeToPro({
|
|
75
78
|
name: processName,
|
|
76
79
|
prolog: mask(code.prolog),
|
|
77
80
|
metadata: mask(code.metadata),
|
|
@@ -84,6 +87,10 @@ export const registerExportProcessToPro = defineTool({
|
|
|
84
87
|
: {}),
|
|
85
88
|
dataSource,
|
|
86
89
|
});
|
|
90
|
+
const proContent = build((s) => s);
|
|
91
|
+
const inlineContent = resolveMaskSecrets(maskSecrets)
|
|
92
|
+
? build(maskCode)
|
|
93
|
+
: proContent;
|
|
87
94
|
let writtenTo = null;
|
|
88
95
|
if (writeToFile) {
|
|
89
96
|
const target = resolveLocalPath(writeToFile, "writeToFile");
|
|
@@ -107,7 +114,7 @@ export const registerExportProcessToPro = defineTool({
|
|
|
107
114
|
credentialsIncluded,
|
|
108
115
|
// Written to disk only when credentials are in play — otherwise
|
|
109
116
|
// the caller already has the body and it stays out of the context.
|
|
110
|
-
...(includeDataSourcePassword ? {} : { content:
|
|
117
|
+
...(includeDataSourcePassword ? {} : { content: inlineContent }),
|
|
111
118
|
}),
|
|
112
119
|
},
|
|
113
120
|
],
|
|
@@ -4,7 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
import { TM1Error, TM1ErrorCode } from "../../types.js";
|
|
5
5
|
import { parseProFile } from "../../lib/pro-parser.js";
|
|
6
6
|
import { withToolHint } from "../error-format.js";
|
|
7
|
-
import { IDEMPOTENT_DESTRUCTIVE
|
|
7
|
+
import { IDEMPOTENT_DESTRUCTIVE } from "../annotations.js";
|
|
8
8
|
import { ImportProFileResultSchema } from "../schemas/items.js";
|
|
9
9
|
import { defineTool } from "../define-tool.js";
|
|
10
10
|
import { OVERWRITE_CONFIRM_SCHEMA, requireOverwriteConfirm, } from "../confirm.js";
|
|
@@ -13,7 +13,7 @@ import { backupProcess } from "./process-backup.js";
|
|
|
13
13
|
export const registerImportProFile = defineTool({
|
|
14
14
|
name: "tm1_import_pro_file",
|
|
15
15
|
description: "Parse a TM1 .pro file (Tabs / Parameters / Variables / DataSource) and deploy the process in one call. Provide either filePath (absolute path on the MCP host) or content (the .pro file body as string). Modes: 'create' (fail if exists), 'update' (fail if missing), 'upsert' (default — create or update). A .pro carries an ODBC password only when it came from tm1_export_process_to_pro against a v12 database, which writes it in clear; TM1's own Datadir .pro encodes slot 565 in a form that cannot be replayed over REST, and v11 exports never contain a password at all. Pass dataSourcePassword in every other case.",
|
|
16
|
-
annotations:
|
|
16
|
+
annotations: IDEMPOTENT_DESTRUCTIVE,
|
|
17
17
|
output: ImportProFileResultSchema,
|
|
18
18
|
input: {
|
|
19
19
|
filePath: z
|
|
@@ -108,25 +108,14 @@ export const registerImportProFile = defineTool({
|
|
|
108
108
|
data: parsed.data,
|
|
109
109
|
epilog: parsed.epilog,
|
|
110
110
|
}), `Code update failed after process '${processName}' was ${exists ? "located" : "created"}. PARTIAL APPLY: the process shell exists but tabs are stale/empty. Re-run tm1_import_pro_file with mode=update once root cause fixed, or tm1_delete_process to roll back.`);
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// layout with an empty variable list — patch on either.
|
|
120
|
-
if (exists ||
|
|
121
|
-
parsed.variables.length > 0 ||
|
|
122
|
-
parsed.variablesUIData.length > 0) {
|
|
123
|
-
await withToolHint(tm1Client.processes.updateVariables(processName, parsed.variables, parsed.variablesUIData.length > 0 || parsed.variables.length === 0
|
|
124
|
-
? parsed.variablesUIData
|
|
125
|
-
: undefined), `Variable update failed for '${processName}'. Code+parameters applied but variables missing. tm1_upsert_process with mode=update + variables=[...] to recover.`);
|
|
126
|
-
}
|
|
127
|
-
if (exists || parsed.dataSource.type !== "None") {
|
|
128
|
-
await withToolHint(tm1Client.processes.updateDataSource(processName, parsed.dataSource), `Datasource update failed for '${processName}' (type=${parsed.dataSource.type}). Code+params+vars applied. Verify datasource credentials/path (ASCII file existence, ODBC DSN, view name) and re-run tm1_upsert_process with mode=update + dataSource={...} to recover.`);
|
|
129
|
-
}
|
|
111
|
+
// The file is the whole truth: an empty list or a None source is sent as
|
|
112
|
+
// well, because TM1 applies both (measured on 11.8 and 12.5) and skipping
|
|
113
|
+
// them left the removed parameters, variables and source on the server.
|
|
114
|
+
await withToolHint(tm1Client.processes.updateParameters(processName, parsed.parameters), `Parameter update failed for '${processName}'. Code applied but parameters missing. Inspect parsed parameters and re-run tm1_upsert_process with mode=update + parameters=[...] to recover.`);
|
|
115
|
+
// A file without a column-layout block leaves the server's layout as it
|
|
116
|
+
// is; one that carries it replaces it.
|
|
117
|
+
await withToolHint(tm1Client.processes.updateVariables(processName, parsed.variables, parsed.variablesUIData.length > 0 ? parsed.variablesUIData : undefined), `Variable update failed for '${processName}'. Code+parameters applied but variables missing. tm1_upsert_process with mode=update + variables=[...] to recover.`);
|
|
118
|
+
await withToolHint(tm1Client.processes.updateDataSource(processName, parsed.dataSource), `Datasource update failed for '${processName}' (type=${parsed.dataSource.type}). Code+params+vars applied. Verify datasource credentials/path (ASCII file existence, ODBC DSN, view name) and re-run tm1_upsert_process with mode=update + dataSource={...} to recover.`);
|
|
130
119
|
return {
|
|
131
120
|
content: [
|
|
132
121
|
{
|
|
@@ -148,24 +148,14 @@ export const registerImportProcessFromGit = defineTool({
|
|
|
148
148
|
// The parsed tabs were preflighted above; unplacedBlobContent() made
|
|
149
149
|
// sure they cover this blob exactly, so what was checked is what deploys.
|
|
150
150
|
await withToolHint(tm1Client.processes.updateCodeBlob(processName, ti.replace(/\r?\n/g, "\r\n")), `Code update failed after process '${processName}' was ${exists ? "located" : "created"}. PARTIAL APPLY: shell exists but tabs are stale/empty. Re-run with mode=update once root cause fixed, or tm1_delete_process to roll back.`);
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// layout with an empty variable list — patch on either.
|
|
160
|
-
if (exists ||
|
|
161
|
-
parsed.variables.length > 0 ||
|
|
162
|
-
(parsed.variablesUIData?.length ?? 0) > 0) {
|
|
163
|
-
await withToolHint(tm1Client.processes.updateVariables(processName, parsed.variables, parsed.variablesUIData ??
|
|
164
|
-
(parsed.variables.length === 0 ? [] : undefined)), `Variable update failed for '${processName}'. Code+parameters applied but variables missing. tm1_upsert_process with mode=update + variables=[...] to recover.`);
|
|
165
|
-
}
|
|
166
|
-
if (exists || dataSource.type !== "None") {
|
|
167
|
-
await withToolHint(tm1Client.processes.updateDataSource(processName, dataSource), `Datasource update failed for '${processName}' (type=${dataSource.type}). Code+params+vars applied. For ODBC verify dataSourcePassword/DSN and re-run with mode=update.`);
|
|
168
|
-
}
|
|
151
|
+
// The file is the whole truth: an empty list or a None source is sent as
|
|
152
|
+
// well, because TM1 applies both (measured on 11.8 and 12.5) and skipping
|
|
153
|
+
// them left the removed parameters, variables and source on the server.
|
|
154
|
+
await withToolHint(tm1Client.processes.updateParameters(processName, parsed.parameters), `Parameter update failed for '${processName}'. Code applied but parameters missing. tm1_upsert_process with mode=update + parameters=[...] to recover.`);
|
|
155
|
+
// An export without variablesUIData (older format) leaves the server's
|
|
156
|
+
// column layout as it is; one that carries it replaces it.
|
|
157
|
+
await withToolHint(tm1Client.processes.updateVariables(processName, parsed.variables, parsed.variablesUIData), `Variable update failed for '${processName}'. Code+parameters applied but variables missing. tm1_upsert_process with mode=update + variables=[...] to recover.`);
|
|
158
|
+
await withToolHint(tm1Client.processes.updateDataSource(processName, dataSource), `Datasource update failed for '${processName}' (type=${dataSource.type}). Code+params+vars applied. For ODBC verify dataSourcePassword/DSN and re-run with mode=update.`);
|
|
169
159
|
if (parsed.hasSecurityAccess !== undefined) {
|
|
170
160
|
await withToolHint(tm1Client.processes.updateSecurityAccess(processName, parsed.hasSecurityAccess), `HasSecurityAccess update failed for '${processName}'. Code+params+vars+datasource applied. Re-run with mode=update once root cause fixed.`);
|
|
171
161
|
}
|