tm1npm 1.6.0 → 2.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 +89 -0
- package/lib/objects/Axis.d.ts +1 -0
- package/lib/objects/Axis.d.ts.map +1 -1
- package/lib/objects/Axis.js +3 -0
- package/lib/objects/Chore.d.ts +2 -2
- package/lib/objects/Chore.d.ts.map +1 -1
- package/lib/objects/Chore.js +7 -13
- package/lib/objects/Cube.d.ts.map +1 -1
- package/lib/objects/Cube.js +2 -1
- package/lib/objects/Hierarchy.js +10 -10
- package/lib/objects/MDXView.d.ts +2 -0
- package/lib/objects/MDXView.d.ts.map +1 -1
- package/lib/objects/MDXView.js +30 -9
- package/lib/objects/NativeView.d.ts +5 -5
- package/lib/objects/NativeView.d.ts.map +1 -1
- package/lib/objects/NativeView.js +17 -34
- package/lib/objects/Process.d.ts +8 -3
- package/lib/objects/Process.d.ts.map +1 -1
- package/lib/objects/Process.js +143 -33
- package/lib/objects/Subset.d.ts.map +1 -1
- package/lib/objects/Subset.js +10 -3
- package/lib/objects/User.d.ts +5 -5
- package/lib/objects/User.d.ts.map +1 -1
- package/lib/objects/User.js +14 -23
- package/lib/services/ApplicationService.d.ts.map +1 -1
- package/lib/services/AsyncOperationService.d.ts +8 -1
- package/lib/services/AsyncOperationService.d.ts.map +1 -1
- package/lib/services/AsyncOperationService.js +69 -26
- package/lib/services/FileService.d.ts.map +1 -1
- package/lib/services/ProcessService.d.ts +18 -13
- package/lib/services/ProcessService.d.ts.map +1 -1
- package/lib/services/ProcessService.js +28 -17
- package/lib/services/RestService.d.ts +213 -25
- package/lib/services/RestService.d.ts.map +1 -1
- package/lib/services/RestService.js +840 -271
- package/lib/services/TM1Service.d.ts +42 -1
- package/lib/services/TM1Service.d.ts.map +1 -1
- package/lib/services/TM1Service.js +94 -4
- package/lib/tests/asyncOperationService.test.js +51 -45
- package/lib/tests/debuggerService.test.js +3 -1
- package/lib/tests/objectModelParity.test.js +362 -11
- package/lib/tests/objects.improved.test.js +28 -5
- package/lib/tests/processService.comprehensive.test.js +2 -2
- package/lib/tests/processService.test.js +20 -6
- package/lib/tests/restService.test.d.ts +0 -4
- package/lib/tests/restService.test.d.ts.map +1 -1
- package/lib/tests/restService.test.js +1558 -143
- package/lib/tests/tm1Service.test.js +80 -8
- package/lib/tests/user.issue61.test.d.ts +2 -0
- package/lib/tests/user.issue61.test.d.ts.map +1 -0
- package/lib/tests/user.issue61.test.js +180 -0
- package/lib/utils/Utils.d.ts +6 -1
- package/lib/utils/Utils.d.ts.map +1 -1
- package/lib/utils/Utils.js +56 -7
- package/package.json +1 -1
- package/src/objects/Axis.ts +4 -0
- package/src/objects/Chore.ts +7 -14
- package/src/objects/Cube.ts +2 -1
- package/src/objects/Hierarchy.ts +11 -11
- package/src/objects/MDXView.ts +29 -9
- package/src/objects/NativeView.ts +26 -42
- package/src/objects/Process.ts +182 -66
- package/src/objects/Subset.ts +17 -3
- package/src/objects/User.ts +17 -23
- package/src/services/ApplicationService.ts +4 -4
- package/src/services/AsyncOperationService.ts +76 -29
- package/src/services/FileService.ts +3 -3
- package/src/services/ProcessService.ts +67 -37
- package/src/services/RestService.ts +1020 -278
- package/src/services/TM1Service.ts +124 -6
- package/src/tests/asyncOperationService.test.ts +52 -48
- package/src/tests/debuggerService.test.ts +3 -1
- package/src/tests/objectModelParity.test.ts +456 -11
- package/src/tests/objects.improved.test.ts +41 -9
- package/src/tests/processService.comprehensive.test.ts +3 -3
- package/src/tests/processService.test.ts +21 -9
- package/src/tests/restService.test.ts +1844 -139
- package/src/tests/tm1Service.test.ts +95 -11
- package/src/tests/user.issue61.test.ts +206 -0
- package/src/utils/Utils.ts +60 -7
|
@@ -1,11 +1,48 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
5
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
39
|
exports.RestService = exports.AuthenticationMode = void 0;
|
|
7
40
|
const axios_1 = __importDefault(require("axios"));
|
|
41
|
+
const https = __importStar(require("https"));
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
8
43
|
const TM1Exception_1 = require("../exceptions/TM1Exception");
|
|
44
|
+
const Utils_1 = require("../utils/Utils");
|
|
45
|
+
const PRODUCT_VERSION_AUTH_SUFFIX = '/Configuration/ProductVersion/$value';
|
|
9
46
|
var AuthenticationMode;
|
|
10
47
|
(function (AuthenticationMode) {
|
|
11
48
|
AuthenticationMode[AuthenticationMode["BASIC"] = 1] = "BASIC";
|
|
@@ -22,37 +59,262 @@ class RestService {
|
|
|
22
59
|
get version() {
|
|
23
60
|
return this._serverVersion;
|
|
24
61
|
}
|
|
62
|
+
// Sync accessors for cached role flags. Return false before the first
|
|
63
|
+
// is_*_admin() call resolves — callers that need the real value must
|
|
64
|
+
// await is_admin() / is_data_admin() / etc. first.
|
|
65
|
+
get isAdmin() { var _a; return (_a = this._isAdmin) !== null && _a !== void 0 ? _a : false; }
|
|
66
|
+
get isDataAdmin() { var _a; return (_a = this._isDataAdmin) !== null && _a !== void 0 ? _a : false; }
|
|
67
|
+
get isSecurityAdmin() { var _a; return (_a = this._isSecurityAdmin) !== null && _a !== void 0 ? _a : false; }
|
|
68
|
+
get isOpsAdmin() { var _a; return (_a = this._isOpsAdmin) !== null && _a !== void 0 ? _a : false; }
|
|
25
69
|
constructor(config) {
|
|
70
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
71
|
+
this.sessionCookies = new Map();
|
|
26
72
|
this.isConnected = false;
|
|
27
73
|
this.config = { ...config };
|
|
74
|
+
this._asyncRequestsMode = (_a = config.asyncRequestsMode) !== null && _a !== void 0 ? _a : false;
|
|
75
|
+
this._cancelAtTimeout = (_b = config.cancelAtTimeout) !== null && _b !== void 0 ? _b : false;
|
|
76
|
+
this._timeout = (_c = config.timeout) !== null && _c !== void 0 ? _c : 60;
|
|
77
|
+
this._asyncPollingInitialDelay = (_d = config.asyncPollingInitialDelay) !== null && _d !== void 0 ? _d : 0.1;
|
|
78
|
+
this._asyncPollingMaxDelay = (_e = config.asyncPollingMaxDelay) !== null && _e !== void 0 ? _e : 1.0;
|
|
79
|
+
this._asyncPollingBackoffFactor = (_f = config.asyncPollingBackoffFactor) !== null && _f !== void 0 ? _f : 2.0;
|
|
80
|
+
this._reConnectOnSessionTimeout = (_g = config.reConnectOnSessionTimeout) !== null && _g !== void 0 ? _g : true;
|
|
81
|
+
this._reConnectOnRemoteDisconnect = (_h = config.reConnectOnRemoteDisconnect) !== null && _h !== void 0 ? _h : true;
|
|
82
|
+
this._remoteDisconnectMaxRetries = (_j = config.remoteDisconnectMaxRetries) !== null && _j !== void 0 ? _j : 5;
|
|
83
|
+
this._remoteDisconnectRetryDelay = (_k = config.remoteDisconnectRetryDelay) !== null && _k !== void 0 ? _k : 1;
|
|
84
|
+
this._remoteDisconnectMaxDelay = (_l = config.remoteDisconnectMaxDelay) !== null && _l !== void 0 ? _l : 30;
|
|
85
|
+
// Pre-populate admin flags for the built-in ADMIN user (mirrors tm1py)
|
|
86
|
+
if (config.user && (0, Utils_1.caseAndSpaceInsensitiveEquals)(config.user, 'ADMIN')) {
|
|
87
|
+
this._isAdmin = true;
|
|
88
|
+
this._isDataAdmin = true;
|
|
89
|
+
this._isSecurityAdmin = true;
|
|
90
|
+
this._isOpsAdmin = true;
|
|
91
|
+
}
|
|
28
92
|
this.setupAxiosInstance();
|
|
93
|
+
if (this.config.sessionId) {
|
|
94
|
+
// Mirror tm1py's _set_session_id_cookie: v12 topologies use paSession,
|
|
95
|
+
// v11 and baseUrl overrides use TM1SessionId.
|
|
96
|
+
const topo = this.determineTopology();
|
|
97
|
+
const cookieName = (topo === 'ibm_cloud' || topo === 'pa_proxy' || topo === 's2s')
|
|
98
|
+
? 'paSession'
|
|
99
|
+
: 'TM1SessionId';
|
|
100
|
+
this.sessionCookies.set(cookieName, this.config.sessionId);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
getSessionCookieValue() {
|
|
104
|
+
for (const name of RestService.SESSION_COOKIE_NAMES) {
|
|
105
|
+
const value = this.sessionCookies.get(name);
|
|
106
|
+
if (value)
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
buildCookieHeader() {
|
|
112
|
+
if (this.sessionCookies.size === 0)
|
|
113
|
+
return undefined;
|
|
114
|
+
const parts = [];
|
|
115
|
+
for (const [name, value] of this.sessionCookies) {
|
|
116
|
+
parts.push(`${name}=${value}`);
|
|
117
|
+
}
|
|
118
|
+
return parts.join('; ');
|
|
119
|
+
}
|
|
120
|
+
parseSetCookieHeaders(setCookie) {
|
|
121
|
+
if (!setCookie)
|
|
122
|
+
return;
|
|
123
|
+
const list = RestService.normaliseSetCookie(setCookie);
|
|
124
|
+
for (const raw of list) {
|
|
125
|
+
const firstSegment = raw.split(';')[0];
|
|
126
|
+
const eqIdx = firstSegment.indexOf('=');
|
|
127
|
+
if (eqIdx <= 0)
|
|
128
|
+
continue;
|
|
129
|
+
// Strip CR/LF/NUL defensively to block header-injection via compromised response
|
|
130
|
+
const sanitize = (s) => s.replace(/[\r\n\0]/g, '').trim();
|
|
131
|
+
const name = sanitize(firstSegment.slice(0, eqIdx));
|
|
132
|
+
const value = sanitize(firstSegment.slice(eqIdx + 1));
|
|
133
|
+
if (!RestService.SESSION_COOKIE_NAMES.includes(name))
|
|
134
|
+
continue;
|
|
135
|
+
if (value === '') {
|
|
136
|
+
this.sessionCookies.delete(name);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
this.sessionCookies.set(name, value);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
removeAuthorizationHeader() {
|
|
144
|
+
delete this.axiosInstance.defaults.headers.common['Authorization'];
|
|
145
|
+
}
|
|
146
|
+
deleteHeaderCaseInsensitive(headers, name) {
|
|
147
|
+
if (!headers)
|
|
148
|
+
return;
|
|
149
|
+
// axios 1.x may supply an AxiosHeaders instance with case-insensitive lookup; plain objects
|
|
150
|
+
// (common in retry paths and test mocks) are case-sensitive and require explicit iteration
|
|
151
|
+
const target = name.toLowerCase();
|
|
152
|
+
for (const key of Object.keys(headers)) {
|
|
153
|
+
if (key.toLowerCase() === target) {
|
|
154
|
+
delete headers[key];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
29
157
|
}
|
|
30
158
|
setupAxiosInstance() {
|
|
31
159
|
const baseURL = this.buildBaseUrl();
|
|
32
|
-
|
|
160
|
+
const axiosConfig = {
|
|
33
161
|
baseURL,
|
|
34
|
-
timeout:
|
|
162
|
+
timeout: this._timeout * 1000,
|
|
35
163
|
headers: {
|
|
36
164
|
...RestService.HEADERS,
|
|
37
165
|
...(this.config.sessionContext && { 'TM1-SessionContext': this.config.sessionContext })
|
|
38
166
|
}
|
|
39
|
-
}
|
|
167
|
+
};
|
|
168
|
+
if (this.config.proxies) {
|
|
169
|
+
const proxyUrl = this.config.proxies.https || this.config.proxies.http;
|
|
170
|
+
if (proxyUrl) {
|
|
171
|
+
const parsed = new URL(proxyUrl);
|
|
172
|
+
axiosConfig.proxy = {
|
|
173
|
+
host: parsed.hostname,
|
|
174
|
+
port: parsed.port
|
|
175
|
+
? parseInt(parsed.port, 10)
|
|
176
|
+
: (parsed.protocol === 'https:' ? 443 : 80),
|
|
177
|
+
protocol: parsed.protocol.replace(':', ''),
|
|
178
|
+
...(parsed.username && {
|
|
179
|
+
auth: {
|
|
180
|
+
username: decodeURIComponent(parsed.username),
|
|
181
|
+
password: decodeURIComponent(parsed.password)
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (this.config.sslContext) {
|
|
188
|
+
axiosConfig.httpsAgent = this.config.sslContext;
|
|
189
|
+
}
|
|
190
|
+
else if (this.config.cert) {
|
|
191
|
+
const [certPath, keyPath] = Array.isArray(this.config.cert)
|
|
192
|
+
? this.config.cert
|
|
193
|
+
: [this.config.cert, undefined];
|
|
194
|
+
axiosConfig.httpsAgent = new https.Agent({
|
|
195
|
+
cert: fs.readFileSync(certPath),
|
|
196
|
+
key: keyPath ? fs.readFileSync(keyPath) : undefined
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
this.axiosInstance = axios_1.default.create(axiosConfig);
|
|
40
200
|
this.setupInterceptors();
|
|
41
201
|
}
|
|
42
202
|
buildBaseUrl() {
|
|
43
|
-
|
|
44
|
-
|
|
203
|
+
return this.resolveRoots().serviceRoot;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Pick the deployment topology based on the provided config, mirroring
|
|
207
|
+
* tm1py's _determine_auth_mode + _construct_service_and_auth_root dispatch.
|
|
208
|
+
*
|
|
209
|
+
* Note: authUrl is intentionally excluded from the v12 signal set because
|
|
210
|
+
* tm1npm historically uses authUrl for CAM SSO (unlike tm1py, where auth_url
|
|
211
|
+
* is a v12-only field). apiKey is also excluded to avoid collision with the
|
|
212
|
+
* existing BASIC_API_KEY auth flow.
|
|
213
|
+
*/
|
|
214
|
+
determineTopology() {
|
|
215
|
+
const c = this.config;
|
|
216
|
+
const hasV12Signal = !!(c.instance || c.database || c.iamUrl || c.paUrl || c.tenant);
|
|
217
|
+
// tm1py's _construct_service_and_auth_root routes v12 modes (IBM Cloud / PA
|
|
218
|
+
// Proxy / S2S) through their dedicated constructors even if base_url is
|
|
219
|
+
// supplied. Only non-v12 configs fall through to the base_url override.
|
|
220
|
+
if (!hasV12Signal)
|
|
221
|
+
return c.baseUrl ? 'base_url' : 'v11';
|
|
222
|
+
if (c.iamUrl)
|
|
223
|
+
return 'ibm_cloud';
|
|
224
|
+
if (c.address && c.user && !c.instance)
|
|
225
|
+
return 'pa_proxy';
|
|
226
|
+
return 's2s';
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Resolve the TM1 service root and auth root URLs for the configured topology.
|
|
230
|
+
* Mirrors tm1py's _construct_service_and_auth_root return tuple.
|
|
231
|
+
*/
|
|
232
|
+
resolveRoots() {
|
|
233
|
+
switch (this.determineTopology()) {
|
|
234
|
+
case 'base_url': return this.rootsFromBaseUrl();
|
|
235
|
+
case 'ibm_cloud': return this.rootsIbmCloud();
|
|
236
|
+
case 'pa_proxy': return this.rootsPaProxy();
|
|
237
|
+
case 's2s': return this.rootsS2s();
|
|
238
|
+
case 'v11':
|
|
239
|
+
default: return this.rootsV11();
|
|
45
240
|
}
|
|
241
|
+
}
|
|
242
|
+
rootsV11() {
|
|
243
|
+
var _a;
|
|
46
244
|
const protocol = this.config.ssl ? 'https' : 'http';
|
|
47
245
|
const address = this.config.address || 'localhost';
|
|
48
|
-
const port = this.config.port
|
|
49
|
-
|
|
246
|
+
const port = (_a = this.config.port) !== null && _a !== void 0 ? _a : 8001;
|
|
247
|
+
const serviceRoot = `${protocol}://${address}:${port}/api/v1`;
|
|
248
|
+
return { serviceRoot, authRoot: serviceRoot + PRODUCT_VERSION_AUTH_SUFFIX };
|
|
249
|
+
}
|
|
250
|
+
rootsIbmCloud() {
|
|
251
|
+
const { address, tenant, database, ssl } = this.config;
|
|
252
|
+
if (!address || !tenant || !database) {
|
|
253
|
+
throw new Error("'address', 'tenant' and 'database' must be provided to connect to TM1 > v12 in IBM Cloud");
|
|
254
|
+
}
|
|
255
|
+
if (!ssl) {
|
|
256
|
+
throw new Error("'ssl' must be true to connect to TM1 > v12 in IBM Cloud");
|
|
257
|
+
}
|
|
258
|
+
const t = encodeURIComponent(tenant);
|
|
259
|
+
const d = encodeURIComponent(database);
|
|
260
|
+
const serviceRoot = `https://${address}/api/${t}/v0/tm1/${d}`;
|
|
261
|
+
return { serviceRoot, authRoot: serviceRoot + PRODUCT_VERSION_AUTH_SUFFIX };
|
|
262
|
+
}
|
|
263
|
+
rootsPaProxy() {
|
|
264
|
+
const { address, database, ssl } = this.config;
|
|
265
|
+
if (!address || !database) {
|
|
266
|
+
throw new Error("'address' and 'database' must be provided to connect to TM1 > v12 using PA Proxy");
|
|
267
|
+
}
|
|
268
|
+
const protocol = ssl ? 'https' : 'http';
|
|
269
|
+
const d = encodeURIComponent(database);
|
|
270
|
+
const serviceRoot = `${protocol}://${address}/tm1/${d}/api/v1`;
|
|
271
|
+
const authRoot = `${protocol}://${address}/login`;
|
|
272
|
+
return { serviceRoot, authRoot };
|
|
273
|
+
}
|
|
274
|
+
rootsS2s() {
|
|
275
|
+
const { instance, database, ssl, port } = this.config;
|
|
276
|
+
if (!instance || !database) {
|
|
277
|
+
throw new Error("'instance' and 'database' arguments are required for v12 authentication with 'address'");
|
|
278
|
+
}
|
|
279
|
+
const protocol = ssl ? 'https' : 'http';
|
|
280
|
+
const address = this.config.address && this.config.address.length > 0
|
|
281
|
+
? this.config.address
|
|
282
|
+
: 'localhost';
|
|
283
|
+
const portPart = port != null ? `:${port}` : '';
|
|
284
|
+
const i = encodeURIComponent(instance);
|
|
285
|
+
const d = encodeURIComponent(database);
|
|
286
|
+
const serviceRoot = `${protocol}://${address}${portPart}/${i}/api/v1/Databases('${d}')`;
|
|
287
|
+
const authRoot = `${protocol}://${address}${portPart}/${i}/auth/v1/session`;
|
|
288
|
+
return { serviceRoot, authRoot };
|
|
289
|
+
}
|
|
290
|
+
rootsFromBaseUrl() {
|
|
291
|
+
const base = this.config.baseUrl;
|
|
292
|
+
if (this.config.address) {
|
|
293
|
+
throw new Error("Base URL and Address cannot be specified at the same time");
|
|
294
|
+
}
|
|
295
|
+
if (/api\/v1\/Databases/.test(base)) {
|
|
296
|
+
if (!this.config.authUrl) {
|
|
297
|
+
throw new Error("Auth_url missing — when connecting to planning analytics engine using base_url, you must specify a corresponding auth_url");
|
|
298
|
+
}
|
|
299
|
+
return { serviceRoot: base, authRoot: this.config.authUrl };
|
|
300
|
+
}
|
|
301
|
+
// Recognize baseUrl shapes documented in docs/connection-guide.md
|
|
302
|
+
// (TM1 11 IBM Cloud `/tm1/api/tm1`, TM1 12 PaaS/access-token `/v0/tm1/...`)
|
|
303
|
+
// and use them verbatim. Only fall through to /api/v1 suffixing when the
|
|
304
|
+
// URL clearly lacks any TM1 API path — matching tm1py's fallback.
|
|
305
|
+
const trimmed = base.replace(/\/+$/, '');
|
|
306
|
+
// Each alternative is $-anchored (after trailing-slash trim above) so the
|
|
307
|
+
// match intent is explicit: the URL already ends in a TM1 API suffix.
|
|
308
|
+
const hasApiSuffix = /\/api\/v1$|\/v0\/tm1\/[^/]+$|\/tm1\/api\/tm1$/.test(trimmed);
|
|
309
|
+
const serviceRoot = hasApiSuffix ? trimmed : `${trimmed}/api/v1`;
|
|
310
|
+
return { serviceRoot, authRoot: serviceRoot + PRODUCT_VERSION_AUTH_SUFFIX };
|
|
50
311
|
}
|
|
51
312
|
setupInterceptors() {
|
|
52
313
|
// Request interceptor
|
|
53
314
|
this.axiosInstance.interceptors.request.use((config) => {
|
|
54
|
-
|
|
55
|
-
|
|
315
|
+
const cookieHeader = this.buildCookieHeader();
|
|
316
|
+
if (cookieHeader) {
|
|
317
|
+
config.headers['Cookie'] = cookieHeader;
|
|
56
318
|
}
|
|
57
319
|
if (this.sandboxName) {
|
|
58
320
|
config.headers['TM1-Sandbox'] = this.sandboxName;
|
|
@@ -60,24 +322,30 @@ class RestService {
|
|
|
60
322
|
return config;
|
|
61
323
|
}, (error) => Promise.reject(error));
|
|
62
324
|
// Response interceptor with retry logic
|
|
63
|
-
this.axiosInstance.interceptors.response.use((response) =>
|
|
325
|
+
this.axiosInstance.interceptors.response.use((response) => {
|
|
64
326
|
var _a;
|
|
327
|
+
this.parseSetCookieHeaders((_a = response.headers) === null || _a === void 0 ? void 0 : _a['set-cookie']);
|
|
328
|
+
return response;
|
|
329
|
+
}, async (error) => {
|
|
330
|
+
var _a, _b, _c, _d;
|
|
331
|
+
if (error.response) {
|
|
332
|
+
this.parseSetCookieHeaders((_a = error.response.headers) === null || _a === void 0 ? void 0 : _a['set-cookie']);
|
|
333
|
+
}
|
|
65
334
|
const originalRequest = error.config;
|
|
66
335
|
// Handle timeout errors
|
|
67
|
-
if (error.code === 'ECONNABORTED' || error.message.includes('timeout')) {
|
|
336
|
+
if (error.code === 'ECONNABORTED' || ((_c = (_b = error.message) === null || _b === void 0 ? void 0 : _b.includes) === null || _c === void 0 ? void 0 : _c.call(_b, 'timeout'))) {
|
|
68
337
|
throw new TM1Exception_1.TM1TimeoutException(`Request timeout: ${error.message}`);
|
|
69
338
|
}
|
|
70
|
-
// Handle authentication errors with retry
|
|
71
|
-
|
|
339
|
+
// Handle authentication errors with retry. Guarded by this.isConnected so a 401
|
|
340
|
+
// during disconnect()'s tm1.Close POST cannot recurse back into reAuthenticate().
|
|
341
|
+
if (((_d = error.response) === null || _d === void 0 ? void 0 : _d.status) === 401 && originalRequest && !originalRequest._retry
|
|
342
|
+
&& this.isConnected && this._reConnectOnSessionTimeout) {
|
|
72
343
|
originalRequest._retry = true;
|
|
73
344
|
try {
|
|
74
|
-
// Attempt re-authentication
|
|
75
345
|
await this.reAuthenticate();
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
// Retry the original request
|
|
346
|
+
// Stale values would defeat the rebuild by the request interceptor on replay
|
|
347
|
+
this.deleteHeaderCaseInsensitive(originalRequest.headers, 'Cookie');
|
|
348
|
+
this.deleteHeaderCaseInsensitive(originalRequest.headers, 'Authorization');
|
|
81
349
|
return this.axiosInstance(originalRequest);
|
|
82
350
|
}
|
|
83
351
|
catch (reAuthError) {
|
|
@@ -86,7 +354,7 @@ class RestService {
|
|
|
86
354
|
}
|
|
87
355
|
}
|
|
88
356
|
// Handle connection errors with retry
|
|
89
|
-
if (this.shouldRetryRequest(error) && this.canRetryRequest(originalRequest)) {
|
|
357
|
+
if (originalRequest && this.shouldRetryRequest(error) && this.canRetryRequest(originalRequest)) {
|
|
90
358
|
return this.retryRequest(originalRequest);
|
|
91
359
|
}
|
|
92
360
|
const response = error.response;
|
|
@@ -101,6 +369,8 @@ class RestService {
|
|
|
101
369
|
* Determine if a request should be retried
|
|
102
370
|
*/
|
|
103
371
|
shouldRetryRequest(error) {
|
|
372
|
+
if (!this._reConnectOnRemoteDisconnect)
|
|
373
|
+
return false;
|
|
104
374
|
// Retry on network errors, timeouts, and 5xx server errors
|
|
105
375
|
return !error.response ||
|
|
106
376
|
error.code === 'ECONNRESET' ||
|
|
@@ -112,18 +382,40 @@ class RestService {
|
|
|
112
382
|
* Check if a request can be retried
|
|
113
383
|
*/
|
|
114
384
|
canRetryRequest(config) {
|
|
385
|
+
if (config._idempotent === false) {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
115
388
|
// Don't retry if already retried maximum times
|
|
116
389
|
config._retryCount = config._retryCount || 0;
|
|
117
|
-
return config._retryCount <
|
|
390
|
+
return config._retryCount < this._remoteDisconnectMaxRetries;
|
|
118
391
|
}
|
|
119
392
|
/**
|
|
120
|
-
* Retry a failed request with exponential backoff
|
|
393
|
+
* Retry a failed request with exponential backoff, reconnecting the
|
|
394
|
+
* session before replay. Mirrors tm1py's _handle_remote_disconnect,
|
|
395
|
+
* which calls _manage_http_adapter() + connect() prior to retrying
|
|
396
|
+
* so a dropped session is re-established rather than replayed dead.
|
|
121
397
|
*/
|
|
122
398
|
async retryRequest(config) {
|
|
123
399
|
config._retryCount = config._retryCount || 0;
|
|
124
400
|
config._retryCount++;
|
|
125
|
-
const
|
|
401
|
+
const baseDelay = this._remoteDisconnectRetryDelay * Math.pow(2, config._retryCount - 1);
|
|
402
|
+
const retryDelay = Math.min(baseDelay, this._remoteDisconnectMaxDelay) * 1000;
|
|
126
403
|
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
404
|
+
// Re-establish session before replay. Clear sessionCookies first so
|
|
405
|
+
// connect() runs a full setupAuthentication (mirrors tm1py, whose
|
|
406
|
+
// connect() always re-runs _start_session / _set_session_id_cookie
|
|
407
|
+
// regardless of prior cookie state). Without this, a stale cookie
|
|
408
|
+
// from a server-invalidated session would be reused, the probe GET
|
|
409
|
+
// would 401, and the whole retry path would silently fail.
|
|
410
|
+
// Flip isConnected=false so the 401 re-auth branch cannot recurse
|
|
411
|
+
// through the probe.
|
|
412
|
+
this.sessionCookies.clear();
|
|
413
|
+
this.isConnected = false;
|
|
414
|
+
await this.connect();
|
|
415
|
+
// Drop stale Cookie/Authorization on the original config — the request
|
|
416
|
+
// interceptor rebuilds Cookie from sessionCookies on replay.
|
|
417
|
+
this.deleteHeaderCaseInsensitive(config.headers, 'Cookie');
|
|
418
|
+
this.deleteHeaderCaseInsensitive(config.headers, 'Authorization');
|
|
127
419
|
return this.axiosInstance(config);
|
|
128
420
|
}
|
|
129
421
|
extractErrorMessage(response) {
|
|
@@ -140,22 +432,120 @@ class RestService {
|
|
|
140
432
|
return `HTTP ${response.status}: ${response.statusText}`;
|
|
141
433
|
}
|
|
142
434
|
}
|
|
435
|
+
*waitTimeGenerator(timeout) {
|
|
436
|
+
let delay = this._asyncPollingInitialDelay;
|
|
437
|
+
let elapsed = 0;
|
|
438
|
+
if (timeout) {
|
|
439
|
+
while (elapsed < timeout) {
|
|
440
|
+
yield delay;
|
|
441
|
+
elapsed += delay;
|
|
442
|
+
delay = Math.min(delay * this._asyncPollingBackoffFactor, this._asyncPollingMaxDelay);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
while (true) {
|
|
447
|
+
yield delay;
|
|
448
|
+
delay = Math.min(delay * this._asyncPollingBackoffFactor, this._asyncPollingMaxDelay);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
async _executeSyncRequest(method, url, data, timeout, idempotent, axiosExtras) {
|
|
453
|
+
const config = {
|
|
454
|
+
method: method,
|
|
455
|
+
url,
|
|
456
|
+
data,
|
|
457
|
+
...axiosExtras
|
|
458
|
+
};
|
|
459
|
+
if (timeout !== undefined) {
|
|
460
|
+
config.timeout = timeout * 1000;
|
|
461
|
+
}
|
|
462
|
+
config._idempotent = idempotent !== null && idempotent !== void 0 ? idempotent : false;
|
|
463
|
+
return this.axiosInstance.request(config);
|
|
464
|
+
}
|
|
465
|
+
async _executeAsyncRequest(method, url, data, timeout, cancelAtTimeout, returnAsyncId, idempotent, axiosExtras) {
|
|
466
|
+
const preferValue = returnAsyncId ? 'respond-async' : 'respond-async,wait=55';
|
|
467
|
+
const config = {
|
|
468
|
+
method: method,
|
|
469
|
+
url,
|
|
470
|
+
data,
|
|
471
|
+
...axiosExtras,
|
|
472
|
+
headers: {
|
|
473
|
+
...axiosExtras === null || axiosExtras === void 0 ? void 0 : axiosExtras.headers,
|
|
474
|
+
Prefer: preferValue
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
if (timeout !== undefined) {
|
|
478
|
+
config.timeout = timeout * 1000;
|
|
479
|
+
}
|
|
480
|
+
config._idempotent = idempotent !== null && idempotent !== void 0 ? idempotent : false;
|
|
481
|
+
const response = await this.axiosInstance.request(config);
|
|
482
|
+
if (response.status !== 202) {
|
|
483
|
+
// Server completed synchronously. If the caller asked for an async
|
|
484
|
+
// ID (returnAsyncId: true) there is none to return — hand back the
|
|
485
|
+
// full response so the caller can inspect the result directly.
|
|
486
|
+
return response;
|
|
487
|
+
}
|
|
488
|
+
const location = response.headers['location'] || '';
|
|
489
|
+
const match = typeof location === 'string' ? location.match(/\('([^']+)'\)/) : null;
|
|
490
|
+
const asyncId = match ? match[1] : undefined;
|
|
491
|
+
if (!asyncId) {
|
|
492
|
+
throw new TM1Exception_1.TM1RestException(`Async request returned 202 but no valid async ID in Location header: ${location}`);
|
|
493
|
+
}
|
|
494
|
+
if (returnAsyncId) {
|
|
495
|
+
return asyncId;
|
|
496
|
+
}
|
|
497
|
+
return this._pollAsyncResponse(asyncId, timeout !== null && timeout !== void 0 ? timeout : this._timeout, cancelAtTimeout !== null && cancelAtTimeout !== void 0 ? cancelAtTimeout : this._cancelAtTimeout);
|
|
498
|
+
}
|
|
499
|
+
async _pollAsyncResponse(asyncId, timeout, cancelAtTimeout) {
|
|
500
|
+
for (const wait of this.waitTimeGenerator(timeout)) {
|
|
501
|
+
const response = await this.retrieve_async_response(asyncId);
|
|
502
|
+
if (response.status === 200 || response.status === 201) {
|
|
503
|
+
this.verifyAsyncResultHeader(response);
|
|
504
|
+
return response;
|
|
505
|
+
}
|
|
506
|
+
await new Promise(resolve => setTimeout(resolve, wait * 1000));
|
|
507
|
+
}
|
|
508
|
+
if (cancelAtTimeout) {
|
|
509
|
+
try {
|
|
510
|
+
await this.cancel_async_operation(asyncId);
|
|
511
|
+
}
|
|
512
|
+
catch (cancelError) {
|
|
513
|
+
console.warn(`Failed to cancel async operation ${asyncId} at timeout:`, cancelError);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
throw new TM1Exception_1.TM1TimeoutException(`Async operation ${asyncId} timed out after ${timeout} seconds`, timeout);
|
|
517
|
+
}
|
|
518
|
+
async _request(method, url, data, options) {
|
|
519
|
+
var _a, _b, _c, _d, _e;
|
|
520
|
+
const timeout = (_a = options === null || options === void 0 ? void 0 : options.timeout) !== null && _a !== void 0 ? _a : this._timeout;
|
|
521
|
+
const cancelAtTimeout = (_b = options === null || options === void 0 ? void 0 : options.cancelAtTimeout) !== null && _b !== void 0 ? _b : this._cancelAtTimeout;
|
|
522
|
+
const asyncMode = (options === null || options === void 0 ? void 0 : options.returnAsyncId) || ((_c = options === null || options === void 0 ? void 0 : options.asyncRequestsMode) !== null && _c !== void 0 ? _c : this._asyncRequestsMode);
|
|
523
|
+
const verifyResponse = (_d = options === null || options === void 0 ? void 0 : options.verifyResponse) !== null && _d !== void 0 ? _d : true;
|
|
524
|
+
const idempotent = (_e = options === null || options === void 0 ? void 0 : options.idempotent) !== null && _e !== void 0 ? _e : false;
|
|
525
|
+
const { asyncRequestsMode: _asyncModeOpt, returnAsyncId, cancelAtTimeout: _cancelAtTimeoutOpt, idempotent: _idempotentOpt, verifyResponse: _verifyResponseOpt, timeout: _timeoutOpt, ...axiosExtras } = options !== null && options !== void 0 ? options : {};
|
|
526
|
+
if (!verifyResponse && axiosExtras.validateStatus === undefined) {
|
|
527
|
+
axiosExtras.validateStatus = () => true;
|
|
528
|
+
}
|
|
529
|
+
if (asyncMode) {
|
|
530
|
+
return this._executeAsyncRequest(method, url, data, timeout, cancelAtTimeout, returnAsyncId, idempotent, axiosExtras);
|
|
531
|
+
}
|
|
532
|
+
return this._executeSyncRequest(method, url, data, timeout, idempotent, axiosExtras);
|
|
533
|
+
}
|
|
143
534
|
async connect() {
|
|
144
535
|
try {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
536
|
+
if (this.getSessionCookieValue() === undefined) {
|
|
537
|
+
await this.setupAuthentication();
|
|
538
|
+
}
|
|
539
|
+
// Mark probe non-idempotent so the response interceptor's retry
|
|
540
|
+
// branch skips it. Without this, a retry-triggered connect() whose
|
|
541
|
+
// probe also fails would spawn another connect(), recursively,
|
|
542
|
+
// bypassing remoteDisconnectMaxRetries (each probe has its own
|
|
543
|
+
// fresh _retryCount).
|
|
544
|
+
await this.axiosInstance.get('/Configuration/ServerName', { _idempotent: false });
|
|
545
|
+
// Strip Authorization only if the session cookie is established; Bearer/API-key
|
|
546
|
+
// modes that never issue a cookie must keep Authorization to stay authenticated
|
|
547
|
+
if (this.getSessionCookieValue()) {
|
|
548
|
+
this.removeAuthorizationHeader();
|
|
159
549
|
}
|
|
160
550
|
this.isConnected = true;
|
|
161
551
|
}
|
|
@@ -164,34 +554,36 @@ class RestService {
|
|
|
164
554
|
}
|
|
165
555
|
}
|
|
166
556
|
async disconnect() {
|
|
167
|
-
|
|
557
|
+
const shouldClose = this.isConnected;
|
|
558
|
+
// Flip isConnected first so a 401 on tm1.Close cannot trigger reAuthenticate recursion
|
|
559
|
+
this.isConnected = false;
|
|
560
|
+
if (shouldClose) {
|
|
168
561
|
try {
|
|
169
562
|
await this.axiosInstance.post('/ActiveSession/tm1.Close', {});
|
|
170
563
|
}
|
|
171
564
|
catch (error) {
|
|
172
565
|
// Ignore errors during disconnect
|
|
173
566
|
}
|
|
174
|
-
this.isConnected = false;
|
|
175
|
-
this.sessionId = undefined;
|
|
176
567
|
}
|
|
568
|
+
this.sessionCookies.clear();
|
|
177
569
|
}
|
|
178
|
-
async get(url,
|
|
179
|
-
return this.
|
|
570
|
+
async get(url, options) {
|
|
571
|
+
return this._request('GET', url, undefined, { idempotent: true, ...options });
|
|
180
572
|
}
|
|
181
|
-
async post(url, data,
|
|
182
|
-
return this.
|
|
573
|
+
async post(url, data, options) {
|
|
574
|
+
return this._request('POST', url, data, options);
|
|
183
575
|
}
|
|
184
|
-
async patch(url, data,
|
|
185
|
-
return this.
|
|
576
|
+
async patch(url, data, options) {
|
|
577
|
+
return this._request('PATCH', url, data, options);
|
|
186
578
|
}
|
|
187
|
-
async put(url, data,
|
|
188
|
-
return this.
|
|
579
|
+
async put(url, data, options) {
|
|
580
|
+
return this._request('PUT', url, data, options);
|
|
189
581
|
}
|
|
190
|
-
async delete(url,
|
|
191
|
-
return this.
|
|
582
|
+
async delete(url, options) {
|
|
583
|
+
return this._request('DELETE', url, undefined, options);
|
|
192
584
|
}
|
|
193
585
|
getSessionId() {
|
|
194
|
-
return this.
|
|
586
|
+
return this.getSessionCookieValue();
|
|
195
587
|
}
|
|
196
588
|
setSandbox(sandboxName) {
|
|
197
589
|
this.sandboxName = sandboxName;
|
|
@@ -200,175 +592,298 @@ class RestService {
|
|
|
200
592
|
return this.sandboxName;
|
|
201
593
|
}
|
|
202
594
|
isLoggedIn() {
|
|
203
|
-
return this.isConnected && !!this.
|
|
595
|
+
return this.isConnected && (!!this.getSessionCookieValue() ||
|
|
596
|
+
!!this.axiosInstance.defaults.headers.common['Authorization']);
|
|
204
597
|
}
|
|
205
598
|
async getApiMetadata() {
|
|
206
599
|
const response = await this.get("/$metadata");
|
|
207
600
|
return response.data;
|
|
208
601
|
}
|
|
602
|
+
// =========================================================================
|
|
603
|
+
// Authentication helpers — mirror tm1py's _build_authorization_token*,
|
|
604
|
+
// _generate_*_access_token, and _start_session flows
|
|
605
|
+
// =========================================================================
|
|
209
606
|
/**
|
|
210
|
-
*
|
|
607
|
+
* Build an httpsAgent option that skips TLS verification when verify is false.
|
|
211
608
|
*/
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
609
|
+
static insecureAgentOption(verify) {
|
|
610
|
+
return verify === false
|
|
611
|
+
? { httpsAgent: new https.Agent({ rejectUnauthorized: false }) }
|
|
612
|
+
: {};
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Normalise a Set-Cookie header value (string | string[] | undefined) into a string[].
|
|
616
|
+
*/
|
|
617
|
+
static normaliseSetCookie(raw) {
|
|
618
|
+
if (!raw)
|
|
619
|
+
return [];
|
|
620
|
+
return Array.isArray(raw) ? raw : [raw];
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Extract a named cookie value from raw Set-Cookie headers.
|
|
624
|
+
*/
|
|
625
|
+
static extractCookieValue(raw, name) {
|
|
626
|
+
const prefix = name + '=';
|
|
627
|
+
for (const header of RestService.normaliseSetCookie(raw)) {
|
|
628
|
+
const segment = header.split(';')[0];
|
|
629
|
+
if (segment.startsWith(prefix)) {
|
|
630
|
+
return segment.slice(prefix.length);
|
|
227
631
|
}
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
// CAM (Cognos Access Manager) authentication
|
|
231
|
-
if (this.config.authUrl && this.config.camPassport) {
|
|
232
|
-
await this.setupCamAuthentication();
|
|
233
|
-
return;
|
|
234
632
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
633
|
+
return undefined;
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Build Basic Authorization header.
|
|
637
|
+
* Mirrors tm1py's _build_authorization_token_basic.
|
|
638
|
+
*/
|
|
639
|
+
static _buildAuthorizationTokenBasic(user, password) {
|
|
640
|
+
return 'Basic ' + Buffer.from(`${user}:${password}`).toString('base64');
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Build CAMNamespace Authorization header.
|
|
644
|
+
* Mirrors tm1py's _build_authorization_token_cam (non-gateway path).
|
|
645
|
+
*/
|
|
646
|
+
static _buildAuthorizationTokenCam(user, password, namespace) {
|
|
647
|
+
return 'CAMNamespace ' + Buffer.from(`${user}:${password}:${namespace}`).toString('base64');
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Build CAMPassport Authorization token via gateway SSO.
|
|
651
|
+
* Mirrors tm1py's _build_authorization_token_cam (gateway path).
|
|
652
|
+
* Makes a GET request to the gateway URL with CAMNamespace as a query
|
|
653
|
+
* parameter and extracts the cam_passport cookie from the response.
|
|
654
|
+
*
|
|
655
|
+
* Note: tm1py uses HttpNegotiateAuth (NTLM/Kerberos) for gateway requests,
|
|
656
|
+
* which is Windows-only. This implementation sends a plain GET and relies on
|
|
657
|
+
* the gateway being accessible without NTLM. For environments requiring NTLM,
|
|
658
|
+
* pass a pre-obtained cam_passport via config.camPassport instead.
|
|
659
|
+
*/
|
|
660
|
+
static async _buildAuthorizationTokenCamSso(gateway, namespace, verify) {
|
|
661
|
+
const response = await axios_1.default.get(gateway, {
|
|
662
|
+
params: { CAMNamespace: namespace },
|
|
663
|
+
...RestService.insecureAgentOption(verify)
|
|
664
|
+
});
|
|
665
|
+
if (response.status !== 200) {
|
|
666
|
+
throw new Error('Failed to authenticate through CAM. Expected status_code 200, received status_code: ' +
|
|
667
|
+
response.status);
|
|
244
668
|
}
|
|
245
|
-
|
|
246
|
-
if (
|
|
247
|
-
|
|
248
|
-
this.axiosInstance.defaults.headers.common['Authorization'] = `Basic ${credentials}`;
|
|
249
|
-
// Add namespace for TM1 Cloud
|
|
250
|
-
if (this.config.namespace) {
|
|
251
|
-
this.axiosInstance.defaults.headers.common['TM1-Namespace'] = this.config.namespace;
|
|
252
|
-
}
|
|
253
|
-
return;
|
|
669
|
+
const passport = RestService.extractCookieValue(response.headers['set-cookie'], 'cam_passport');
|
|
670
|
+
if (!passport) {
|
|
671
|
+
throw new Error("Failed to authenticate through CAM. HTTP response does not contain 'cam_passport' cookie");
|
|
254
672
|
}
|
|
255
|
-
|
|
673
|
+
return 'CAMPassport ' + passport;
|
|
256
674
|
}
|
|
257
675
|
/**
|
|
258
|
-
*
|
|
676
|
+
* Generate IBM IAM Cloud access token.
|
|
677
|
+
* Mirrors tm1py's _generate_ibm_iam_cloud_access_token.
|
|
259
678
|
*/
|
|
260
|
-
async
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const authResponse = await axios_1.default.post(this.config.authUrl, {
|
|
266
|
-
parameters: [{
|
|
267
|
-
name: 'CAMPassport',
|
|
268
|
-
value: this.config.camPassport
|
|
269
|
-
}]
|
|
270
|
-
}, {
|
|
271
|
-
headers: {
|
|
272
|
-
'Content-Type': 'application/json'
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
if (authResponse.data && authResponse.data.sessionId) {
|
|
276
|
-
this.sessionId = authResponse.data.sessionId;
|
|
277
|
-
this.axiosInstance.defaults.headers.common['TM1SessionId'] = this.sessionId;
|
|
278
|
-
}
|
|
279
|
-
else {
|
|
280
|
-
throw new Error('CAM authentication failed: No session ID returned');
|
|
281
|
-
}
|
|
679
|
+
async _generateIbmIamCloudAccessToken() {
|
|
680
|
+
var _a;
|
|
681
|
+
const { iamUrl, apiKey } = this.config;
|
|
682
|
+
if (!iamUrl || !apiKey) {
|
|
683
|
+
throw new Error("'iamUrl' and 'apiKey' must be provided to generate access token from IBM Cloud");
|
|
282
684
|
}
|
|
283
|
-
|
|
284
|
-
|
|
685
|
+
const payload = `grant_type=urn%3Aibm%3Aparams%3Aoauth%3Agrant-type%3Aapikey&apikey=${encodeURIComponent(apiKey)}`;
|
|
686
|
+
const headers = {
|
|
687
|
+
'Accept': 'application/json',
|
|
688
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
689
|
+
};
|
|
690
|
+
const response = await axios_1.default.post(iamUrl, payload, {
|
|
691
|
+
headers,
|
|
692
|
+
...RestService.insecureAgentOption(this.config.verify)
|
|
693
|
+
});
|
|
694
|
+
if (!((_a = response.data) === null || _a === void 0 ? void 0 : _a.access_token)) {
|
|
695
|
+
throw new Error(`Failed to generate access_token from URL: '${iamUrl}'`);
|
|
285
696
|
}
|
|
697
|
+
return response.data.access_token;
|
|
286
698
|
}
|
|
287
699
|
/**
|
|
288
|
-
*
|
|
700
|
+
* Generate CPD (Cloud Pak for Data) access token.
|
|
701
|
+
* Mirrors tm1py's _generate_cpd_access_token.
|
|
289
702
|
*/
|
|
290
|
-
async
|
|
291
|
-
|
|
292
|
-
|
|
703
|
+
async _generateCpdAccessToken(credentials) {
|
|
704
|
+
var _a;
|
|
705
|
+
const { cpdUrl } = this.config;
|
|
706
|
+
if (!cpdUrl) {
|
|
707
|
+
throw new Error("'cpdUrl' must be provided to authenticate via CPD/Cloud Pak for Data");
|
|
293
708
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
'Content-Type': 'application/json'
|
|
303
|
-
}
|
|
304
|
-
});
|
|
305
|
-
if (authResponse.data && authResponse.data.token) {
|
|
306
|
-
this.axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${authResponse.data.token}`;
|
|
307
|
-
}
|
|
308
|
-
else if (authResponse.data && authResponse.data.sessionId) {
|
|
309
|
-
this.sessionId = authResponse.data.sessionId;
|
|
310
|
-
this.axiosInstance.defaults.headers.common['TM1SessionId'] = this.sessionId;
|
|
311
|
-
}
|
|
312
|
-
else {
|
|
313
|
-
throw new Error('CAM SSO authentication failed: No token or session ID returned');
|
|
314
|
-
}
|
|
709
|
+
const url = `${cpdUrl}/v1/preauth/signin`;
|
|
710
|
+
const headers = { 'Content-Type': 'application/json;charset=UTF-8' };
|
|
711
|
+
const response = await axios_1.default.post(url, credentials, {
|
|
712
|
+
headers,
|
|
713
|
+
...RestService.insecureAgentOption(this.config.verify)
|
|
714
|
+
});
|
|
715
|
+
if (!((_a = response.data) === null || _a === void 0 ? void 0 : _a.token)) {
|
|
716
|
+
throw new Error(`Failed to generate CPD access token from URL: '${url}'`);
|
|
315
717
|
}
|
|
316
|
-
|
|
317
|
-
|
|
718
|
+
return response.data.token;
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Authenticate with PA Proxy using a CPD JWT token.
|
|
722
|
+
* Mirrors tm1py's PA_PROXY flow in _start_session.
|
|
723
|
+
*/
|
|
724
|
+
async _authenticateWithPaProxy(jwt) {
|
|
725
|
+
const authRoot = this.resolveRoots().authRoot;
|
|
726
|
+
const headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
|
|
727
|
+
const payload = `jwt=${jwt}`;
|
|
728
|
+
const response = await axios_1.default.post(authRoot, payload, {
|
|
729
|
+
headers,
|
|
730
|
+
...RestService.insecureAgentOption(this.config.verify)
|
|
731
|
+
});
|
|
732
|
+
const setCookie = response.headers['set-cookie'];
|
|
733
|
+
const csrfValue = RestService.extractCookieValue(setCookie, 'ba-sso-csrf');
|
|
734
|
+
if (csrfValue) {
|
|
735
|
+
this.axiosInstance.defaults.headers.common['ba-sso-authenticity'] = csrfValue;
|
|
318
736
|
}
|
|
737
|
+
this.parseSetCookieHeaders(setCookie);
|
|
319
738
|
}
|
|
320
739
|
/**
|
|
321
|
-
*
|
|
740
|
+
* Authenticate Service-to-Service (v12).
|
|
741
|
+
* Mirrors tm1py's SERVICE_TO_SERVICE flow in _start_session:
|
|
742
|
+
* Uses Basic auth with applicationClientId:applicationClientSecret,
|
|
743
|
+
* then POSTs {"User": user} to the auth endpoint.
|
|
322
744
|
*/
|
|
323
|
-
async
|
|
324
|
-
|
|
745
|
+
async _authenticateServiceToService() {
|
|
746
|
+
var _a;
|
|
747
|
+
const { applicationClientId, applicationClientSecret, user } = this.config;
|
|
748
|
+
if (!applicationClientId || !applicationClientSecret) {
|
|
325
749
|
throw new Error('Service-to-Service authentication requires applicationClientId and applicationClientSecret');
|
|
326
750
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
headers: {
|
|
336
|
-
'Content-Type': 'application/x-www-form-urlencoded'
|
|
337
|
-
}
|
|
338
|
-
});
|
|
339
|
-
if (tokenResponse.data && tokenResponse.data.access_token) {
|
|
340
|
-
this.axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${tokenResponse.data.access_token}`;
|
|
341
|
-
}
|
|
342
|
-
else {
|
|
343
|
-
throw new Error('Service-to-Service authentication failed: No access token returned');
|
|
751
|
+
// Guard: v11 and plain baseUrl topologies resolve authRoot to a metadata
|
|
752
|
+
// probe URL, not a token endpoint. Require explicit authUrl in those cases.
|
|
753
|
+
if (!this.config.authUrl) {
|
|
754
|
+
const topo = this.determineTopology();
|
|
755
|
+
const baseUrlIsV12 = topo === 'base_url'
|
|
756
|
+
&& /api\/v1\/Databases/.test((_a = this.config.baseUrl) !== null && _a !== void 0 ? _a : '');
|
|
757
|
+
if (topo === 'v11' || (topo === 'base_url' && !baseUrlIsV12)) {
|
|
758
|
+
throw new Error("'authUrl' is required for Service-to-Service authentication on v11 topology");
|
|
344
759
|
}
|
|
345
760
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
761
|
+
const authRoot = this.config.authUrl || this.resolveRoots().authRoot;
|
|
762
|
+
const basicAuth = Buffer.from(`${applicationClientId}:${applicationClientSecret}`).toString('base64');
|
|
763
|
+
this.axiosInstance.defaults.headers.common['Authorization'] = `Basic ${basicAuth}`;
|
|
764
|
+
const response = await axios_1.default.post(authRoot, JSON.stringify({ User: user }), {
|
|
765
|
+
headers: {
|
|
766
|
+
...RestService.HEADERS,
|
|
767
|
+
'Authorization': `Basic ${basicAuth}`
|
|
768
|
+
},
|
|
769
|
+
...RestService.insecureAgentOption(this.config.verify)
|
|
770
|
+
});
|
|
771
|
+
this.parseSetCookieHeaders(response.headers['set-cookie']);
|
|
349
772
|
}
|
|
350
773
|
/**
|
|
351
|
-
*
|
|
774
|
+
* Determine the authentication mode from config.
|
|
775
|
+
* Mirrors tm1py's _determine_auth_mode, using the URL topology as the
|
|
776
|
+
* primary discriminator for v12 modes.
|
|
352
777
|
*/
|
|
353
778
|
getAuthenticationMode() {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
AuthenticationMode.
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
779
|
+
const topo = this.determineTopology();
|
|
780
|
+
const c = this.config;
|
|
781
|
+
switch (topo) {
|
|
782
|
+
case 'ibm_cloud':
|
|
783
|
+
return AuthenticationMode.IBM_CLOUD_API_KEY;
|
|
784
|
+
case 'pa_proxy':
|
|
785
|
+
return AuthenticationMode.PA_PROXY;
|
|
786
|
+
case 's2s':
|
|
787
|
+
return AuthenticationMode.SERVICE_TO_SERVICE;
|
|
788
|
+
case 'v11':
|
|
789
|
+
case 'base_url':
|
|
790
|
+
default: {
|
|
791
|
+
// v11 / base_url: check auth-specific config flags
|
|
792
|
+
if (c.accessToken)
|
|
793
|
+
return AuthenticationMode.ACCESS_TOKEN;
|
|
794
|
+
if (c.apiKey)
|
|
795
|
+
return AuthenticationMode.BASIC_API_KEY;
|
|
796
|
+
if (c.applicationClientId && c.applicationClientSecret) {
|
|
797
|
+
return AuthenticationMode.SERVICE_TO_SERVICE;
|
|
798
|
+
}
|
|
799
|
+
if (c.camPassport)
|
|
800
|
+
return AuthenticationMode.CAM;
|
|
801
|
+
if (c.gateway && c.namespace)
|
|
802
|
+
return AuthenticationMode.CAM_SSO;
|
|
803
|
+
if (c.integratedLogin)
|
|
804
|
+
return AuthenticationMode.WIA;
|
|
805
|
+
if (c.namespace)
|
|
806
|
+
return AuthenticationMode.CAM;
|
|
807
|
+
return AuthenticationMode.BASIC;
|
|
808
|
+
}
|
|
367
809
|
}
|
|
368
|
-
|
|
369
|
-
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Set up authentication based on configuration.
|
|
813
|
+
* Mirrors tm1py's _start_session routing.
|
|
814
|
+
*/
|
|
815
|
+
async setupAuthentication() {
|
|
816
|
+
const authMode = this.getAuthenticationMode();
|
|
817
|
+
const password = this.config.decodeB64 && this.config.password
|
|
818
|
+
? RestService.b64_decode_password(this.config.password)
|
|
819
|
+
: this.config.password;
|
|
820
|
+
switch (authMode) {
|
|
821
|
+
case AuthenticationMode.ACCESS_TOKEN:
|
|
822
|
+
this.axiosInstance.defaults.headers.common['Authorization'] =
|
|
823
|
+
`Bearer ${this.config.accessToken}`;
|
|
824
|
+
break;
|
|
825
|
+
case AuthenticationMode.BASIC_API_KEY:
|
|
826
|
+
if (this.config.user === 'apikey') {
|
|
827
|
+
this.axiosInstance.defaults.headers.common['Authorization'] =
|
|
828
|
+
RestService._buildAuthorizationTokenBasic('apikey', this.config.apiKey);
|
|
829
|
+
}
|
|
830
|
+
else {
|
|
831
|
+
this.axiosInstance.defaults.headers.common['API-Key'] = this.config.apiKey;
|
|
832
|
+
}
|
|
833
|
+
break;
|
|
834
|
+
case AuthenticationMode.IBM_CLOUD_API_KEY: {
|
|
835
|
+
const accessToken = await this._generateIbmIamCloudAccessToken();
|
|
836
|
+
this.axiosInstance.defaults.headers.common['Authorization'] =
|
|
837
|
+
`Bearer ${accessToken}`;
|
|
838
|
+
break;
|
|
839
|
+
}
|
|
840
|
+
case AuthenticationMode.PA_PROXY: {
|
|
841
|
+
if (!this.config.user || !password) {
|
|
842
|
+
throw new Error('PA Proxy authentication requires user and password');
|
|
843
|
+
}
|
|
844
|
+
const jwt = await this._generateCpdAccessToken({
|
|
845
|
+
username: this.config.user,
|
|
846
|
+
password
|
|
847
|
+
});
|
|
848
|
+
await this._authenticateWithPaProxy(jwt);
|
|
849
|
+
break;
|
|
850
|
+
}
|
|
851
|
+
case AuthenticationMode.SERVICE_TO_SERVICE:
|
|
852
|
+
await this._authenticateServiceToService();
|
|
853
|
+
break;
|
|
854
|
+
case AuthenticationMode.CAM_SSO: {
|
|
855
|
+
// CAM_SSO is only reached when gateway is set (see getAuthenticationMode)
|
|
856
|
+
const token = await RestService._buildAuthorizationTokenCamSso(this.config.gateway, this.config.namespace, this.config.verify);
|
|
857
|
+
this.axiosInstance.defaults.headers.common['Authorization'] = token;
|
|
858
|
+
break;
|
|
859
|
+
}
|
|
860
|
+
case AuthenticationMode.CAM: {
|
|
861
|
+
if (this.config.camPassport) {
|
|
862
|
+
this.axiosInstance.defaults.headers.common['Authorization'] =
|
|
863
|
+
'CAMPassport ' + this.config.camPassport;
|
|
864
|
+
}
|
|
865
|
+
else if (this.config.namespace && this.config.user && password) {
|
|
866
|
+
this.axiosInstance.defaults.headers.common['Authorization'] =
|
|
867
|
+
RestService._buildAuthorizationTokenCam(this.config.user, password, this.config.namespace);
|
|
868
|
+
}
|
|
869
|
+
else {
|
|
870
|
+
throw new Error('CAM authentication requires either camPassport or user/password/namespace');
|
|
871
|
+
}
|
|
872
|
+
break;
|
|
873
|
+
}
|
|
874
|
+
case AuthenticationMode.WIA:
|
|
875
|
+
throw new Error('Windows Integrated Authentication (WIA) is not supported in Node.js. ' +
|
|
876
|
+
'Use CAM or Basic authentication instead.');
|
|
877
|
+
case AuthenticationMode.BASIC:
|
|
878
|
+
default: {
|
|
879
|
+
if (!this.config.user || !password) {
|
|
880
|
+
throw new Error('No valid authentication configuration provided');
|
|
881
|
+
}
|
|
882
|
+
this.axiosInstance.defaults.headers.common['Authorization'] =
|
|
883
|
+
RestService._buildAuthorizationTokenBasic(this.config.user, password);
|
|
884
|
+
break;
|
|
885
|
+
}
|
|
370
886
|
}
|
|
371
|
-
return AuthenticationMode.BASIC;
|
|
372
887
|
}
|
|
373
888
|
/**
|
|
374
889
|
* Re-authenticate using stored configuration
|
|
@@ -408,10 +923,10 @@ class RestService {
|
|
|
408
923
|
getConnectionStats() {
|
|
409
924
|
return {
|
|
410
925
|
isConnected: this.isConnected,
|
|
411
|
-
sessionId: this.
|
|
926
|
+
sessionId: this.getSessionCookieValue(),
|
|
412
927
|
authMode: this.getAuthenticationMode(),
|
|
413
928
|
baseUrl: this.buildBaseUrl(),
|
|
414
|
-
timeout:
|
|
929
|
+
timeout: this._timeout,
|
|
415
930
|
sandbox: this.sandboxName
|
|
416
931
|
};
|
|
417
932
|
}
|
|
@@ -485,13 +1000,14 @@ class RestService {
|
|
|
485
1000
|
* Check if currently connected to TM1
|
|
486
1001
|
*/
|
|
487
1002
|
is_connected() {
|
|
488
|
-
return this.isConnected && !!this.
|
|
1003
|
+
return this.isConnected && (!!this.getSessionCookieValue() ||
|
|
1004
|
+
!!this.axiosInstance.defaults.headers.common['Authorization']);
|
|
489
1005
|
}
|
|
490
1006
|
/**
|
|
491
1007
|
* Get the current session ID
|
|
492
1008
|
*/
|
|
493
1009
|
session_id() {
|
|
494
|
-
return this.
|
|
1010
|
+
return this.getSessionCookieValue();
|
|
495
1011
|
}
|
|
496
1012
|
/**
|
|
497
1013
|
* Get TM1 server version (async method for tm1py compatibility)
|
|
@@ -514,56 +1030,69 @@ class RestService {
|
|
|
514
1030
|
this._serverVersion = version;
|
|
515
1031
|
}
|
|
516
1032
|
/**
|
|
517
|
-
*
|
|
1033
|
+
* Fetch the active user's group names as a CaseAndSpaceInsensitiveSet so
|
|
1034
|
+
* membership tests are case- and whitespace-insensitive (mirrors tm1py).
|
|
1035
|
+
* Concurrent callers (e.g. Promise.all([is_admin(), is_data_admin(), ...]))
|
|
1036
|
+
* coalesce onto a single in-flight request.
|
|
1037
|
+
*/
|
|
1038
|
+
fetchActiveUserGroupNames() {
|
|
1039
|
+
if (this._activeUserGroupsPromise)
|
|
1040
|
+
return this._activeUserGroupsPromise;
|
|
1041
|
+
const clear = () => { this._activeUserGroupsPromise = undefined; };
|
|
1042
|
+
const fetch = this.get('/ActiveUser/Groups').then((response) => {
|
|
1043
|
+
var _a, _b;
|
|
1044
|
+
clear();
|
|
1045
|
+
const set = new Utils_1.CaseAndSpaceInsensitiveSet();
|
|
1046
|
+
const value = (_b = (_a = response.data) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : [];
|
|
1047
|
+
for (const group of value) {
|
|
1048
|
+
if (group === null || group === void 0 ? void 0 : group.Name)
|
|
1049
|
+
set.add(group.Name);
|
|
1050
|
+
}
|
|
1051
|
+
return set;
|
|
1052
|
+
}, (err) => { clear(); throw err; });
|
|
1053
|
+
this._activeUserGroupsPromise = fetch;
|
|
1054
|
+
return fetch;
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Check if current user is admin. Result is cached after the first
|
|
1058
|
+
* computation, and pre-populated when the configured user is ADMIN.
|
|
518
1059
|
*/
|
|
519
1060
|
async is_admin() {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
catch (error) {
|
|
526
|
-
return false;
|
|
527
|
-
}
|
|
1061
|
+
if (this._isAdmin !== undefined)
|
|
1062
|
+
return this._isAdmin;
|
|
1063
|
+
const groups = await this.fetchActiveUserGroupNames();
|
|
1064
|
+
this._isAdmin = groups.has('ADMIN');
|
|
1065
|
+
return this._isAdmin;
|
|
528
1066
|
}
|
|
529
1067
|
/**
|
|
530
|
-
* Check if current user is data admin
|
|
1068
|
+
* Check if current user is data admin (member of Admin or DataAdmin).
|
|
531
1069
|
*/
|
|
532
1070
|
async is_data_admin() {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
catch (error) {
|
|
539
|
-
return false;
|
|
540
|
-
}
|
|
1071
|
+
if (this._isDataAdmin !== undefined)
|
|
1072
|
+
return this._isDataAdmin;
|
|
1073
|
+
const groups = await this.fetchActiveUserGroupNames();
|
|
1074
|
+
this._isDataAdmin = groups.has('Admin') || groups.has('DataAdmin');
|
|
1075
|
+
return this._isDataAdmin;
|
|
541
1076
|
}
|
|
542
1077
|
/**
|
|
543
|
-
* Check if current user is ops admin
|
|
1078
|
+
* Check if current user is ops admin (member of Admin or OperationsAdmin).
|
|
544
1079
|
*/
|
|
545
1080
|
async is_ops_admin() {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
catch (error) {
|
|
552
|
-
return false;
|
|
553
|
-
}
|
|
1081
|
+
if (this._isOpsAdmin !== undefined)
|
|
1082
|
+
return this._isOpsAdmin;
|
|
1083
|
+
const groups = await this.fetchActiveUserGroupNames();
|
|
1084
|
+
this._isOpsAdmin = groups.has('Admin') || groups.has('OperationsAdmin');
|
|
1085
|
+
return this._isOpsAdmin;
|
|
554
1086
|
}
|
|
555
1087
|
/**
|
|
556
|
-
* Check if current user is security admin
|
|
1088
|
+
* Check if current user is security admin (member of Admin or SecurityAdmin).
|
|
557
1089
|
*/
|
|
558
1090
|
async is_security_admin() {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
catch (error) {
|
|
565
|
-
return false;
|
|
566
|
-
}
|
|
1091
|
+
if (this._isSecurityAdmin !== undefined)
|
|
1092
|
+
return this._isSecurityAdmin;
|
|
1093
|
+
const groups = await this.fetchActiveUserGroupNames();
|
|
1094
|
+
this._isSecurityAdmin = groups.has('Admin') || groups.has('SecurityAdmin');
|
|
1095
|
+
return this._isSecurityAdmin;
|
|
567
1096
|
}
|
|
568
1097
|
/**
|
|
569
1098
|
* Add custom HTTP header to all requests
|
|
@@ -591,70 +1120,109 @@ class RestService {
|
|
|
591
1120
|
}
|
|
592
1121
|
return headers;
|
|
593
1122
|
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Insert `tm1.compact=v0` into the Accept header (after the
|
|
1125
|
+
* `application/json` segment) and return the previous header value.
|
|
1126
|
+
* Mirrors tm1py's add_compact_json_header.
|
|
1127
|
+
*/
|
|
1128
|
+
add_compact_json_header() {
|
|
1129
|
+
var _a;
|
|
1130
|
+
const original = (_a = this.axiosInstance.defaults.headers.common['Accept']) !== null && _a !== void 0 ? _a : '';
|
|
1131
|
+
const parts = original.split(';');
|
|
1132
|
+
// Insertion point matters: must come immediately after `application/json`
|
|
1133
|
+
parts.splice(1, 0, 'tm1.compact=v0');
|
|
1134
|
+
this.axiosInstance.defaults.headers.common['Accept'] = parts.join(';');
|
|
1135
|
+
return original;
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Decode a Base64-encoded password to its UTF-8 plaintext form
|
|
1139
|
+
* (mirrors tm1py's b64_decode_password).
|
|
1140
|
+
*/
|
|
1141
|
+
static b64_decode_password(encryptedPassword) {
|
|
1142
|
+
return Buffer.from(encryptedPassword, 'base64').toString('utf-8');
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Coerce a boolean/number/string config value to a boolean. Strings
|
|
1146
|
+
* are stripped of whitespace and lowercased before comparison with
|
|
1147
|
+
* `'true'` (mirrors tm1py's translate_to_boolean).
|
|
1148
|
+
*/
|
|
1149
|
+
static translate_to_boolean(value) {
|
|
1150
|
+
if (typeof value === 'boolean')
|
|
1151
|
+
return value;
|
|
1152
|
+
if (typeof value === 'number')
|
|
1153
|
+
return Boolean(value);
|
|
1154
|
+
if (typeof value === 'string') {
|
|
1155
|
+
return value.replace(/\s+/g, '').toLowerCase() === 'true';
|
|
1156
|
+
}
|
|
1157
|
+
throw new Error(`Invalid argument: '${String(value)}'. Must be type 'boolean', 'number', or 'string'`);
|
|
1158
|
+
}
|
|
594
1159
|
/**
|
|
595
1160
|
* Cancel an async operation by ID
|
|
596
1161
|
*/
|
|
597
1162
|
async cancel_async_operation(async_id) {
|
|
598
|
-
|
|
599
|
-
await this.post(`/AsyncOperations('${async_id}')/tm1.Cancel`);
|
|
600
|
-
}
|
|
601
|
-
catch (error) {
|
|
602
|
-
throw new TM1Exception_1.TM1RestException(`Failed to cancel async operation ${async_id}: ${error}`);
|
|
603
|
-
}
|
|
1163
|
+
await this.delete((0, Utils_1.formatUrl)("/_async('{}')", async_id), { asyncRequestsMode: false });
|
|
604
1164
|
}
|
|
605
1165
|
/**
|
|
606
1166
|
* Retrieve async operation response
|
|
607
1167
|
*/
|
|
608
1168
|
async retrieve_async_response(async_id) {
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
if (error instanceof TM1Exception_1.TM1RestException) {
|
|
620
|
-
throw error;
|
|
621
|
-
}
|
|
622
|
-
const status = (_a = error === null || error === void 0 ? void 0 : error.status) !== null && _a !== void 0 ? _a : (_b = error === null || error === void 0 ? void 0 : error.response) === null || _b === void 0 ? void 0 : _b.status;
|
|
623
|
-
throw new TM1Exception_1.TM1RestException(`Failed to retrieve async response ${async_id}: ${error}`, status, error === null || error === void 0 ? void 0 : error.response);
|
|
624
|
-
}
|
|
1169
|
+
// tm1py's retrieve_async_response returns the raw response without
|
|
1170
|
+
// raising on non-2xx because its caller (_poll_async_response) gates
|
|
1171
|
+
// on status_code in [200, 201]. Mirror that: accept all statuses so
|
|
1172
|
+
// transient 404s (resource not yet materialized) or 202s (still
|
|
1173
|
+
// running) flow through to the polling loop rather than aborting it.
|
|
1174
|
+
return this.get((0, Utils_1.formatUrl)("/_async('{}')", async_id), {
|
|
1175
|
+
asyncRequestsMode: false,
|
|
1176
|
+
verifyResponse: false,
|
|
1177
|
+
validateStatus: () => true
|
|
1178
|
+
});
|
|
625
1179
|
}
|
|
626
1180
|
/**
|
|
627
|
-
*
|
|
1181
|
+
* TM1 v12 returns completed async results with HTTP 200 and encodes
|
|
1182
|
+
* the true operation status in the `asyncresult` header (e.g.
|
|
1183
|
+
* "500 Internal Server Error"). Mirror tm1py's
|
|
1184
|
+
* `_transform_async_response` by throwing on any embedded non-2xx
|
|
1185
|
+
* status so callers are not handed a 500 as "success".
|
|
628
1186
|
*/
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
1187
|
+
verifyAsyncResultHeader(response) {
|
|
1188
|
+
var _a;
|
|
1189
|
+
const headerValue = (_a = response.headers) === null || _a === void 0 ? void 0 : _a['asyncresult'];
|
|
1190
|
+
if (typeof headerValue !== 'string')
|
|
1191
|
+
return;
|
|
1192
|
+
const embeddedStatus = parseInt(headerValue.trim().split(/\s+/)[0], 10);
|
|
1193
|
+
if (Number.isNaN(embeddedStatus))
|
|
1194
|
+
return;
|
|
1195
|
+
if (embeddedStatus >= 200 && embeddedStatus < 300)
|
|
1196
|
+
return;
|
|
1197
|
+
throw new TM1Exception_1.TM1RestException(`Async operation failed with status ${headerValue}`, embeddedStatus, response);
|
|
637
1198
|
}
|
|
638
1199
|
/**
|
|
639
|
-
* Wait for async operation to complete
|
|
1200
|
+
* Wait for async operation to complete using a fixed polling cadence.
|
|
1201
|
+
*
|
|
1202
|
+
* Unlike the internal dispatcher's {@link waitTimeGenerator} (capped
|
|
1203
|
+
* exponential backoff), this public helper polls every
|
|
1204
|
+
* {@link poll_interval_seconds} seconds so existing callers who tuned
|
|
1205
|
+
* the cadence keep their original behavior.
|
|
640
1206
|
*/
|
|
641
|
-
async wait_for_async_operation(async_id, timeout_seconds = 300, poll_interval_seconds = 1) {
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
1207
|
+
async wait_for_async_operation(async_id, timeout_seconds = 300, poll_interval_seconds = 1, cancel_at_timeout = false) {
|
|
1208
|
+
const deadline = Date.now() + timeout_seconds * 1000;
|
|
1209
|
+
while (Date.now() < deadline) {
|
|
1210
|
+
const response = await this.retrieve_async_response(async_id);
|
|
1211
|
+
if (response.status === 200 || response.status === 201) {
|
|
1212
|
+
this.verifyAsyncResultHeader(response);
|
|
1213
|
+
return response.data;
|
|
1214
|
+
}
|
|
1215
|
+
await new Promise(resolve => setTimeout(resolve, poll_interval_seconds * 1000));
|
|
1216
|
+
}
|
|
1217
|
+
if (cancel_at_timeout) {
|
|
1218
|
+
try {
|
|
1219
|
+
await this.cancel_async_operation(async_id);
|
|
649
1220
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
throw new TM1Exception_1.TM1RestException(`Async operation failed: ${JSON.stringify(response)}`);
|
|
1221
|
+
catch (cancelError) {
|
|
1222
|
+
console.warn(`Failed to cancel async operation ${async_id} at timeout:`, cancelError);
|
|
653
1223
|
}
|
|
654
|
-
// Wait before polling again
|
|
655
|
-
await new Promise(resolve => setTimeout(resolve, poll_interval_ms));
|
|
656
1224
|
}
|
|
657
|
-
throw new TM1Exception_1.TM1TimeoutException(`Async operation ${async_id} timed out after ${timeout_seconds} seconds
|
|
1225
|
+
throw new TM1Exception_1.TM1TimeoutException(`Async operation ${async_id} timed out after ${timeout_seconds} seconds`, timeout_seconds);
|
|
658
1226
|
}
|
|
659
1227
|
/**
|
|
660
1228
|
* Get active user name
|
|
@@ -739,4 +1307,5 @@ RestService.HEADERS = {
|
|
|
739
1307
|
};
|
|
740
1308
|
RestService.DEFAULT_CONNECTION_POOL_SIZE = 10;
|
|
741
1309
|
RestService.DEFAULT_POOL_CONNECTIONS = 1;
|
|
1310
|
+
RestService.SESSION_COOKIE_NAMES = ['TM1SessionId', 'paSession'];
|
|
742
1311
|
//# sourceMappingURL=RestService.js.map
|