@vario-software/vario-app-framework-backend 2025.37.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/api/Api.js ADDED
@@ -0,0 +1,449 @@
1
+ const { http, https } = require('follow-redirects');
2
+ const { Readable } = require('stream');
3
+ const { omitBy, isNil } = require('lodash');
4
+ const fetchFn = require('#backend/api/helpers/fetch.js');
5
+ const getResponseStreamFn = require('#backend/api/helpers/getResponseStream.js');
6
+ const gatewayFn = require('#backend/api/helpers/gateway.js');
7
+ const redirectRequestFn = require('#backend/api/helpers/redirectRequest.js');
8
+ const vqlFn = require('#backend/api/helpers/vql.js');
9
+ const { getApp } = require('#backend/utils/context.js');
10
+ const HttpError = require('#backend/utils/httpError.js');
11
+
12
+ class Api
13
+ {
14
+ baseUrl = '';
15
+
16
+ constructor(path, {
17
+ method = 'GET',
18
+ saveResponse = true,
19
+ resolveOn = 'end',
20
+ timeout = 15 * 60 * 1000,
21
+ suppressLogs = false,
22
+ followRedirects,
23
+ body,
24
+ inputStream,
25
+ formData,
26
+ outputStream,
27
+ headers,
28
+ useInternalApi,
29
+ secret,
30
+ ...restOptions
31
+ } = {})
32
+ {
33
+ this.method = method;
34
+ this.saveResponse = saveResponse;
35
+ this.resolveOn = resolveOn;
36
+ this.timeout = timeout;
37
+ this.timer = performance.now();
38
+ this.outputStream = outputStream;
39
+ this.useInternalApi = useInternalApi;
40
+ this.secret = secret;
41
+ this.restOptions = restOptions;
42
+ this.suppressLogs = suppressLogs;
43
+ this.followRedirects = followRedirects;
44
+
45
+ this.app = getApp();
46
+
47
+ this.setPath(path);
48
+
49
+ this.setHeaders({
50
+ Accept: 'application/json',
51
+ 'Content-Type': 'application/json',
52
+ 'user-agent': this.app.client.appIdentifier,
53
+ ...headers,
54
+ });
55
+
56
+ if (formData ?? inputStream)
57
+ {
58
+ this.inputStream = formData ?? inputStream;
59
+ }
60
+ else if (body)
61
+ {
62
+ this.body = body;
63
+ }
64
+ }
65
+
66
+ get fullPath()
67
+ {
68
+ return this.baseUrl + this.path;
69
+ }
70
+
71
+ get serviceName()
72
+ {
73
+ return `backend/api/${this.constructor.name}`;
74
+ }
75
+
76
+ get requestOptions()
77
+ {
78
+ return {
79
+ method: this.method,
80
+ headers: omitBy(this.requestHeaders, isNil),
81
+ followRedirects: this.followRedirects,
82
+ ...this.restOptions,
83
+ };
84
+ }
85
+
86
+ setPath(path)
87
+ {
88
+ this.path = path;
89
+
90
+ return this;
91
+ }
92
+
93
+ setBaseUrl(baseUrl)
94
+ {
95
+ this.baseUrl = baseUrl;
96
+
97
+ return this;
98
+ }
99
+
100
+ setHeaders(headers)
101
+ {
102
+ const headerBlacklist = [
103
+ 'host',
104
+ 'x-forwarded-for',
105
+ 'x-forwarded-proto',
106
+ 'x-forwarded-port',
107
+ 'x-amzn-trace-id',
108
+ 'x-envoy-external-address',
109
+ 'x-request-id',
110
+ 'x-envoy-attempt-count',
111
+ 'x-tenant-id',
112
+ 'x-forwarded-client-cert',
113
+ 'x-b3-traceid',
114
+ 'x-b3-spanid',
115
+ 'x-b3-parentspanid',
116
+ 'x-b3-sampled',
117
+ ];
118
+
119
+ Object.keys(headers).forEach(key =>
120
+ {
121
+ if (headerBlacklist.includes(key.toLowerCase()))
122
+ {
123
+ delete headers[key];
124
+ }
125
+ });
126
+
127
+ this.requestHeaders = {
128
+ ...this.requestHeaders,
129
+ ...headers,
130
+ };
131
+
132
+ return this;
133
+ }
134
+
135
+ setAuthorization(Authorization)
136
+ {
137
+ this.requestHeaders.Authorization = Authorization;
138
+
139
+ return this;
140
+ }
141
+
142
+ getResponseHeaders()
143
+ {
144
+ return this.response?.headers;
145
+ }
146
+
147
+ getStatusCode()
148
+ {
149
+ return this.response?.statusCode;
150
+ }
151
+
152
+ handleData(chunk)
153
+ {
154
+ if (this.saveResponse)
155
+ {
156
+ this.data.push(chunk);
157
+ }
158
+
159
+ this.onData(chunk);
160
+ }
161
+
162
+ async finishRequest()
163
+ {
164
+ if (this.checkIfSuccessful())
165
+ {
166
+ const response = this.getData();
167
+
168
+ const message = {
169
+ request: {
170
+ requestUrl: this.fullPath,
171
+ requestOptions: this.requestOptions,
172
+ body: this.body,
173
+ },
174
+ response: this.secret ? maskSpecificKey(response, 'value') : response,
175
+ duration: `${(performance.now() - this.timer).toFixed(2)}ms`,
176
+ retryCount: this.retryCount,
177
+ };
178
+
179
+ await this.log(message);
180
+ }
181
+
182
+ await this.finish('end');
183
+
184
+ this.onEnd();
185
+ }
186
+
187
+ async log(message, level = 'DEBUG')
188
+ {
189
+ if (this.suppressLogs)
190
+ {
191
+ return null;
192
+ }
193
+
194
+ return this.app.log(message, this.serviceName, level);
195
+ }
196
+
197
+ async responseHandler()
198
+ {
199
+ this.data = [];
200
+
201
+ await this.onResponse();
202
+
203
+ await this.finish('response');
204
+ }
205
+
206
+ onBeforeRequest()
207
+ {
208
+ }
209
+
210
+ onData()
211
+ {
212
+ }
213
+
214
+ onEnd()
215
+ {
216
+ }
217
+
218
+ onResponse()
219
+ {
220
+ }
221
+
222
+ onClose()
223
+ {
224
+ }
225
+
226
+ async onError(error)
227
+ {
228
+ const message = {
229
+ request: {
230
+ requestUrl: this.fullPath,
231
+ requestOptions: this.requestOptions,
232
+ body: this.body,
233
+ },
234
+ response: error,
235
+ duration: `${(performance.now() - this.timer).toFixed(2)}ms`,
236
+ retryCount: this.retryCount,
237
+ };
238
+
239
+ const logId = await this.log(message);
240
+
241
+ this.reject(new HttpError(
242
+ 'UNABLE_TO_SEND_REQUEST',
243
+ this.getStatusCode(),
244
+ this.serviceName,
245
+ {
246
+ request: {
247
+ requestUrl: this.fullPath,
248
+ requestOptions: this.requestOptions,
249
+ body: this.body,
250
+ },
251
+ response: {
252
+ data: error,
253
+ },
254
+ },
255
+ logId,
256
+ ));
257
+ }
258
+
259
+ async execute()
260
+ {
261
+ await this.onBeforeRequest();
262
+
263
+ return new Promise((resolve, reject) =>
264
+ {
265
+ this.resolve = resolve;
266
+ this.reject = reject;
267
+
268
+ const protocol = this.fullPath.startsWith('https://') ? https : http;
269
+
270
+ this.request = protocol.request(
271
+ this.fullPath,
272
+ this.requestOptions,
273
+ async response =>
274
+ {
275
+ this.response = response;
276
+
277
+ if (this.outputStream)
278
+ {
279
+ response.pipe(this.outputStream);
280
+ }
281
+
282
+ response.on('data', (...args) => this.handleData(...args));
283
+ response.on('end', (...args) => this.finishRequest(...args));
284
+ response.on('error', (...args) => this.onError(...args));
285
+
286
+ this.responseHandler(response);
287
+ });
288
+
289
+ this.request.on('error', (...args) => this.onError(...args));
290
+ this.request.on('close', (...args) => this.onClose(...args));
291
+
292
+ if (this.timeout)
293
+ {
294
+ this.request.setTimeout(this.timeout, () =>
295
+ {
296
+ this.request.destroy(new HttpError(
297
+ 'REQUEST_TIMEOUT',
298
+ 408,
299
+ this.serviceName,
300
+ {
301
+ requestUrl: this.fullPath,
302
+ },
303
+ undefined,
304
+ undefined,
305
+ ));
306
+ });
307
+ }
308
+
309
+ if (this.inputStream)
310
+ {
311
+ const buffer = [];
312
+
313
+ this.inputStream.on('data', chunk =>
314
+ {
315
+ if (!Buffer.isBuffer(chunk))
316
+ {
317
+ chunk = Buffer.from(chunk);
318
+ }
319
+
320
+ buffer.push(chunk);
321
+ });
322
+
323
+ this.inputStream.on('end', () =>
324
+ {
325
+ this.inputStream = Readable.from(Buffer.concat(buffer));
326
+ });
327
+
328
+ this.inputStream.pipe(this.request);
329
+ }
330
+ else
331
+ {
332
+ if (this.body)
333
+ {
334
+ if (typeof this.body !== 'string')
335
+ {
336
+ this.request.write(JSON.stringify(this.body));
337
+ }
338
+ else
339
+ {
340
+ this.request.write(this.body);
341
+ }
342
+ }
343
+
344
+ this.request.end();
345
+ }
346
+ });
347
+ }
348
+
349
+ getData()
350
+ {
351
+ const data = Buffer.concat(this.data);
352
+
353
+ if (!data.length)
354
+ {
355
+ return null;
356
+ }
357
+
358
+ const responseHeaders = this.getResponseHeaders();
359
+
360
+ if (!responseHeaders['content-type']?.startsWith('application/'))
361
+ {
362
+ return data.toString();
363
+ }
364
+
365
+ if (responseHeaders['content-type']?.startsWith('application/json'))
366
+ {
367
+ return JSON.parse(data);
368
+ }
369
+
370
+ return data;
371
+ }
372
+
373
+ async finish(type)
374
+ {
375
+ if (this.resolveOn !== type)
376
+ {
377
+ return;
378
+ }
379
+
380
+ const statusCode = this.getStatusCode();
381
+ const responseHeaders = this.getResponseHeaders();
382
+
383
+ if (statusCode === 429)
384
+ {
385
+ const retryInSeconds = Number(responseHeaders['x-rate-limit-retry-after-seconds']);
386
+
387
+ setTimeout(() =>
388
+ {
389
+ this.retryCount = (this.retryCount ?? 0) + 1;
390
+
391
+ this.execute()
392
+ .then(this.resolve)
393
+ .catch(this.reject);
394
+ }, Math.max(retryInSeconds * 1000, 6000));
395
+
396
+ return;
397
+ }
398
+
399
+ if (this.checkIfSuccessful())
400
+ {
401
+ this.resolve();
402
+ }
403
+ else
404
+ {
405
+ await this.onError(this.getData());
406
+ }
407
+ }
408
+
409
+ checkIfSuccessful()
410
+ {
411
+ const statusCode = this.getStatusCode();
412
+
413
+ return statusCode >= 200 && statusCode < 400;
414
+ }
415
+ }
416
+
417
+ Api.fetch = fetchFn;
418
+ Api.getResponseStream = getResponseStreamFn;
419
+ Api.gateway = gatewayFn;
420
+ Api.vql = vqlFn;
421
+ Api.redirectRequest = redirectRequestFn;
422
+
423
+ module.exports = Api;
424
+
425
+ function maskSpecificKey(response, keyToMask = 'value', mask = '[secret]')
426
+ {
427
+ if (!response || typeof response !== 'object')
428
+ {
429
+ return response;
430
+ }
431
+
432
+ return Object.keys(response).reduce((acc, key) =>
433
+ {
434
+ if (key === keyToMask)
435
+ {
436
+ acc[key] = mask;
437
+ }
438
+ else if (typeof response[key] === 'object' && response[key] !== null)
439
+ {
440
+ acc[key] = maskSpecificKey(response[key], keyToMask, mask);
441
+ }
442
+ else
443
+ {
444
+ acc[key] = response[key];
445
+ }
446
+
447
+ return acc;
448
+ }, {});
449
+ }
package/api/ErpApi.js ADDED
@@ -0,0 +1,94 @@
1
+ const Api = require('#backend/api/Api.js');
2
+ const { getTenant } = require('#backend/utils/context.js');
3
+
4
+ const PromiseSingletonMap = require('#backend/utils/promiseSingletonMap.js');
5
+ const refreshAccessToken = require('#backend/utils/keycloak.js');
6
+ const Eav = require('#backend/api/modules/eav.js');
7
+ const Migration = require('#backend/api/modules/migration.js');
8
+ const TextEnum = require('#backend/api/modules/textEnum.js');
9
+ const Webhook = require('#backend/api/modules/webhook.js');
10
+ const { validateOfflineToken } = require('#backend/utils/token.js');
11
+
12
+ const singletonPromise = new PromiseSingletonMap();
13
+ class ErpApi extends Api
14
+ {
15
+ async onBeforeRequest()
16
+ {
17
+ const { baseUrl, Authorization } = await this.getAuthorization();
18
+
19
+ this.setAuthorization(Authorization);
20
+
21
+ if (this.useInternalApi)
22
+ {
23
+ this.setBaseUrl(`${baseUrl}/api/vario`);
24
+ }
25
+ else
26
+ {
27
+ this.setBaseUrl(`${baseUrl}/api/vario/community/${this.app.version}`);
28
+ }
29
+ }
30
+
31
+ async onResponse()
32
+ {
33
+ const statusCode = this.getStatusCode();
34
+
35
+ if (statusCode === 401)
36
+ {
37
+ const tenant = getTenant();
38
+
39
+ await this.app.accessToken.delete(tenant);
40
+ }
41
+ }
42
+
43
+ async getAuthorization()
44
+ {
45
+ const tenant = getTenant();
46
+
47
+ return singletonPromise.run(tenant, async () =>
48
+ {
49
+ const savedAccessToken = await this.app.accessToken.get(tenant);
50
+
51
+ if (savedAccessToken)
52
+ {
53
+ const baseUrl = await this.app.baseUrlCache.get();
54
+
55
+ return {
56
+ baseUrl,
57
+ Authorization: `Bearer ${savedAccessToken}`,
58
+ };
59
+ }
60
+
61
+ const offlineToken = await this.app.offlineToken.get(tenant);
62
+ const { iss } = await validateOfflineToken(offlineToken);
63
+
64
+ const domain = iss.replace('https://sso.', '').split('/')[0];
65
+ const baseUrl = `https://${tenant}.${domain}`;
66
+
67
+ await this.app.baseUrlCache.set(baseUrl);
68
+
69
+ const {
70
+ access_token: accessToken,
71
+ expires_in: expiresIn,
72
+ } = await refreshAccessToken(
73
+ offlineToken,
74
+ `${iss}/protocol/openid-connect/token`,
75
+ );
76
+
77
+ const expiresAt = Date.now() + (expiresIn * 0.9) * 1000;
78
+
79
+ this.app.accessToken.set(tenant, accessToken, expiresAt);
80
+
81
+ return {
82
+ baseUrl,
83
+ Authorization: `Bearer ${accessToken}`,
84
+ };
85
+ });
86
+ }
87
+ }
88
+
89
+ ErpApi.migration = new Migration(ErpApi);
90
+ ErpApi.eav = new Eav(ErpApi);
91
+ ErpApi.textenum = new TextEnum(ErpApi);
92
+ ErpApi.webhook = new Webhook(ErpApi);
93
+
94
+ module.exports = ErpApi;
@@ -0,0 +1,10 @@
1
+ async function fetch(path, options = {})
2
+ {
3
+ const apiRequest = new this(path, options);
4
+
5
+ await apiRequest.execute();
6
+
7
+ return { data: apiRequest.getData(), response: apiRequest.response };
8
+ }
9
+
10
+ module.exports = fetch;
@@ -0,0 +1,19 @@
1
+ const { getResponse } = require('#backend/utils/context.js');
2
+
3
+ async function gateway(path, options = {})
4
+ {
5
+ const res = getResponse();
6
+
7
+ options.outputStream = res;
8
+
9
+ const apiRequest = new this(path, options);
10
+
11
+ apiRequest.onResponse = () =>
12
+ {
13
+ res.set(apiRequest.getResponseHeaders());
14
+ };
15
+
16
+ await apiRequest.execute();
17
+ }
18
+
19
+ module.exports = gateway;
@@ -0,0 +1,17 @@
1
+ const { PassThrough } = require('stream');
2
+
3
+ async function getResponseStream(path, options = {})
4
+ {
5
+ const stream = new PassThrough();
6
+
7
+ options.outputStream = stream;
8
+ options.resolveOn = 'response';
9
+
10
+ const apiRequest = new this(path, options);
11
+
12
+ await apiRequest.execute();
13
+
14
+ return stream;
15
+ }
16
+
17
+ module.exports = getResponseStream;
@@ -0,0 +1,13 @@
1
+ const { getRequest } = require('#backend/utils/context.js');
2
+
3
+ async function redirectRequest(path, options = {})
4
+ {
5
+ const req = getRequest();
6
+
7
+ options.headers = req.headers;
8
+ options.inputStream = req;
9
+
10
+ return this.getResponseStream(path, options);
11
+ }
12
+
13
+ module.exports = redirectRequest;
@@ -0,0 +1,61 @@
1
+ async function vql({ statement, variableSubstitutions = [], limit = null, offset = null })
2
+ {
3
+ const payload = {
4
+ statement,
5
+ variableSubstitutionList: {
6
+ variableSubstitutions,
7
+ },
8
+ };
9
+
10
+ if (limit)
11
+ {
12
+ payload.limit = limit;
13
+ }
14
+
15
+ if (offset > 0)
16
+ {
17
+ payload.offset = offset;
18
+ }
19
+
20
+ const result = await this.fetch('/cmn/computed-queries/execute', {
21
+ useInternalApi: true,
22
+ method: 'POST',
23
+ body: JSON.stringify(payload),
24
+ });
25
+
26
+ return {
27
+ ...result.data,
28
+ data: mapDisplayName(result.data.definition, result.data.data),
29
+ moreElements: result.response.headers['x-query-more-elements'],
30
+ nextOffset: result.response.headers['x-query-next-offset'],
31
+ };
32
+ }
33
+
34
+ module.exports = vql;
35
+
36
+ function mapDisplayName(definition, items)
37
+ {
38
+ const transformedArray = [];
39
+
40
+ items.forEach(item =>
41
+ {
42
+ const newItem = {};
43
+
44
+ definition.forEach(def =>
45
+ {
46
+ if (item[def.attribute] !== null && item[def.attribute] !== undefined)
47
+ {
48
+ newItem[def.displayName] = item[def.attribute];
49
+ }
50
+ // Special Case: Computed Array of Objects (i.e. contacts)
51
+ else if (def.definition && def._type === 'meta' && item[def.definition.path])
52
+ {
53
+ newItem[def.definition.displayName] = item[def.definition.path];
54
+ }
55
+ });
56
+
57
+ transformedArray.push(newItem);
58
+ });
59
+
60
+ return transformedArray;
61
+ }