@ffschrattenecker/tm1-mcp-server 7.0.1 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/CHANGELOG.md +102 -1
  2. package/README.md +5 -5
  3. package/dist/config.js +34 -17
  4. package/dist/connections.d.ts +18 -1
  5. package/dist/connections.js +34 -18
  6. package/dist/http-transport.js +51 -4
  7. package/dist/lib/callgraph/referenceIndex.js +2 -1
  8. package/dist/lib/callgraph/rulesLinter.d.ts +0 -19
  9. package/dist/lib/callgraph/rulesLinter.js +0 -590
  10. package/dist/lib/callgraph/tiParser.js +10 -5
  11. package/dist/lib/callgraph/tm1-adapter.d.ts +5 -1
  12. package/dist/lib/callgraph/tm1-adapter.js +41 -9
  13. package/dist/lib/callgraph/variableEnv.js +3 -2
  14. package/dist/lib/cell-address.d.ts +11 -1
  15. package/dist/lib/cell-address.js +22 -3
  16. package/dist/lib/complexity/antipatterns.js +3 -1
  17. package/dist/lib/complexity/comment-classifier.js +2 -1
  18. package/dist/lib/coordinate-error.js +5 -1
  19. package/dist/lib/feeders/element-type-cache.js +4 -6
  20. package/dist/lib/naming/odata-filter.js +2 -1
  21. package/dist/lib/pro-parser.js +13 -4
  22. package/dist/lib/safe-regex.d.ts +0 -13
  23. package/dist/lib/safe-regex.js +40 -13
  24. package/dist/lib/sample-cells.d.ts +6 -0
  25. package/dist/lib/sample-cells.js +20 -2
  26. package/dist/lib/ti-identifier.d.ts +11 -0
  27. package/dist/lib/ti-identifier.js +11 -0
  28. package/dist/lib/tm1-name.d.ts +9 -0
  29. package/dist/lib/tm1-name.js +9 -0
  30. package/dist/lib/v12-compat/deprecated-ti.js +2 -2
  31. package/dist/session-manager.d.ts +1 -0
  32. package/dist/session-manager.js +24 -0
  33. package/dist/tm1-client/connection/profile.js +2 -4
  34. package/dist/tm1-client/dispatcher.d.ts +1 -1
  35. package/dist/tm1-client/dispatcher.js +18 -6
  36. package/dist/tm1-client/http.d.ts +21 -0
  37. package/dist/tm1-client/http.js +39 -30
  38. package/dist/tm1-client/services/cell-service.d.ts +9 -7
  39. package/dist/tm1-client/services/cell-service.js +54 -25
  40. package/dist/tm1-client/services/chore-service.js +8 -9
  41. package/dist/tm1-client/services/cube-service.d.ts +14 -18
  42. package/dist/tm1-client/services/cube-service.js +51 -45
  43. package/dist/tm1-client/services/dimension-order.d.ts +13 -0
  44. package/dist/tm1-client/services/dimension-order.js +42 -0
  45. package/dist/tm1-client/services/dimension-service.js +4 -6
  46. package/dist/tm1-client/services/element-service.d.ts +35 -11
  47. package/dist/tm1-client/services/element-service.js +124 -60
  48. package/dist/tm1-client/services/file-service.d.ts +42 -6
  49. package/dist/tm1-client/services/file-service.js +225 -28
  50. package/dist/tm1-client/services/hierarchy-service.d.ts +19 -9
  51. package/dist/tm1-client/services/hierarchy-service.js +118 -12
  52. package/dist/tm1-client/services/monitoring-service.js +2 -5
  53. package/dist/tm1-client/services/odata-page.d.ts +6 -0
  54. package/dist/tm1-client/services/odata-page.js +8 -0
  55. package/dist/tm1-client/services/process-service.d.ts +1 -1
  56. package/dist/tm1-client/services/process-service.js +26 -26
  57. package/dist/tm1-client/services/security-service.js +8 -9
  58. package/dist/tm1-client/services/server-service.js +14 -15
  59. package/dist/tm1-client/services/subset-service.d.ts +23 -10
  60. package/dist/tm1-client/services/subset-service.js +82 -26
  61. package/dist/tm1-client/services/view-service.js +12 -13
  62. package/dist/tm1-client.js +5 -2
  63. package/dist/tools/analysis/check-v12-readiness.js +3 -3
  64. package/dist/tools/celldata/check-feeders.js +6 -6
  65. package/dist/tools/celldata/check-writable-coords.js +43 -25
  66. package/dist/tools/celldata/execute-mdx.js +19 -23
  67. package/dist/tools/celldata/get-cell-value.js +1 -1
  68. package/dist/tools/celldata/get-view.js +10 -9
  69. package/dist/tools/celldata/member-ref.d.ts +15 -0
  70. package/dist/tools/celldata/member-ref.js +67 -0
  71. package/dist/tools/celldata/sample-cells.js +3 -0
  72. package/dist/tools/celldata/trace-cell-calculation.js +6 -6
  73. package/dist/tools/celldata/trace-feeders.js +6 -6
  74. package/dist/tools/celldata/write-cells.js +62 -12
  75. package/dist/tools/dimension-management/create-element-attribute.js +1 -1
  76. package/dist/tools/dimension-management/delete-hierarchy.js +3 -4
  77. package/dist/tools/dimension-management/get-element-attribute-values.js +7 -2
  78. package/dist/tools/dimension-management/list-element-attributes.js +1 -1
  79. package/dist/tools/dimension-management/update-element-attribute-value.js +8 -3
  80. package/dist/tools/dimension-management/update-element.js +14 -7
  81. package/dist/tools/fileops/container.d.ts +8 -0
  82. package/dist/tools/fileops/container.js +11 -0
  83. package/dist/tools/fileops/delete-file.js +4 -2
  84. package/dist/tools/fileops/get-file-content.js +45 -7
  85. package/dist/tools/fileops/list-files.js +5 -2
  86. package/dist/tools/fileops/search-files.js +4 -1
  87. package/dist/tools/fileops/upload-file.js +5 -2
  88. package/dist/tools/format.d.ts +1 -0
  89. package/dist/tools/format.js +7 -2
  90. package/dist/tools/index.js +0 -2
  91. package/dist/tools/model-building/check-cube-rule.js +3 -2
  92. package/dist/tools/model-building/clear-cube.js +35 -26
  93. package/dist/tools/model-building/set-cube-rules.js +13 -20
  94. package/dist/tools/model-building/unload-cube.js +1 -1
  95. package/dist/tools/operations/get-cube-stats.js +1 -1
  96. package/dist/tools/operations/get-transaction-log.js +1 -1
  97. package/dist/tools/operations/list-error-logs.js +27 -2
  98. package/dist/tools/schemas/items-fileops.d.ts +4 -0
  99. package/dist/tools/schemas/items-fileops.js +1 -0
  100. package/dist/tools/security/list-clients.js +5 -5
  101. package/dist/tools/security/list-groups.js +1 -1
  102. package/dist/tools/subsets/create-subset.js +9 -5
  103. package/dist/tools/subsets/delete-subset.js +8 -3
  104. package/dist/tools/subsets/update-subset.js +9 -9
  105. package/dist/tools/ti-development/diff-process-with-file.js +4 -29
  106. package/dist/tools/ti-development/diff-processes.js +9 -0
  107. package/dist/tools/ti-development/export-process-to-git.js +9 -7
  108. package/dist/tools/ti-development/export-process-to-pro.js +15 -8
  109. package/dist/tools/ti-development/import-pro-file.js +10 -21
  110. package/dist/tools/ti-development/import-process-from-git.js +8 -18
  111. package/dist/tools/ti-development/install-pro-bundle.js +2 -2
  112. package/dist/tools/ti-development/upsert-process.js +26 -5
  113. package/npm-shrinkwrap.json +465 -500
  114. package/package.json +14 -10
  115. package/dist/tools/dimension-management/move-element.d.ts +0 -2
  116. 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
