@unito/integration-sdk 1.0.26 → 1.0.28
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/dist/src/helpers.d.ts +2 -2
- package/dist/src/helpers.js +2 -2
- package/dist/src/index.cjs +267 -215
- package/dist/src/integration.js +8 -8
- package/dist/src/middlewares/correlationId.d.ts +2 -2
- package/dist/src/middlewares/correlationId.js +3 -3
- package/dist/src/middlewares/credentials.d.ts +2 -2
- package/dist/src/middlewares/credentials.js +3 -3
- package/dist/src/middlewares/errors.d.ts +2 -2
- package/dist/src/middlewares/errors.js +3 -3
- package/dist/src/middlewares/filters.d.ts +2 -2
- package/dist/src/middlewares/filters.js +4 -4
- package/dist/src/middlewares/finish.d.ts +2 -2
- package/dist/src/middlewares/finish.js +3 -3
- package/dist/src/middlewares/logger.d.ts +2 -2
- package/dist/src/middlewares/logger.js +3 -3
- package/dist/src/middlewares/notFound.d.ts +2 -2
- package/dist/src/middlewares/notFound.js +3 -3
- package/dist/src/middlewares/secrets.d.ts +2 -2
- package/dist/src/middlewares/secrets.js +3 -3
- package/dist/src/middlewares/selects.d.ts +2 -2
- package/dist/src/middlewares/selects.js +3 -3
- package/dist/src/middlewares/signal.d.ts +2 -2
- package/dist/src/middlewares/signal.js +3 -3
- package/dist/src/middlewares/{requestStartTime.d.ts → start.d.ts} +2 -2
- package/dist/src/middlewares/{requestStartTime.js → start.js} +3 -3
- package/dist/src/resources/logger.d.ts +2 -1
- package/dist/src/resources/logger.js +59 -7
- package/dist/test/middlewares/correlationId.test.js +3 -3
- package/dist/test/middlewares/credentials.test.js +4 -4
- package/dist/test/middlewares/errors.test.js +4 -4
- package/dist/test/middlewares/filters.test.js +20 -12
- package/dist/test/middlewares/finish.test.js +4 -4
- package/dist/test/middlewares/logger.test.js +5 -5
- package/dist/test/middlewares/notFound.test.js +2 -2
- package/dist/test/middlewares/secrets.test.js +3 -3
- package/dist/test/middlewares/selects.test.js +3 -3
- package/dist/test/middlewares/signal.test.js +3 -3
- package/dist/test/middlewares/start.test.d.ts +1 -0
- package/dist/test/middlewares/start.test.js +11 -0
- package/dist/test/resources/logger.test.js +71 -45
- package/package.json +1 -1
- package/src/helpers.ts +2 -2
- package/src/integration.ts +8 -8
- package/src/middlewares/correlationId.ts +3 -3
- package/src/middlewares/credentials.ts +3 -3
- package/src/middlewares/errors.ts +3 -3
- package/src/middlewares/filters.ts +4 -4
- package/src/middlewares/finish.ts +3 -3
- package/src/middlewares/logger.ts +3 -3
- package/src/middlewares/notFound.ts +3 -3
- package/src/middlewares/secrets.ts +3 -3
- package/src/middlewares/selects.ts +3 -3
- package/src/middlewares/signal.ts +3 -3
- package/src/middlewares/{requestStartTime.ts → start.ts} +3 -3
- package/src/resources/logger.ts +66 -8
- package/test/middlewares/correlationId.test.ts +3 -3
- package/test/middlewares/credentials.test.ts +4 -4
- package/test/middlewares/errors.test.ts +4 -4
- package/test/middlewares/filters.test.ts +31 -12
- package/test/middlewares/finish.test.ts +4 -4
- package/test/middlewares/logger.test.ts +5 -5
- package/test/middlewares/notFound.test.ts +2 -2
- package/test/middlewares/secrets.test.ts +3 -3
- package/test/middlewares/selects.test.ts +3 -3
- package/test/middlewares/signal.test.ts +3 -3
- package/test/middlewares/start.test.ts +14 -0
- package/test/resources/logger.test.ts +82 -47
package/dist/src/helpers.d.ts
CHANGED
|
@@ -10,6 +10,6 @@ import { Filter } from './index.js';
|
|
|
10
10
|
* @param fields The schema of the item against which the filters are applied
|
|
11
11
|
* @returns The validated filters
|
|
12
12
|
*/
|
|
13
|
-
export declare
|
|
13
|
+
export declare function getApplicableFilters(context: {
|
|
14
14
|
filters: Filter[];
|
|
15
|
-
}, fields: FieldSchema[])
|
|
15
|
+
}, fields: FieldSchema[]): Filter[];
|
package/dist/src/helpers.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* @param fields The schema of the item against which the filters are applied
|
|
9
9
|
* @returns The validated filters
|
|
10
10
|
*/
|
|
11
|
-
export
|
|
11
|
+
export function getApplicableFilters(context, fields) {
|
|
12
12
|
const applicableFilters = [];
|
|
13
13
|
for (const filter of context.filters) {
|
|
14
14
|
let field = undefined;
|
|
@@ -25,4 +25,4 @@ export const getApplicableFilters = (context, fields) => {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
return applicableFilters;
|
|
28
|
-
}
|
|
28
|
+
}
|
package/dist/src/index.cjs
CHANGED
|
@@ -34,6 +34,35 @@ var LogLevel;
|
|
|
34
34
|
LogLevel["LOG"] = "log";
|
|
35
35
|
LogLevel["DEBUG"] = "debug";
|
|
36
36
|
})(LogLevel || (LogLevel = {}));
|
|
37
|
+
/**
|
|
38
|
+
* See https://docs.datadoghq.com/logs/log_collection/?tab=host#custom-log-forwarding
|
|
39
|
+
* - Datadog Agent splits at 256kB (256000 bytes)...
|
|
40
|
+
* - ... but the same docs say that "for optimal performance, it is
|
|
41
|
+
* recommended that an individual log be no greater than 25kB"
|
|
42
|
+
* -> Truncating at 25kB - a bit of wiggle room for metadata = 20kB.
|
|
43
|
+
*/
|
|
44
|
+
const MAX_LOG_MESSAGE_SIZE = parseInt(process.env.MAX_LOG_MESSAGE_SIZE ?? '20000', 10);
|
|
45
|
+
const LOG_LINE_TRUNCATED_SUFFIX = ' - LOG LINE TRUNCATED';
|
|
46
|
+
/**
|
|
47
|
+
* For *LogMeta* sanitization, we let in anything that was passed, except for clearly-problematic keys
|
|
48
|
+
*/
|
|
49
|
+
const LOGMETA_BLACKLIST = [
|
|
50
|
+
// Security
|
|
51
|
+
'access_token',
|
|
52
|
+
'bot_auth_code',
|
|
53
|
+
'client_secret',
|
|
54
|
+
'jwt',
|
|
55
|
+
'oauth_token',
|
|
56
|
+
'password',
|
|
57
|
+
'refresh_token',
|
|
58
|
+
'shared_secret',
|
|
59
|
+
'token',
|
|
60
|
+
// Privacy
|
|
61
|
+
'billing_email',
|
|
62
|
+
'email',
|
|
63
|
+
'first_name',
|
|
64
|
+
'last_name',
|
|
65
|
+
];
|
|
37
66
|
/**
|
|
38
67
|
* Logger class that can be configured with metadata add creation and when logging to add additional context to your logs.
|
|
39
68
|
*/
|
|
@@ -113,20 +142,27 @@ class Logger {
|
|
|
113
142
|
this.metadata = {};
|
|
114
143
|
}
|
|
115
144
|
send(logLevel, message, metadata) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
145
|
+
// We need to provide the date to Datadog. Otherwise, the date is set to when they receive the log.
|
|
146
|
+
const date = Date.now();
|
|
147
|
+
if (message.length > MAX_LOG_MESSAGE_SIZE) {
|
|
148
|
+
message = `${message.substring(0, MAX_LOG_MESSAGE_SIZE)}${LOG_LINE_TRUNCATED_SUFFIX}`;
|
|
149
|
+
}
|
|
150
|
+
let processedMetadata = Logger.snakifyKeys({ ...this.metadata, ...metadata, logMessageSize: message.length });
|
|
151
|
+
processedMetadata = Logger.pruneSensitiveMetadata(processedMetadata);
|
|
152
|
+
const processedLogs = {
|
|
153
|
+
...processedMetadata,
|
|
119
154
|
message,
|
|
155
|
+
date,
|
|
120
156
|
status: logLevel,
|
|
121
|
-
}
|
|
157
|
+
};
|
|
122
158
|
if (process.env.NODE_ENV === 'development') {
|
|
123
|
-
console[logLevel](JSON.stringify(
|
|
159
|
+
console[logLevel](JSON.stringify(processedLogs, null, 2));
|
|
124
160
|
}
|
|
125
161
|
else {
|
|
126
|
-
console[logLevel](JSON.stringify(
|
|
162
|
+
console[logLevel](JSON.stringify(processedLogs));
|
|
127
163
|
}
|
|
128
164
|
}
|
|
129
|
-
snakifyKeys(value) {
|
|
165
|
+
static snakifyKeys(value) {
|
|
130
166
|
const result = {};
|
|
131
167
|
for (const key in value) {
|
|
132
168
|
const deepValue = typeof value[key] === 'object' ? this.snakifyKeys(value[key]) : value[key];
|
|
@@ -135,6 +171,22 @@ class Logger {
|
|
|
135
171
|
}
|
|
136
172
|
return result;
|
|
137
173
|
}
|
|
174
|
+
static pruneSensitiveMetadata(metadata, topLevelMeta) {
|
|
175
|
+
const prunedMetadata = {};
|
|
176
|
+
for (const key in metadata) {
|
|
177
|
+
if (LOGMETA_BLACKLIST.includes(key)) {
|
|
178
|
+
prunedMetadata[key] = '[REDACTED]';
|
|
179
|
+
(topLevelMeta ?? prunedMetadata).has_sensitive_attribute = true;
|
|
180
|
+
}
|
|
181
|
+
else if (typeof metadata[key] === 'object') {
|
|
182
|
+
prunedMetadata[key] = Logger.pruneSensitiveMetadata(metadata[key], topLevelMeta ?? prunedMetadata);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
prunedMetadata[key] = metadata[key];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return prunedMetadata;
|
|
189
|
+
}
|
|
138
190
|
}
|
|
139
191
|
|
|
140
192
|
/**
|
|
@@ -369,201 +421,6 @@ function buildHttpError(responseStatus, message) {
|
|
|
369
421
|
return httpError;
|
|
370
422
|
}
|
|
371
423
|
|
|
372
|
-
const middleware$a = (req, res, next) => {
|
|
373
|
-
res.locals.correlationId = req.header('X-Unito-Correlation-Id') ?? crypto.randomUUID();
|
|
374
|
-
next();
|
|
375
|
-
};
|
|
376
|
-
|
|
377
|
-
const ADDITIONAL_CONTEXT_HEADER = 'X-Unito-Additional-Logging-Context';
|
|
378
|
-
const middleware$9 = (req, res, next) => {
|
|
379
|
-
const logger = new Logger({ correlation_id: res.locals.correlationId });
|
|
380
|
-
res.locals.logger = logger;
|
|
381
|
-
const rawAdditionalContext = req.header(ADDITIONAL_CONTEXT_HEADER);
|
|
382
|
-
if (typeof rawAdditionalContext === 'string') {
|
|
383
|
-
try {
|
|
384
|
-
const additionalContext = JSON.parse(rawAdditionalContext);
|
|
385
|
-
logger.decorate(additionalContext);
|
|
386
|
-
}
|
|
387
|
-
catch (error) {
|
|
388
|
-
logger.warn(`Failed parsing header ${ADDITIONAL_CONTEXT_HEADER}: ${rawAdditionalContext}`);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
next();
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
const CREDENTIALS_HEADER = 'X-Unito-Credentials';
|
|
395
|
-
const middleware$8 = (req, res, next) => {
|
|
396
|
-
const credentialsHeader = req.header(CREDENTIALS_HEADER);
|
|
397
|
-
if (credentialsHeader) {
|
|
398
|
-
let credentials;
|
|
399
|
-
try {
|
|
400
|
-
credentials = JSON.parse(Buffer.from(credentialsHeader, 'base64').toString('utf8'));
|
|
401
|
-
}
|
|
402
|
-
catch {
|
|
403
|
-
throw new BadRequestError(`Malformed HTTP header ${CREDENTIALS_HEADER}`);
|
|
404
|
-
}
|
|
405
|
-
res.locals.credentials = credentials;
|
|
406
|
-
}
|
|
407
|
-
next();
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
const OPERATION_DEADLINE_HEADER = 'X-Unito-Operation-Deadline';
|
|
411
|
-
const middleware$7 = (req, res, next) => {
|
|
412
|
-
const operationDeadlineHeader = Number(req.header(OPERATION_DEADLINE_HEADER));
|
|
413
|
-
if (operationDeadlineHeader) {
|
|
414
|
-
// `operationDeadlineHeader` represents a timestamp in the future, in seconds.
|
|
415
|
-
// We need to convert it to a number of milliseconds.
|
|
416
|
-
const deadline = operationDeadlineHeader * 1000 - Date.now();
|
|
417
|
-
if (deadline > 0) {
|
|
418
|
-
res.locals.signal = AbortSignal.timeout(deadline);
|
|
419
|
-
}
|
|
420
|
-
else {
|
|
421
|
-
throw new TimeoutError('Request already timed out upon reception');
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
else {
|
|
425
|
-
// Default to 20s, which is the maximum time frame allowed for an operation by Unito.
|
|
426
|
-
res.locals.signal = AbortSignal.timeout(20000);
|
|
427
|
-
}
|
|
428
|
-
next();
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
const SECRETS_HEADER = 'X-Unito-Secrets';
|
|
432
|
-
const middleware$6 = (req, res, next) => {
|
|
433
|
-
const secretsHeader = req.header(SECRETS_HEADER);
|
|
434
|
-
if (secretsHeader) {
|
|
435
|
-
let secrets;
|
|
436
|
-
try {
|
|
437
|
-
secrets = JSON.parse(Buffer.from(secretsHeader, 'base64').toString('utf8'));
|
|
438
|
-
}
|
|
439
|
-
catch {
|
|
440
|
-
throw new BadRequestError(`Malformed HTTP header ${SECRETS_HEADER}`);
|
|
441
|
-
}
|
|
442
|
-
res.locals.secrets = secrets;
|
|
443
|
-
}
|
|
444
|
-
next();
|
|
445
|
-
};
|
|
446
|
-
|
|
447
|
-
// The operators are ordered by their symbol length, in descending order.
|
|
448
|
-
// This is necessary because the symbol of an operator can be
|
|
449
|
-
// a subset of the symbol of another operator.
|
|
450
|
-
//
|
|
451
|
-
// For example, the symbol "=" (EQUAL) is a subset of the symbol "!=" (NOT_EQUAL).
|
|
452
|
-
const ORDERED_OPERATORS = Object.values(integrationApi.OperatorType).sort((o1, o2) => o1.length - o2.length);
|
|
453
|
-
const middleware$5 = (req, res, next) => {
|
|
454
|
-
const rawFilters = req.query.filter;
|
|
455
|
-
res.locals.filters = [];
|
|
456
|
-
if (typeof rawFilters === 'string') {
|
|
457
|
-
for (const rawFilter of rawFilters.split(',')) {
|
|
458
|
-
for (const operator of ORDERED_OPERATORS) {
|
|
459
|
-
if (rawFilter.includes(operator)) {
|
|
460
|
-
const [field, valuesRaw] = rawFilter.split(operator, 2);
|
|
461
|
-
const values = valuesRaw ? valuesRaw.split('|').map(decodeURIComponent) : [];
|
|
462
|
-
res.locals.filters.push({ field: field, operator, values });
|
|
463
|
-
break;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
next();
|
|
469
|
-
};
|
|
470
|
-
|
|
471
|
-
const middleware$4 = (req, res, next) => {
|
|
472
|
-
const rawSelect = req.query.select;
|
|
473
|
-
if (typeof rawSelect === 'string') {
|
|
474
|
-
res.locals.selects = rawSelect.split(',');
|
|
475
|
-
}
|
|
476
|
-
else {
|
|
477
|
-
res.locals.selects = [];
|
|
478
|
-
}
|
|
479
|
-
next();
|
|
480
|
-
};
|
|
481
|
-
|
|
482
|
-
const middleware$3 = (err, _req, res, next) => {
|
|
483
|
-
if (res.headersSent) {
|
|
484
|
-
return next(err);
|
|
485
|
-
}
|
|
486
|
-
let error;
|
|
487
|
-
if (err instanceof HttpError) {
|
|
488
|
-
error = {
|
|
489
|
-
code: err.status.toString(),
|
|
490
|
-
message: err.message,
|
|
491
|
-
details: {
|
|
492
|
-
stack: err.stack,
|
|
493
|
-
},
|
|
494
|
-
};
|
|
495
|
-
}
|
|
496
|
-
else {
|
|
497
|
-
error = {
|
|
498
|
-
code: '500',
|
|
499
|
-
message: 'Oops! Something went wrong',
|
|
500
|
-
originalError: {
|
|
501
|
-
code: err.name,
|
|
502
|
-
message: err.message,
|
|
503
|
-
details: {
|
|
504
|
-
stack: err.stack,
|
|
505
|
-
},
|
|
506
|
-
},
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
res.locals.error = structuredClone(error);
|
|
510
|
-
// Keep the stack details in development for the Debugger
|
|
511
|
-
if (process.env.NODE_ENV !== 'development') {
|
|
512
|
-
delete error.details;
|
|
513
|
-
delete error.originalError?.details;
|
|
514
|
-
}
|
|
515
|
-
res.status(Number(error.code)).json(error);
|
|
516
|
-
};
|
|
517
|
-
|
|
518
|
-
const middleware$2 = (req, res, next) => {
|
|
519
|
-
if (req.originalUrl !== '/health') {
|
|
520
|
-
res.on('finish', function () {
|
|
521
|
-
const error = res.locals.error;
|
|
522
|
-
const durationInNs = Number(process.hrtime.bigint() - res.locals.requestStartTime);
|
|
523
|
-
const durationInMs = (durationInNs / 1_000_000) | 0;
|
|
524
|
-
const message = `${req.method} ${req.originalUrl} ${res.statusCode} - ${durationInMs} ms`;
|
|
525
|
-
const metadata = {
|
|
526
|
-
duration: durationInNs,
|
|
527
|
-
// Use reserved and standard attributes of Datadog
|
|
528
|
-
// https://app.datadoghq.com/logs/pipelines/standard-attributes
|
|
529
|
-
http: { method: req.method, status_code: res.statusCode, url_details: { path: req.originalUrl } },
|
|
530
|
-
...(error
|
|
531
|
-
? {
|
|
532
|
-
error: {
|
|
533
|
-
kind: error.message,
|
|
534
|
-
stack: (error.originalError?.details?.stack ?? error.details?.stack),
|
|
535
|
-
message: error.originalError?.message ?? error.message,
|
|
536
|
-
},
|
|
537
|
-
}
|
|
538
|
-
: {}),
|
|
539
|
-
};
|
|
540
|
-
if ([404, 429].includes(res.statusCode)) {
|
|
541
|
-
res.locals.logger.warn(message, metadata);
|
|
542
|
-
}
|
|
543
|
-
else if (res.statusCode >= 400) {
|
|
544
|
-
res.locals.logger.error(message, metadata);
|
|
545
|
-
}
|
|
546
|
-
else {
|
|
547
|
-
res.locals.logger.info(message, metadata);
|
|
548
|
-
}
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
|
-
next();
|
|
552
|
-
};
|
|
553
|
-
|
|
554
|
-
const middleware$1 = (req, res, _next) => {
|
|
555
|
-
const error = {
|
|
556
|
-
code: '404',
|
|
557
|
-
message: `Path ${req.path} not found.`,
|
|
558
|
-
};
|
|
559
|
-
res.status(404).json(error);
|
|
560
|
-
};
|
|
561
|
-
|
|
562
|
-
const middleware = (_, res, next) => {
|
|
563
|
-
res.locals.requestStartTime = process.hrtime.bigint();
|
|
564
|
-
next();
|
|
565
|
-
};
|
|
566
|
-
|
|
567
424
|
function assertValidPath(path) {
|
|
568
425
|
if (!path.startsWith('/')) {
|
|
569
426
|
throw new InvalidHandler(`The provided path '${path}' is invalid. All paths must start with a '/'.`);
|
|
@@ -896,6 +753,201 @@ class Handler {
|
|
|
896
753
|
}
|
|
897
754
|
}
|
|
898
755
|
|
|
756
|
+
function extractCorrelationId(req, res, next) {
|
|
757
|
+
res.locals.correlationId = req.header('X-Unito-Correlation-Id') ?? crypto.randomUUID();
|
|
758
|
+
next();
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const CREDENTIALS_HEADER = 'X-Unito-Credentials';
|
|
762
|
+
function extractCredentials(req, res, next) {
|
|
763
|
+
const credentialsHeader = req.header(CREDENTIALS_HEADER);
|
|
764
|
+
if (credentialsHeader) {
|
|
765
|
+
let credentials;
|
|
766
|
+
try {
|
|
767
|
+
credentials = JSON.parse(Buffer.from(credentialsHeader, 'base64').toString('utf8'));
|
|
768
|
+
}
|
|
769
|
+
catch {
|
|
770
|
+
throw new BadRequestError(`Malformed HTTP header ${CREDENTIALS_HEADER}`);
|
|
771
|
+
}
|
|
772
|
+
res.locals.credentials = credentials;
|
|
773
|
+
}
|
|
774
|
+
next();
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function onError(err, _req, res, next) {
|
|
778
|
+
if (res.headersSent) {
|
|
779
|
+
return next(err);
|
|
780
|
+
}
|
|
781
|
+
let error;
|
|
782
|
+
if (err instanceof HttpError) {
|
|
783
|
+
error = {
|
|
784
|
+
code: err.status.toString(),
|
|
785
|
+
message: err.message,
|
|
786
|
+
details: {
|
|
787
|
+
stack: err.stack,
|
|
788
|
+
},
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
error = {
|
|
793
|
+
code: '500',
|
|
794
|
+
message: 'Oops! Something went wrong',
|
|
795
|
+
originalError: {
|
|
796
|
+
code: err.name,
|
|
797
|
+
message: err.message,
|
|
798
|
+
details: {
|
|
799
|
+
stack: err.stack,
|
|
800
|
+
},
|
|
801
|
+
},
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
res.locals.error = structuredClone(error);
|
|
805
|
+
// Keep the stack details in development for the Debugger
|
|
806
|
+
if (process.env.NODE_ENV !== 'development') {
|
|
807
|
+
delete error.details;
|
|
808
|
+
delete error.originalError?.details;
|
|
809
|
+
}
|
|
810
|
+
res.status(Number(error.code)).json(error);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// The operators are ordered by their symbol length, in descending order.
|
|
814
|
+
// This is necessary because the symbol of an operator can be
|
|
815
|
+
// a subset of the symbol of another operator.
|
|
816
|
+
//
|
|
817
|
+
// For example, the symbol "=" (EQUAL) is a subset of the symbol "!=" (NOT_EQUAL).
|
|
818
|
+
const ORDERED_OPERATORS = Object.values(integrationApi.OperatorType).sort((o1, o2) => o2.length - o1.length);
|
|
819
|
+
function extractFilters(req, res, next) {
|
|
820
|
+
const rawFilters = req.query.filter;
|
|
821
|
+
res.locals.filters = [];
|
|
822
|
+
if (typeof rawFilters === 'string') {
|
|
823
|
+
for (const rawFilter of rawFilters.split(',')) {
|
|
824
|
+
for (const operator of ORDERED_OPERATORS) {
|
|
825
|
+
if (rawFilter.includes(operator)) {
|
|
826
|
+
const [field, valuesRaw] = rawFilter.split(operator, 2);
|
|
827
|
+
const values = valuesRaw ? valuesRaw.split('|').map(decodeURIComponent) : [];
|
|
828
|
+
res.locals.filters.push({ field: field, operator, values });
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
next();
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function onFinish(req, res, next) {
|
|
838
|
+
if (req.originalUrl !== '/health') {
|
|
839
|
+
res.on('finish', function () {
|
|
840
|
+
const error = res.locals.error;
|
|
841
|
+
const durationInNs = Number(process.hrtime.bigint() - res.locals.requestStartTime);
|
|
842
|
+
const durationInMs = (durationInNs / 1_000_000) | 0;
|
|
843
|
+
const message = `${req.method} ${req.originalUrl} ${res.statusCode} - ${durationInMs} ms`;
|
|
844
|
+
const metadata = {
|
|
845
|
+
duration: durationInNs,
|
|
846
|
+
// Use reserved and standard attributes of Datadog
|
|
847
|
+
// https://app.datadoghq.com/logs/pipelines/standard-attributes
|
|
848
|
+
http: { method: req.method, status_code: res.statusCode, url_details: { path: req.originalUrl } },
|
|
849
|
+
...(error
|
|
850
|
+
? {
|
|
851
|
+
error: {
|
|
852
|
+
kind: error.message,
|
|
853
|
+
stack: (error.originalError?.details?.stack ?? error.details?.stack),
|
|
854
|
+
message: error.originalError?.message ?? error.message,
|
|
855
|
+
},
|
|
856
|
+
}
|
|
857
|
+
: {}),
|
|
858
|
+
};
|
|
859
|
+
if ([404, 429].includes(res.statusCode)) {
|
|
860
|
+
res.locals.logger.warn(message, metadata);
|
|
861
|
+
}
|
|
862
|
+
else if (res.statusCode >= 400) {
|
|
863
|
+
res.locals.logger.error(message, metadata);
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
res.locals.logger.info(message, metadata);
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
next();
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function notFound(req, res, _next) {
|
|
874
|
+
const error = {
|
|
875
|
+
code: '404',
|
|
876
|
+
message: `Path ${req.path} not found.`,
|
|
877
|
+
};
|
|
878
|
+
res.status(404).json(error);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const ADDITIONAL_CONTEXT_HEADER = 'X-Unito-Additional-Logging-Context';
|
|
882
|
+
function injectLogger(req, res, next) {
|
|
883
|
+
const logger = new Logger({ correlation_id: res.locals.correlationId });
|
|
884
|
+
res.locals.logger = logger;
|
|
885
|
+
const rawAdditionalContext = req.header(ADDITIONAL_CONTEXT_HEADER);
|
|
886
|
+
if (typeof rawAdditionalContext === 'string') {
|
|
887
|
+
try {
|
|
888
|
+
const additionalContext = JSON.parse(rawAdditionalContext);
|
|
889
|
+
logger.decorate(additionalContext);
|
|
890
|
+
}
|
|
891
|
+
catch (error) {
|
|
892
|
+
logger.warn(`Failed parsing header ${ADDITIONAL_CONTEXT_HEADER}: ${rawAdditionalContext}`);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
next();
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function start(_, res, next) {
|
|
899
|
+
res.locals.requestStartTime = process.hrtime.bigint();
|
|
900
|
+
next();
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const SECRETS_HEADER = 'X-Unito-Secrets';
|
|
904
|
+
function extractSecrets(req, res, next) {
|
|
905
|
+
const secretsHeader = req.header(SECRETS_HEADER);
|
|
906
|
+
if (secretsHeader) {
|
|
907
|
+
let secrets;
|
|
908
|
+
try {
|
|
909
|
+
secrets = JSON.parse(Buffer.from(secretsHeader, 'base64').toString('utf8'));
|
|
910
|
+
}
|
|
911
|
+
catch {
|
|
912
|
+
throw new BadRequestError(`Malformed HTTP header ${SECRETS_HEADER}`);
|
|
913
|
+
}
|
|
914
|
+
res.locals.secrets = secrets;
|
|
915
|
+
}
|
|
916
|
+
next();
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function extractSelects(req, res, next) {
|
|
920
|
+
const rawSelect = req.query.select;
|
|
921
|
+
if (typeof rawSelect === 'string') {
|
|
922
|
+
res.locals.selects = rawSelect.split(',');
|
|
923
|
+
}
|
|
924
|
+
else {
|
|
925
|
+
res.locals.selects = [];
|
|
926
|
+
}
|
|
927
|
+
next();
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const OPERATION_DEADLINE_HEADER = 'X-Unito-Operation-Deadline';
|
|
931
|
+
function extractOperationDeadline(req, res, next) {
|
|
932
|
+
const operationDeadlineHeader = Number(req.header(OPERATION_DEADLINE_HEADER));
|
|
933
|
+
if (operationDeadlineHeader) {
|
|
934
|
+
// `operationDeadlineHeader` represents a timestamp in the future, in seconds.
|
|
935
|
+
// We need to convert it to a number of milliseconds.
|
|
936
|
+
const deadline = operationDeadlineHeader * 1000 - Date.now();
|
|
937
|
+
if (deadline > 0) {
|
|
938
|
+
res.locals.signal = AbortSignal.timeout(deadline);
|
|
939
|
+
}
|
|
940
|
+
else {
|
|
941
|
+
throw new TimeoutError('Request already timed out upon reception');
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
else {
|
|
945
|
+
// Default to 20s, which is the maximum time frame allowed for an operation by Unito.
|
|
946
|
+
res.locals.signal = AbortSignal.timeout(20000);
|
|
947
|
+
}
|
|
948
|
+
next();
|
|
949
|
+
}
|
|
950
|
+
|
|
899
951
|
function printErrorMessage(message) {
|
|
900
952
|
console.error();
|
|
901
953
|
console.error(`\x1b[31m Oops! Something went wrong! \x1b[0m`);
|
|
@@ -997,11 +1049,11 @@ class Integration {
|
|
|
997
1049
|
app.set('query parser', 'extended');
|
|
998
1050
|
app.use(express.json());
|
|
999
1051
|
// Must be one of the first handlers (to catch all the errors).
|
|
1000
|
-
app.use(
|
|
1052
|
+
app.use(onFinish);
|
|
1001
1053
|
// Instantiate internal middlewares.
|
|
1002
|
-
app.use(
|
|
1003
|
-
app.use(
|
|
1004
|
-
app.use(
|
|
1054
|
+
app.use(start);
|
|
1055
|
+
app.use(extractCorrelationId);
|
|
1056
|
+
app.use(injectLogger);
|
|
1005
1057
|
// Making sure we log all incoming requests (except to '/health'), prior any processing.
|
|
1006
1058
|
app.use((req, res, next) => {
|
|
1007
1059
|
if (req.originalUrl !== '/health') {
|
|
@@ -1011,11 +1063,11 @@ class Integration {
|
|
|
1011
1063
|
});
|
|
1012
1064
|
// Instantiate application middlewares. These can throw, so they have an implicit dependency on the internal
|
|
1013
1065
|
// middlewares such as the logger, the correlationId, and the error handling.
|
|
1014
|
-
app.use(
|
|
1015
|
-
app.use(
|
|
1016
|
-
app.use(
|
|
1017
|
-
app.use(
|
|
1018
|
-
app.use(
|
|
1066
|
+
app.use(extractCredentials);
|
|
1067
|
+
app.use(extractSecrets);
|
|
1068
|
+
app.use(extractFilters);
|
|
1069
|
+
app.use(extractSelects);
|
|
1070
|
+
app.use(extractOperationDeadline);
|
|
1019
1071
|
// Load handlers as needed.
|
|
1020
1072
|
if (this.handlers.length) {
|
|
1021
1073
|
for (const handler of this.handlers) {
|
|
@@ -1031,9 +1083,9 @@ class Integration {
|
|
|
1031
1083
|
process.exit(1);
|
|
1032
1084
|
}
|
|
1033
1085
|
// Must be the (last - 1) handler.
|
|
1034
|
-
app.use(
|
|
1086
|
+
app.use(onError);
|
|
1035
1087
|
// Must be the last handler.
|
|
1036
|
-
app.use(
|
|
1088
|
+
app.use(notFound);
|
|
1037
1089
|
// Start the server.
|
|
1038
1090
|
this.instance = app.listen(this.port, () => console.info(`Server started on port ${this.port}.`));
|
|
1039
1091
|
}
|
|
@@ -1358,7 +1410,7 @@ class Provider {
|
|
|
1358
1410
|
* @param fields The schema of the item against which the filters are applied
|
|
1359
1411
|
* @returns The validated filters
|
|
1360
1412
|
*/
|
|
1361
|
-
|
|
1413
|
+
function getApplicableFilters(context, fields) {
|
|
1362
1414
|
const applicableFilters = [];
|
|
1363
1415
|
for (const filter of context.filters) {
|
|
1364
1416
|
let field = undefined;
|
|
@@ -1375,7 +1427,7 @@ const getApplicableFilters = (context, fields) => {
|
|
|
1375
1427
|
}
|
|
1376
1428
|
}
|
|
1377
1429
|
return applicableFilters;
|
|
1378
|
-
}
|
|
1430
|
+
}
|
|
1379
1431
|
|
|
1380
1432
|
exports.Api = integrationApi__namespace;
|
|
1381
1433
|
exports.Cache = Cache;
|
package/dist/src/integration.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import { InvalidHandler } from './errors.js';
|
|
3
|
+
import { Handler } from './handler.js';
|
|
3
4
|
import correlationIdMiddleware from './middlewares/correlationId.js';
|
|
4
|
-
import loggerMiddleware from './middlewares/logger.js';
|
|
5
5
|
import credentialsMiddleware from './middlewares/credentials.js';
|
|
6
|
-
import signalMiddleware from './middlewares/signal.js';
|
|
7
|
-
import secretsMiddleware from './middlewares/secrets.js';
|
|
8
|
-
import filtersMiddleware from './middlewares/filters.js';
|
|
9
|
-
import selectsMiddleware from './middlewares/selects.js';
|
|
10
6
|
import errorsMiddleware from './middlewares/errors.js';
|
|
7
|
+
import filtersMiddleware from './middlewares/filters.js';
|
|
11
8
|
import finishMiddleware from './middlewares/finish.js';
|
|
12
9
|
import notFoundMiddleware from './middlewares/notFound.js';
|
|
13
|
-
import
|
|
14
|
-
import
|
|
10
|
+
import loggerMiddleware from './middlewares/logger.js';
|
|
11
|
+
import startMiddleware from './middlewares/start.js';
|
|
12
|
+
import secretsMiddleware from './middlewares/secrets.js';
|
|
13
|
+
import selectsMiddleware from './middlewares/selects.js';
|
|
14
|
+
import signalMiddleware from './middlewares/signal.js';
|
|
15
15
|
function printErrorMessage(message) {
|
|
16
16
|
console.error();
|
|
17
17
|
console.error(`\x1b[31m Oops! Something went wrong! \x1b[0m`);
|
|
@@ -115,7 +115,7 @@ export default class Integration {
|
|
|
115
115
|
// Must be one of the first handlers (to catch all the errors).
|
|
116
116
|
app.use(finishMiddleware);
|
|
117
117
|
// Instantiate internal middlewares.
|
|
118
|
-
app.use(
|
|
118
|
+
app.use(startMiddleware);
|
|
119
119
|
app.use(correlationIdMiddleware);
|
|
120
120
|
app.use(loggerMiddleware);
|
|
121
121
|
// Making sure we log all incoming requests (except to '/health'), prior any processing.
|
|
@@ -6,5 +6,5 @@ declare global {
|
|
|
6
6
|
}
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
|
-
declare
|
|
10
|
-
export default
|
|
9
|
+
declare function extractCorrelationId(req: Request, res: Response, next: NextFunction): void;
|
|
10
|
+
export default extractCorrelationId;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
|
-
|
|
2
|
+
function extractCorrelationId(req, res, next) {
|
|
3
3
|
res.locals.correlationId = req.header('X-Unito-Correlation-Id') ?? crypto.randomUUID();
|
|
4
4
|
next();
|
|
5
|
-
}
|
|
6
|
-
export default
|
|
5
|
+
}
|
|
6
|
+
export default extractCorrelationId;
|
|
@@ -10,5 +10,5 @@ export type Credentials = {
|
|
|
10
10
|
accessToken?: string;
|
|
11
11
|
[keys: string]: unknown;
|
|
12
12
|
};
|
|
13
|
-
declare
|
|
14
|
-
export default
|
|
13
|
+
declare function extractCredentials(req: Request, res: Response, next: NextFunction): void;
|
|
14
|
+
export default extractCredentials;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BadRequestError } from '../httpErrors.js';
|
|
2
2
|
const CREDENTIALS_HEADER = 'X-Unito-Credentials';
|
|
3
|
-
|
|
3
|
+
function extractCredentials(req, res, next) {
|
|
4
4
|
const credentialsHeader = req.header(CREDENTIALS_HEADER);
|
|
5
5
|
if (credentialsHeader) {
|
|
6
6
|
let credentials;
|
|
@@ -13,5 +13,5 @@ const middleware = (req, res, next) => {
|
|
|
13
13
|
res.locals.credentials = credentials;
|
|
14
14
|
}
|
|
15
15
|
next();
|
|
16
|
-
}
|
|
17
|
-
export default
|
|
16
|
+
}
|
|
17
|
+
export default extractCredentials;
|
|
@@ -7,5 +7,5 @@ declare global {
|
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
|
-
declare
|
|
11
|
-
export default
|
|
10
|
+
declare function onError(err: Error, _req: Request, res: Response, next: NextFunction): void;
|
|
11
|
+
export default onError;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HttpError } from '../httpErrors.js';
|
|
2
|
-
|
|
2
|
+
function onError(err, _req, res, next) {
|
|
3
3
|
if (res.headersSent) {
|
|
4
4
|
return next(err);
|
|
5
5
|
}
|
|
@@ -33,5 +33,5 @@ const middleware = (err, _req, res, next) => {
|
|
|
33
33
|
delete error.originalError?.details;
|
|
34
34
|
}
|
|
35
35
|
res.status(Number(error.code)).json(error);
|
|
36
|
-
}
|
|
37
|
-
export default
|
|
36
|
+
}
|
|
37
|
+
export default onError;
|