@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
|
@@ -10,12 +10,15 @@ import { mapSettledWithConcurrency } from "../../lib/concurrency.js";
|
|
|
10
10
|
import { TM1Error, TM1ErrorCode } from "../../types.js";
|
|
11
11
|
import { BatchUnsupportedError } from "./batch-service.js";
|
|
12
12
|
import { rethrowIfSystemic } from "./fallback.js";
|
|
13
|
+
import { escapeOdataLiteral, odataKey } from "./odata-page.js";
|
|
13
14
|
// Max in-flight per-element REST calls within a single bulkUpsert pass. Bounds
|
|
14
15
|
// pressure on TM1's worker pool (mirrors the cap the feeder-audit fan-out uses)
|
|
15
16
|
// while removing the serialized 2-3N round-trips on an explicitly-bulk op.
|
|
16
17
|
const BULK_UPSERT_CONCURRENCY = 8;
|
|
17
|
-
//
|
|
18
|
-
|
|
18
|
+
// Names per `consolidatedAmong` request. Keeps the generated $filter well
|
|
19
|
+
// inside any proxy URL limit while still costing one round-trip for the
|
|
20
|
+
// element count a normal write carries.
|
|
21
|
+
const CONSOLIDATED_PROBE_CHUNK = 50;
|
|
19
22
|
// TM1 signals "element already exists" with different HTTP statuses across
|
|
20
23
|
// versions: some return 409 Conflict, but v11.x (REST 11.8) returns 400 with
|
|
21
24
|
// the message "An element with name ... already exists". Detect both so bulk
|
|
@@ -101,11 +104,11 @@ export class ElementService {
|
|
|
101
104
|
* all-1, and a PATCH that sets 1 to 1 is a round trip for nothing.
|
|
102
105
|
*/
|
|
103
106
|
async applyEdgeWeights(dimensionName, hierarchyName, parentName, components) {
|
|
104
|
-
const base = `/api/v1/Dimensions('${
|
|
107
|
+
const base = `/api/v1/Dimensions('${odataKey(dimensionName)}')/Hierarchies('${odataKey(hierarchyName)}')`;
|
|
105
108
|
for (const c of components) {
|
|
106
109
|
if (c.weight === undefined || c.weight === 1)
|
|
107
110
|
continue;
|
|
108
|
-
await this.http.request("PATCH", `${base}/Edges(ParentName='${
|
|
111
|
+
await this.http.request("PATCH", `${base}/Edges(ParentName='${odataKey(parentName)}',ComponentName='${odataKey(c.name)}')`, { Weight: c.weight });
|
|
109
112
|
}
|
|
110
113
|
}
|
|
111
114
|
/**
|
|
@@ -118,7 +121,7 @@ export class ElementService {
|
|
|
118
121
|
*/
|
|
119
122
|
async exists(dimensionName, hierarchyName, elementName) {
|
|
120
123
|
try {
|
|
121
|
-
await this.http.request("GET", `/api/v1/Dimensions('${
|
|
124
|
+
await this.http.request("GET", `/api/v1/Dimensions('${odataKey(dimensionName)}')/Hierarchies('${odataKey(hierarchyName)}')/Elements('${odataKey(elementName)}')?$select=Name`);
|
|
122
125
|
return true;
|
|
123
126
|
}
|
|
124
127
|
catch (e) {
|
|
@@ -128,7 +131,7 @@ export class ElementService {
|
|
|
128
131
|
}
|
|
129
132
|
}
|
|
130
133
|
async create(dimensionName, hierarchyName, element) {
|
|
131
|
-
const path = `/api/v1/Dimensions('${
|
|
134
|
+
const path = `/api/v1/Dimensions('${odataKey(dimensionName)}')/Hierarchies('${odataKey(hierarchyName)}')/Elements`;
|
|
132
135
|
const body = {
|
|
133
136
|
Name: element.name,
|
|
134
137
|
Type: element.type,
|
|
@@ -137,7 +140,7 @@ export class ElementService {
|
|
|
137
140
|
element.components &&
|
|
138
141
|
element.components.length > 0) {
|
|
139
142
|
body.Components = element.components.map((c) => ({
|
|
140
|
-
"@odata.id": `Dimensions('${
|
|
143
|
+
"@odata.id": `Dimensions('${odataKey(dimensionName)}')/Hierarchies('${odataKey(hierarchyName)}')/Elements('${odataKey(c.name)}')`,
|
|
141
144
|
}));
|
|
142
145
|
}
|
|
143
146
|
await this.http.request("POST", path, body);
|
|
@@ -150,7 +153,21 @@ export class ElementService {
|
|
|
150
153
|
* PATCH /api/v1/Dimensions('{d}')/Hierarchies('{h}')/Elements('{name}')
|
|
151
154
|
*/
|
|
152
155
|
async update(dimensionName, hierarchyName, elementName, update) {
|
|
153
|
-
const path = `/api/v1/Dimensions('${
|
|
156
|
+
const path = `/api/v1/Dimensions('${odataKey(dimensionName)}')/Hierarchies('${odataKey(hierarchyName)}')/Elements('${odataKey(elementName)}')`;
|
|
157
|
+
// Read the prior type so an in-place conversion is reported: turning a
|
|
158
|
+
// Numeric element into Consolidated/String discards its leaf cell values.
|
|
159
|
+
// Same probe and outage guard as bulkUpsert; an unreadable type reports
|
|
160
|
+
// nothing rather than guessing.
|
|
161
|
+
let from = null;
|
|
162
|
+
if (update.type !== undefined) {
|
|
163
|
+
const existing = await this.http
|
|
164
|
+
.request("GET", `${path}?$select=Type`)
|
|
165
|
+
.catch((e) => {
|
|
166
|
+
rethrowIfSystemic(e);
|
|
167
|
+
return null;
|
|
168
|
+
});
|
|
169
|
+
from = existing ? normalizeElementType(existing.Type) : null;
|
|
170
|
+
}
|
|
154
171
|
const body = {};
|
|
155
172
|
if (update.newName !== undefined) {
|
|
156
173
|
body.Name = update.newName;
|
|
@@ -160,7 +177,7 @@ export class ElementService {
|
|
|
160
177
|
}
|
|
161
178
|
if (update.components !== undefined) {
|
|
162
179
|
body.Components = update.components.map((c) => ({
|
|
163
|
-
"@odata.id": `Dimensions('${
|
|
180
|
+
"@odata.id": `Dimensions('${odataKey(dimensionName)}')/Hierarchies('${odataKey(hierarchyName)}')/Elements('${odataKey(c.name)}')`,
|
|
164
181
|
}));
|
|
165
182
|
}
|
|
166
183
|
await this.http.request("PATCH", path, body);
|
|
@@ -170,13 +187,18 @@ export class ElementService {
|
|
|
170
187
|
// key must use the NEW parent name.
|
|
171
188
|
update.newName ?? elementName, update.components);
|
|
172
189
|
}
|
|
190
|
+
return {
|
|
191
|
+
typeChange: from !== null && update.type !== undefined && from !== update.type
|
|
192
|
+
? { from, to: update.type }
|
|
193
|
+
: null,
|
|
194
|
+
};
|
|
173
195
|
}
|
|
174
196
|
/**
|
|
175
197
|
* Delete an element. May fail if the element is referenced in rules.
|
|
176
198
|
* DELETE /api/v1/Dimensions('{d}')/Hierarchies('{h}')/Elements('{name}')
|
|
177
199
|
*/
|
|
178
200
|
async delete(dimensionName, hierarchyName, elementName) {
|
|
179
|
-
const path = `/api/v1/Dimensions('${
|
|
201
|
+
const path = `/api/v1/Dimensions('${odataKey(dimensionName)}')/Hierarchies('${odataKey(hierarchyName)}')/Elements('${odataKey(elementName)}')`;
|
|
180
202
|
await this.http.request("DELETE", path);
|
|
181
203
|
}
|
|
182
204
|
/**
|
|
@@ -194,8 +216,8 @@ export class ElementService {
|
|
|
194
216
|
* the edges expanded — instead of loading the hierarchy.
|
|
195
217
|
*/
|
|
196
218
|
async planBulkUpsert(dimensionName, hierarchyName, elements) {
|
|
197
|
-
const base = `/api/v1/Dimensions('${
|
|
198
|
-
const lit = (n) => `'${n
|
|
219
|
+
const base = `/api/v1/Dimensions('${odataKey(dimensionName)}')/Hierarchies('${odataKey(hierarchyName)}')/Elements`;
|
|
220
|
+
const lit = (n) => `'${escapeOdataLiteral(n)}'`;
|
|
199
221
|
const current = new Map();
|
|
200
222
|
const names = [...new Set(elements.map((e) => e.name))];
|
|
201
223
|
for (let i = 0; i < names.length; i += 40) {
|
|
@@ -249,7 +271,7 @@ export class ElementService {
|
|
|
249
271
|
* reports NOT_FOUND instead of claiming a second delete.
|
|
250
272
|
*/
|
|
251
273
|
async deleteMany(dimensionName, hierarchyName, elementNames) {
|
|
252
|
-
const pathOf = (name) => `/api/v1/Dimensions('${
|
|
274
|
+
const pathOf = (name) => `/api/v1/Dimensions('${odataKey(dimensionName)}')/Hierarchies('${odataKey(hierarchyName)}')/Elements('${odataKey(name)}')`;
|
|
253
275
|
const failed = (elementName, e) => ({
|
|
254
276
|
elementName,
|
|
255
277
|
deleted: false,
|
|
@@ -314,7 +336,7 @@ export class ElementService {
|
|
|
314
336
|
*/
|
|
315
337
|
async scanElementNames(dimensionName, hierarchyName, opts) {
|
|
316
338
|
const { pageSize, maxScan, filter, scopeTotal } = opts;
|
|
317
|
-
const hierarchyPath = `/api/v1/Dimensions('${
|
|
339
|
+
const hierarchyPath = `/api/v1/Dimensions('${odataKey(dimensionName)}')/Hierarchies('${odataKey(hierarchyName)}')`;
|
|
318
340
|
const basePath = `${hierarchyPath}/Elements?$select=Name&$top=${pageSize}` +
|
|
319
341
|
(filter ? `&$filter=${encodeURIComponent(filter)}` : "");
|
|
320
342
|
// First page carries $count=true so we learn the total in one round-trip
|
|
@@ -364,18 +386,46 @@ export class ElementService {
|
|
|
364
386
|
};
|
|
365
387
|
}
|
|
366
388
|
/**
|
|
367
|
-
*
|
|
368
|
-
*
|
|
389
|
+
* Type of one element, or null when the hierarchy has no such element.
|
|
390
|
+
* Keyed lookup, so TM1 applies its own case- and space-insensitive name
|
|
391
|
+
* matching and answers with the stored spelling.
|
|
392
|
+
* GET /api/v1/Dimensions('{d}')/Hierarchies('{h}')/Elements('{name}')?$select=Name,Type
|
|
369
393
|
*/
|
|
370
|
-
async
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
394
|
+
async getType(dimensionName, hierarchyName, elementName) {
|
|
395
|
+
try {
|
|
396
|
+
const e = await this.http.request("GET", `/api/v1/Dimensions('${odataKey(dimensionName)}')/Hierarchies('${odataKey(hierarchyName)}')/Elements('${odataKey(elementName)}')?$select=Name,Type`);
|
|
397
|
+
return { name: e.Name, type: normalizeElementType(e.Type) };
|
|
398
|
+
}
|
|
399
|
+
catch (err) {
|
|
400
|
+
if (err instanceof TM1Error && err.code === TM1ErrorCode.NOT_FOUND) {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
throw err;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Which of `names` are consolidated (Type 3) in this hierarchy.
|
|
408
|
+
*
|
|
409
|
+
* One filtered request per chunk of names rather than a GET per element:
|
|
410
|
+
* the server evaluates `Type eq 3 and (Name eq ...)` and answers with the
|
|
411
|
+
* hits only, so the response is bounded by the consolidations among the
|
|
412
|
+
* names asked about, never by the size of the hierarchy.
|
|
413
|
+
*
|
|
414
|
+
* GET /api/v1/Dimensions('{d}')/Hierarchies('{h}')/Elements?$select=Name&$filter=...
|
|
415
|
+
*/
|
|
416
|
+
async consolidatedAmong(dimensionName, hierarchyName, names) {
|
|
417
|
+
const hits = [];
|
|
418
|
+
for (let i = 0; i < names.length; i += CONSOLIDATED_PROBE_CHUNK) {
|
|
419
|
+
const clause = names
|
|
420
|
+
.slice(i, i + CONSOLIDATED_PROBE_CHUNK)
|
|
421
|
+
.map((n) => `Name eq '${n.replace(/'/g, "''")}'`)
|
|
422
|
+
.join(" or ");
|
|
423
|
+
const filter = encodeURIComponent(`Type eq 3 and (${clause})`);
|
|
424
|
+
const page = await this.http.request("GET", `/api/v1/Dimensions('${odataKey(dimensionName)}')/Hierarchies('${odataKey(hierarchyName)}')/Elements?$select=Name&$filter=${filter}`);
|
|
425
|
+
for (const e of page.value)
|
|
426
|
+
hits.push(e.Name);
|
|
427
|
+
}
|
|
428
|
+
return hits;
|
|
379
429
|
}
|
|
380
430
|
/**
|
|
381
431
|
* Bulk upsert elements into a hierarchy. Two-pass to ensure leaves exist
|
|
@@ -436,7 +486,7 @@ export class ElementService {
|
|
|
436
486
|
*/
|
|
437
487
|
async bulkUpsertViaBatch(dimensionName, hierarchyName, elements) {
|
|
438
488
|
const batch = this.batch;
|
|
439
|
-
const baseUrl = `/api/v1/Dimensions('${
|
|
489
|
+
const baseUrl = `/api/v1/Dimensions('${odataKey(dimensionName)}')/Hierarchies('${odataKey(hierarchyName)}')/Elements`;
|
|
440
490
|
// Correlate by element INDEX, not name: names are caller-supplied and could
|
|
441
491
|
// repeat, which would collapse two sub-responses onto one id.
|
|
442
492
|
const idOf = (i) => `e${i}`;
|
|
@@ -470,7 +520,7 @@ export class ElementService {
|
|
|
470
520
|
const probed = await batch.execute(existing.map((i) => ({
|
|
471
521
|
id: idOf(i),
|
|
472
522
|
method: "GET",
|
|
473
|
-
path: `${baseUrl}('${
|
|
523
|
+
path: `${baseUrl}('${odataKey(elements[i].name)}')?$select=Type`,
|
|
474
524
|
})));
|
|
475
525
|
const typeById = new Map();
|
|
476
526
|
for (const r of probed) {
|
|
@@ -498,7 +548,7 @@ export class ElementService {
|
|
|
498
548
|
patches.push({
|
|
499
549
|
id: idOf(i),
|
|
500
550
|
method: "PATCH",
|
|
501
|
-
path: `${baseUrl}('${
|
|
551
|
+
path: `${baseUrl}('${odataKey(el.name)}')`,
|
|
502
552
|
body: { Type: el.type },
|
|
503
553
|
});
|
|
504
554
|
// A Numeric->Consolidated / Numeric->String conversion discards the
|
|
@@ -523,10 +573,10 @@ export class ElementService {
|
|
|
523
573
|
components.push({
|
|
524
574
|
id: idOf(i),
|
|
525
575
|
method: "PATCH",
|
|
526
|
-
path: `${baseUrl}('${
|
|
576
|
+
path: `${baseUrl}('${odataKey(el.name)}')`,
|
|
527
577
|
body: {
|
|
528
578
|
Components: el.components.map((c) => ({
|
|
529
|
-
"@odata.id": `Dimensions('${
|
|
579
|
+
"@odata.id": `Dimensions('${odataKey(dimensionName)}')/Hierarchies('${odataKey(hierarchyName)}')/Elements('${odataKey(c.name)}')`,
|
|
530
580
|
Weight: c.weight,
|
|
531
581
|
})),
|
|
532
582
|
},
|
|
@@ -553,7 +603,7 @@ export class ElementService {
|
|
|
553
603
|
edges.push({
|
|
554
604
|
id: `e${edgeOwner.length}`,
|
|
555
605
|
method: "PATCH",
|
|
556
|
-
path: `/api/v1/Dimensions('${
|
|
606
|
+
path: `/api/v1/Dimensions('${odataKey(dimensionName)}')/Hierarchies('${odataKey(hierarchyName)}')/Edges(ParentName='${odataKey(el.name)}',ComponentName='${odataKey(c.name)}')`,
|
|
557
607
|
body: { Weight: c.weight },
|
|
558
608
|
});
|
|
559
609
|
edgeOwner.push(i);
|
|
@@ -569,7 +619,7 @@ export class ElementService {
|
|
|
569
619
|
* Fans the calls out with bounded concurrency.
|
|
570
620
|
*/
|
|
571
621
|
async bulkUpsertPerRequest(dimensionName, hierarchyName, elements) {
|
|
572
|
-
const baseUrl = `/api/v1/Dimensions('${
|
|
622
|
+
const baseUrl = `/api/v1/Dimensions('${odataKey(dimensionName)}')/Hierarchies('${odataKey(hierarchyName)}')/Elements`;
|
|
573
623
|
// Pass 1: Create/upsert all elements without components. Same-type element
|
|
574
624
|
// writes are independent, so fan them out with bounded concurrency instead
|
|
575
625
|
// of one serialized round-trip each (was 2-3N sequential calls). The cap
|
|
@@ -590,7 +640,7 @@ export class ElementService {
|
|
|
590
640
|
// element's leaf cell values, so the caller must be told it happened
|
|
591
641
|
// rather than have it occur silently.
|
|
592
642
|
const existing = await this.http
|
|
593
|
-
.request("GET", `${baseUrl}('${
|
|
643
|
+
.request("GET", `${baseUrl}('${odataKey(el.name)}')?$select=Type`)
|
|
594
644
|
.catch((e) => {
|
|
595
645
|
// A transport/auth outage here must NOT collapse into the
|
|
596
646
|
// unconditional-PATCH branch below: that would silently change the
|
|
@@ -601,12 +651,12 @@ export class ElementService {
|
|
|
601
651
|
});
|
|
602
652
|
const from = existing ? normalizeElementType(existing.Type) : null;
|
|
603
653
|
if (from && from !== el.type) {
|
|
604
|
-
await this.http.request("PATCH", `${baseUrl}('${
|
|
654
|
+
await this.http.request("PATCH", `${baseUrl}('${odataKey(el.name)}')`, { Type: el.type });
|
|
605
655
|
return { name: el.name, from, to: el.type };
|
|
606
656
|
}
|
|
607
657
|
if (!from) {
|
|
608
658
|
// Type unreadable — preserve prior behaviour and patch unconditionally.
|
|
609
|
-
await this.http.request("PATCH", `${baseUrl}('${
|
|
659
|
+
await this.http.request("PATCH", `${baseUrl}('${odataKey(el.name)}')`, { Type: el.type });
|
|
610
660
|
}
|
|
611
661
|
return null;
|
|
612
662
|
}
|
|
@@ -636,10 +686,10 @@ export class ElementService {
|
|
|
636
686
|
// out with the same bounded concurrency.
|
|
637
687
|
const consolidated = elements.filter((el) => el.type === "Consolidated" && el.components && el.components.length > 0);
|
|
638
688
|
const pass2 = await mapSettledWithConcurrency(consolidated, BULK_UPSERT_CONCURRENCY, async (el) => {
|
|
639
|
-
const path = `${baseUrl}('${
|
|
689
|
+
const path = `${baseUrl}('${odataKey(el.name)}')`;
|
|
640
690
|
const body = {
|
|
641
691
|
Components: el.components.map((c) => ({
|
|
642
|
-
"@odata.id": `Dimensions('${
|
|
692
|
+
"@odata.id": `Dimensions('${odataKey(dimensionName)}')/Hierarchies('${odataKey(hierarchyName)}')/Elements('${odataKey(c.name)}')`,
|
|
643
693
|
Weight: c.weight,
|
|
644
694
|
})),
|
|
645
695
|
};
|
|
@@ -665,7 +715,7 @@ export class ElementService {
|
|
|
665
715
|
* GET /api/v1/Dimensions('{d}')/Hierarchies('{h}')/ElementAttributes
|
|
666
716
|
*/
|
|
667
717
|
async listAttributes(dimensionName, hierarchyName) {
|
|
668
|
-
const path = `/api/v1/Dimensions('${
|
|
718
|
+
const path = `/api/v1/Dimensions('${odataKey(dimensionName)}')/Hierarchies('${odataKey(hierarchyName)}')/ElementAttributes`;
|
|
669
719
|
const response = await this.http.request("GET", path);
|
|
670
720
|
return response.value.map((a) => ({
|
|
671
721
|
name: a.Name,
|
|
@@ -679,7 +729,7 @@ export class ElementService {
|
|
|
679
729
|
* POST /api/v1/Dimensions('{d}')/Hierarchies('{h}')/ElementAttributes
|
|
680
730
|
*/
|
|
681
731
|
async createAttribute(dimensionName, hierarchyName, attributeName, attributeType) {
|
|
682
|
-
const path = `/api/v1/Dimensions('${
|
|
732
|
+
const path = `/api/v1/Dimensions('${odataKey(dimensionName)}')/Hierarchies('${odataKey(hierarchyName)}')/ElementAttributes`;
|
|
683
733
|
await this.http.request("POST", path, {
|
|
684
734
|
Name: attributeName,
|
|
685
735
|
Type: attributeType,
|
|
@@ -690,17 +740,26 @@ export class ElementService {
|
|
|
690
740
|
* `}ElementAttributes_{Dim}`. Routes through CellService.executeMdx for the
|
|
691
741
|
* cellset round-trip.
|
|
692
742
|
*/
|
|
693
|
-
async getAttributeValues(dimensionName, elementName) {
|
|
743
|
+
async getAttributeValues(dimensionName, elementName, hierarchyName = dimensionName) {
|
|
744
|
+
// No attribute defined means no }ElementAttributes_ cube, and the MDX
|
|
745
|
+
// below would fail with a raw syntax error. Nothing to read is the answer.
|
|
746
|
+
if ((await this.listAttributes(dimensionName, hierarchyName)).length === 0)
|
|
747
|
+
return [];
|
|
694
748
|
// Escape `]` → `]]` in every bracketed identifier: an element or dimension
|
|
695
749
|
// named e.g. `Foo]` would otherwise break out of its MDX identifier and
|
|
696
750
|
// shift the read onto arbitrary members (MDX injection).
|
|
697
751
|
const esc = (s) => s.replace(/]/g, "]]");
|
|
698
752
|
const dim = esc(dimensionName);
|
|
699
753
|
const elem = esc(elementName);
|
|
754
|
+
const hier = esc(hierarchyName);
|
|
700
755
|
const ctrlCube = `}ElementAttributes_${dim}`;
|
|
701
756
|
const mdx = `SELECT {[}ElementAttributes_${dim}].MEMBERS} ON COLUMNS ` +
|
|
702
757
|
`FROM [${ctrlCube}] ` +
|
|
703
|
-
|
|
758
|
+
// Always name the hierarchy: the attribute cube spans every hierarchy of
|
|
759
|
+
// the dimension, and v12 refuses a bare name another hierarchy shares
|
|
760
|
+
// ("Member name A is ambiguous"), while v11 finds a bare name only in
|
|
761
|
+
// the default hierarchy.
|
|
762
|
+
`WHERE ([${dim}].[${hier}].[${elem}])`;
|
|
704
763
|
const result = await this.cells.executeMdx(mdx);
|
|
705
764
|
const out = [];
|
|
706
765
|
const tuples = result.axes[0]?.tuples ?? [];
|
|
@@ -726,7 +785,7 @@ export class ElementService {
|
|
|
726
785
|
* `attributeNames` narrows the columns; omitted, every attribute is read.
|
|
727
786
|
*/
|
|
728
787
|
async getAttributeValuesPage(dimensionName, opts) {
|
|
729
|
-
const countPath = `/api/v1/Dimensions('${
|
|
788
|
+
const countPath = `/api/v1/Dimensions('${odataKey(dimensionName)}')/Hierarchies('${odataKey(dimensionName)}')/Elements?$select=Name&$top=1&$count=true`;
|
|
730
789
|
const counted = await this.http.request("GET", countPath);
|
|
731
790
|
const total = counted["@odata.count"] ?? 0;
|
|
732
791
|
if (opts.limit === 0 || opts.offset >= total)
|
|
@@ -756,38 +815,40 @@ export class ElementService {
|
|
|
756
815
|
/**
|
|
757
816
|
* Set a single attribute value. See {@link updateAttributeValues}.
|
|
758
817
|
*/
|
|
759
|
-
async updateAttributeValue(dimensionName, elementName, attributeName, value) {
|
|
760
|
-
await this.updateAttributeValues(dimensionName, [
|
|
761
|
-
{ elementName, attributeName, value },
|
|
762
|
-
]);
|
|
818
|
+
async updateAttributeValue(dimensionName, elementName, attributeName, value, hierarchyName = dimensionName) {
|
|
819
|
+
await this.updateAttributeValues(dimensionName, [{ elementName, attributeName, value }], hierarchyName);
|
|
763
820
|
}
|
|
764
821
|
/**
|
|
765
822
|
* Set attribute values by writing to the `}ElementAttributes_{Dim}` control
|
|
766
823
|
* cube — every update in ONE cellset write (CellService.writeCells).
|
|
767
824
|
*
|
|
768
|
-
*
|
|
825
|
+
* The attributes are looked up once. TM1 matches attribute names ignoring
|
|
826
|
+
* case and spaces, so the lookup compares the same way; an unknown attribute
|
|
827
|
+
* is NOT_FOUND, raised before anything is written — without the check TM1
|
|
828
|
+
* answers with a raw MDX syntax error (no attribute at all means no
|
|
829
|
+
* attribute cube). Values are coerced to the attribute's type: a Numeric
|
|
769
830
|
* attribute takes a number or a numeric string (`"12.5"`), String and Alias
|
|
770
|
-
* attributes take the value as text
|
|
771
|
-
*
|
|
772
|
-
* before anything is written.
|
|
831
|
+
* attributes take the value as text; a non-numeric value for a Numeric
|
|
832
|
+
* attribute is a VALIDATION_ERROR that names it.
|
|
773
833
|
*
|
|
774
834
|
* Prefer TI processes (CellPutS / AttrPutS) for reproducible deployments;
|
|
775
835
|
* this REST-direct path is for ad-hoc / debugging use.
|
|
776
836
|
*/
|
|
777
|
-
async updateAttributeValues(dimensionName, updates) {
|
|
837
|
+
async updateAttributeValues(dimensionName, updates, hierarchyName = dimensionName) {
|
|
778
838
|
if (updates.length === 0)
|
|
779
839
|
return;
|
|
780
|
-
const
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
])
|
|
840
|
+
const attrs = await this.listAttributes(dimensionName, hierarchyName);
|
|
841
|
+
const key = (n) => n.replace(/\s/g, "").toLowerCase();
|
|
842
|
+
const types = new Map(attrs.map((a) => [key(a.name), a.type]));
|
|
843
|
+
const esc = (s) => s.replace(/]/g, "]]");
|
|
784
844
|
const cells = updates.map((u) => {
|
|
785
|
-
const type = types.get(u.attributeName);
|
|
845
|
+
const type = types.get(key(u.attributeName));
|
|
786
846
|
if (type === undefined) {
|
|
787
847
|
throw new TM1Error({
|
|
788
|
-
code: TM1ErrorCode.
|
|
789
|
-
message:
|
|
790
|
-
|
|
848
|
+
code: TM1ErrorCode.NOT_FOUND,
|
|
849
|
+
message: attrs.length === 0
|
|
850
|
+
? `Dimension '${dimensionName}' has no attributes; create '${u.attributeName}' with tm1_create_element_attribute first.`
|
|
851
|
+
: `Attribute '${u.attributeName}' does not exist on dimension '${dimensionName}'. Existing: ${attrs.map((a) => a.name).join(", ")}.`,
|
|
791
852
|
});
|
|
792
853
|
}
|
|
793
854
|
let value = String(u.value);
|
|
@@ -801,7 +862,10 @@ export class ElementService {
|
|
|
801
862
|
}
|
|
802
863
|
value = n;
|
|
803
864
|
}
|
|
804
|
-
|
|
865
|
+
// Qualified for the same reason as getAttributeValues; writeCells passes
|
|
866
|
+
// a `[Dim].[Hier].[Elem]` coordinate through unchanged.
|
|
867
|
+
const member = `[${esc(dimensionName)}].[${esc(hierarchyName)}].[${esc(u.elementName)}]`;
|
|
868
|
+
return { elements: [member, u.attributeName], value };
|
|
805
869
|
});
|
|
806
870
|
await this.cells.writeCells(`}ElementAttributes_${dimensionName}`, [dimensionName, `}ElementAttributes_${dimensionName}`], cells);
|
|
807
871
|
}
|
|
@@ -1,26 +1,53 @@
|
|
|
1
1
|
import type { TM1HttpClient } from "../http.js";
|
|
2
|
+
/** Which storage tree a file operation addresses. */
|
|
3
|
+
export type FileContainer = "files" | "applications";
|
|
2
4
|
export declare class FileService {
|
|
3
5
|
private readonly http;
|
|
4
6
|
constructor(http: TM1HttpClient);
|
|
7
|
+
/**
|
|
8
|
+
* Entries directly under an Applications URL.
|
|
9
|
+
*
|
|
10
|
+
* Follows `@odata.nextLink` if the server sends one. Whether these builds
|
|
11
|
+
* page this endpoint at all was not observed; a missed page here would not
|
|
12
|
+
* just shorten a listing, it would make name resolution answer NOT_FOUND for
|
|
13
|
+
* an entry that exists, so the loop runs either way.
|
|
14
|
+
*/
|
|
15
|
+
private appsChildren;
|
|
16
|
+
/**
|
|
17
|
+
* Walk name segments down the Applications tree, one listing per level.
|
|
18
|
+
*
|
|
19
|
+
* Costs a request per segment, which the depth of this tree makes cheap, and
|
|
20
|
+
* buys two things a derived key cannot: the entry's real ID whatever the
|
|
21
|
+
* server's naming rule is, and its type — so a caller asking for the bytes of
|
|
22
|
+
* a ViewReference gets told what it actually hit.
|
|
23
|
+
*/
|
|
24
|
+
private appsResolve;
|
|
25
|
+
/** URL of the bytes behind a DocumentReference, or a typed refusal. */
|
|
26
|
+
private appsContentUrl;
|
|
5
27
|
/**
|
|
6
28
|
* List files in TM1 server's blob/file storage.
|
|
7
29
|
* v12: GET /api/v1/Contents('Files')[/Contents('subdir')...]/Contents?$select=Name
|
|
8
30
|
* v11: same with 'Blobs' instead of 'Files'.
|
|
9
31
|
* Tries v12 'Files' first, falls back to v11 'Blobs'.
|
|
10
32
|
*/
|
|
11
|
-
list(path?: string): Promise<string[]>;
|
|
33
|
+
list(path?: string, container?: FileContainer): Promise<string[]>;
|
|
12
34
|
/**
|
|
13
35
|
* Get the content of a file from TM1 server's blob/file storage.
|
|
14
36
|
* Returns raw text (CSV/TXT/etc).
|
|
15
37
|
* Tries v12 'Files' first, falls back to v11 'Blobs'.
|
|
16
38
|
*/
|
|
17
|
-
getContent(fileName: string): Promise<string>;
|
|
39
|
+
getContent(fileName: string, container?: FileContainer): Promise<string>;
|
|
40
|
+
/**
|
|
41
|
+
* The same read, byte-for-byte. The Applications tree holds spreadsheets and
|
|
42
|
+
* other binaries, which a UTF-8 decode would quietly destroy.
|
|
43
|
+
*/
|
|
44
|
+
getContentBytes(fileName: string, container?: FileContainer): Promise<Buffer>;
|
|
18
45
|
/**
|
|
19
46
|
* Check whether a file exists. Tries v12 'Files' first, falls back to 'Blobs'.
|
|
20
47
|
* Implemented as a cheap GET on the entity ($select=Name) — TM1 REST does
|
|
21
48
|
* not expose HEAD on these. 404 → false; other errors propagate.
|
|
22
49
|
*/
|
|
23
|
-
exists(fileName: string): Promise<boolean>;
|
|
50
|
+
exists(fileName: string, container?: FileContainer): Promise<boolean>;
|
|
24
51
|
/**
|
|
25
52
|
* Upload (create-or-update) a file. Two-step v11 protocol:
|
|
26
53
|
* 1. POST entity into the parent Contents collection (only if missing)
|
|
@@ -30,14 +57,14 @@ export declare class FileService {
|
|
|
30
57
|
* Subfolders only supported on TM1 v12. Caller must ensure parent folders
|
|
31
58
|
* exist (folder-create not yet exposed).
|
|
32
59
|
*/
|
|
33
|
-
upload(fileName: string, content: Uint8Array): Promise<{
|
|
60
|
+
upload(fileName: string, content: Uint8Array, container?: FileContainer): Promise<{
|
|
34
61
|
created: boolean;
|
|
35
|
-
root: "Files" | "Blobs";
|
|
62
|
+
root: "Files" | "Blobs" | "Applications";
|
|
36
63
|
}>;
|
|
37
64
|
/**
|
|
38
65
|
* Delete a file from blob/file storage. Tries 'Files' first, then 'Blobs'.
|
|
39
66
|
*/
|
|
40
|
-
delete(fileName: string): Promise<void>;
|
|
67
|
+
delete(fileName: string, container?: FileContainer): Promise<void>;
|
|
41
68
|
/**
|
|
42
69
|
* Search file names in a folder using OData $filter.
|
|
43
70
|
* - startswith: case-insensitive prefix match
|
|
@@ -48,6 +75,15 @@ export declare class FileService {
|
|
|
48
75
|
contains?: string[] | undefined;
|
|
49
76
|
operator?: "and" | "or" | undefined;
|
|
50
77
|
path?: string | undefined;
|
|
78
|
+
container?: FileContainer | undefined;
|
|
51
79
|
}): Promise<string[]>;
|
|
80
|
+
/**
|
|
81
|
+
* Create-or-update a document in the Applications tree.
|
|
82
|
+
*
|
|
83
|
+
* Three steps, each one measured: POST the entity WITHOUT Content (inlining
|
|
84
|
+
* it is refused — the property is a stream), re-resolve to learn the ID the
|
|
85
|
+
* server assigned, then PUT the bytes behind the cast.
|
|
86
|
+
*/
|
|
87
|
+
private uploadToApplications;
|
|
52
88
|
}
|
|
53
89
|
//# sourceMappingURL=file-service.d.ts.map
|