- // OData key encoder: double ' per OData literal rules, then percent-encode.
18
- const enc = (s) => encodeURIComponent(String(s).replace(/'/g, "''"));
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('${enc(dimensionName)}')/Hierarchies('${enc(hierarchyName)}')`;
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='${enc(parentName)}',ComponentName='${enc(c.name)}')`, { Weight: c.weight });
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('${enc(dimensionName)}')/Hierarchies('${enc(hierarchyName)}')/Elements('${enc(elementName)}')?$select=Name`);
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('${enc(dimensionName)}')/Hierarchies('${enc(hierarchyName)}')/Elements`;
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('${enc(dimensionName)}')/Hierarchies('${enc(hierarchyName)}')/Elements('${enc(c.name)}')`,
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('${enc(dimensionName)}')/Hierarchies('${enc(hierarchyName)}')/Elements('${enc(elementName)}')`;
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('${enc(dimensionName)}')/Hierarchies('${enc(hierarchyName)}')/Elements('${enc(c.name)}')`,
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('${enc(dimensionName)}')/Hierarchies('${enc(hierarchyName)}')/Elements('${enc(elementName)}')`;
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('${enc(dimensionName)}')/Hierarchies('${enc(hierarchyName)}')/Elements`;
198
- const lit = (n) => `'${n.replace(/'/g, "''")}'`;
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('${enc(dimensionName)}')/Hierarchies('${enc(hierarchyName)}')/Elements('${enc(name)}')`;
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('${enc(dimensionName)}')/Hierarchies('${enc(hierarchyName)}')`;
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
- * Add an element as a component of a new parent.
368
- * POST /api/v1/Dimensions('{d}')/Hierarchies('{h}')/Elements('{newParent}')/Components
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 move(dimensionName, hierarchyName, elementName, newParent, weight) {
371
- const path = `/api/v1/Dimensions('${enc(dimensionName)}')/Hierarchies('${enc(hierarchyName)}')/Elements('${enc(newParent)}')/Components`;
372
- const body = {
373
- "@odata.id": `Dimensions('${enc(dimensionName)}')/Hierarchies('${enc(hierarchyName)}')/Elements('${enc(elementName)}')`,
374
- };
375
- await this.http.request("POST", path, body);
376
- await this.applyEdgeWeights(dimensionName, hierarchyName, newParent, [
377
- { name: elementName, ...(weight !== undefined ? { weight } : {}) },
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('${enc(dimensionName)}')/Hierarchies('${enc(hierarchyName)}')/Elements`;
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}('${enc(elements[i].name)}')?$select=Type`,
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}('${enc(el.name)}')`,
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}('${enc(el.name)}')`,
576
+ path: `${baseUrl}('${odataKey(el.name)}')`,
527
577
  body: {
528
578
  Components: el.components.map((c) => ({
529
- "@odata.id": `Dimensions('${enc(dimensionName)}')/Hierarchies('${enc(hierarchyName)}')/Elements('${enc(c.name)}')`,
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('${enc(dimensionName)}')/Hierarchies('${enc(hierarchyName)}')/Edges(ParentName='${enc(el.name)}',ComponentName='${enc(c.name)}')`,
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('${enc(dimensionName)}')/Hierarchies('${enc(hierarchyName)}')/Elements`;
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}('${enc(el.name)}')?$select=Type`)
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}('${enc(el.name)}')`, { Type: el.type });
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}('${enc(el.name)}')`, { Type: el.type });
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}('${enc(el.name)}')`;
689
+ const path = `${baseUrl}('${odataKey(el.name)}')`;
640
690
  const body = {
641
691
  Components: el.components.map((c) => ({
642
- "@odata.id": `Dimensions('${enc(dimensionName)}')/Hierarchies('${enc(hierarchyName)}')/Elements('${enc(c.name)}')`,
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('${enc(dimensionName)}')/Hierarchies('${enc(hierarchyName)}')/ElementAttributes`;
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('${enc(dimensionName)}')/Hierarchies('${enc(hierarchyName)}')/ElementAttributes`;
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
- `WHERE ([${dim}].[${elem}])`;
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('${enc(dimensionName)}')/Hierarchies('${enc(dimensionName)}')/Elements?$select=Name&$top=1&$count=true`;
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
- * Values are coerced to the attribute's type, looked up once: a Numeric
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. An unknown attribute or a non-numeric
771
- * value for a Numeric attribute is a VALIDATION_ERROR that names it, raised
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 types = new Map((await this.listAttributes(dimensionName, dimensionName)).map((a) => [
781
- a.name,
782
- a.type,
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.VALIDATION_ERROR,
789
- message: `Attribute '${u.attributeName}' does not exist on dimension '${dimensionName}'.`,
790
- hint: `Known attributes: ${[...types.keys()].join(", ") || "(none)"}. Create it first with tm1_create_element_attribute.`,
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
- return { elements: [u.elementName, u.attributeName], value };
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