@unito/integration-sdk 1.0.27 → 1.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/dist/src/errors.js +4 -1
- package/dist/src/helpers.d.ts +2 -2
- package/dist/src/helpers.js +2 -2
- package/dist/src/httpErrors.d.ts +6 -0
- package/dist/src/httpErrors.js +8 -0
- package/dist/src/index.cjs +221 -209
- 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/test/errors.test.js +1 -1
- 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/package.json +1 -1
- package/src/errors.ts +3 -1
- package/src/helpers.ts +2 -2
- package/src/httpErrors.ts +9 -0
- 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/test/errors.test.ts +1 -1
- 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/dist/src/errors.js
CHANGED
|
@@ -13,9 +13,12 @@ export function buildHttpError(responseStatus, message) {
|
|
|
13
13
|
if (responseStatus === 400) {
|
|
14
14
|
httpError = new HttpErrors.BadRequestError(message);
|
|
15
15
|
}
|
|
16
|
-
else if (responseStatus === 401
|
|
16
|
+
else if (responseStatus === 401) {
|
|
17
17
|
httpError = new HttpErrors.UnauthorizedError(message);
|
|
18
18
|
}
|
|
19
|
+
else if (responseStatus === 403) {
|
|
20
|
+
httpError = new HttpErrors.ForbiddenError(message);
|
|
21
|
+
}
|
|
19
22
|
else if (responseStatus === 404) {
|
|
20
23
|
httpError = new HttpErrors.NotFoundError(message);
|
|
21
24
|
}
|
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/httpErrors.d.ts
CHANGED
|
@@ -21,6 +21,12 @@ export declare class BadRequestError extends HttpError {
|
|
|
21
21
|
export declare class UnauthorizedError extends HttpError {
|
|
22
22
|
constructor(message?: string);
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Used to generate a 403 Forbidden. Usually used when user lacks sufficient permission to access a ressource.
|
|
26
|
+
*/
|
|
27
|
+
export declare class ForbiddenError extends HttpError {
|
|
28
|
+
constructor(message?: string);
|
|
29
|
+
}
|
|
24
30
|
/**
|
|
25
31
|
* Used to generate a 404 Not Found. Usually used when the requested `Item` is not found.
|
|
26
32
|
*/
|
package/dist/src/httpErrors.js
CHANGED
|
@@ -29,6 +29,14 @@ export class UnauthorizedError extends HttpError {
|
|
|
29
29
|
super(message || 'Unauthorized', 401);
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Used to generate a 403 Forbidden. Usually used when user lacks sufficient permission to access a ressource.
|
|
34
|
+
*/
|
|
35
|
+
export class ForbiddenError extends HttpError {
|
|
36
|
+
constructor(message) {
|
|
37
|
+
super(message || 'Forbidden', 403);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
32
40
|
/**
|
|
33
41
|
* Used to generate a 404 Not Found. Usually used when the requested `Item` is not found.
|
|
34
42
|
*/
|
package/dist/src/index.cjs
CHANGED
|
@@ -315,6 +315,14 @@ class UnauthorizedError extends HttpError {
|
|
|
315
315
|
super(message || 'Unauthorized', 401);
|
|
316
316
|
}
|
|
317
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* Used to generate a 403 Forbidden. Usually used when user lacks sufficient permission to access a ressource.
|
|
320
|
+
*/
|
|
321
|
+
class ForbiddenError extends HttpError {
|
|
322
|
+
constructor(message) {
|
|
323
|
+
super(message || 'Forbidden', 403);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
318
326
|
/**
|
|
319
327
|
* Used to generate a 404 Not Found. Usually used when the requested `Item` is not found.
|
|
320
328
|
*/
|
|
@@ -370,6 +378,7 @@ class RateLimitExceededError extends HttpError {
|
|
|
370
378
|
var httpErrors = /*#__PURE__*/Object.freeze({
|
|
371
379
|
__proto__: null,
|
|
372
380
|
BadRequestError: BadRequestError,
|
|
381
|
+
ForbiddenError: ForbiddenError,
|
|
373
382
|
HttpError: HttpError,
|
|
374
383
|
NotFoundError: NotFoundError,
|
|
375
384
|
ProviderInstanceLockedError: ProviderInstanceLockedError,
|
|
@@ -394,9 +403,12 @@ function buildHttpError(responseStatus, message) {
|
|
|
394
403
|
if (responseStatus === 400) {
|
|
395
404
|
httpError = new BadRequestError(message);
|
|
396
405
|
}
|
|
397
|
-
else if (responseStatus === 401
|
|
406
|
+
else if (responseStatus === 401) {
|
|
398
407
|
httpError = new UnauthorizedError(message);
|
|
399
408
|
}
|
|
409
|
+
else if (responseStatus === 403) {
|
|
410
|
+
httpError = new ForbiddenError(message);
|
|
411
|
+
}
|
|
400
412
|
else if (responseStatus === 404) {
|
|
401
413
|
httpError = new NotFoundError(message);
|
|
402
414
|
}
|
|
@@ -421,201 +433,6 @@ function buildHttpError(responseStatus, message) {
|
|
|
421
433
|
return httpError;
|
|
422
434
|
}
|
|
423
435
|
|
|
424
|
-
const middleware$a = (req, res, next) => {
|
|
425
|
-
res.locals.correlationId = req.header('X-Unito-Correlation-Id') ?? crypto.randomUUID();
|
|
426
|
-
next();
|
|
427
|
-
};
|
|
428
|
-
|
|
429
|
-
const ADDITIONAL_CONTEXT_HEADER = 'X-Unito-Additional-Logging-Context';
|
|
430
|
-
const middleware$9 = (req, res, next) => {
|
|
431
|
-
const logger = new Logger({ correlation_id: res.locals.correlationId });
|
|
432
|
-
res.locals.logger = logger;
|
|
433
|
-
const rawAdditionalContext = req.header(ADDITIONAL_CONTEXT_HEADER);
|
|
434
|
-
if (typeof rawAdditionalContext === 'string') {
|
|
435
|
-
try {
|
|
436
|
-
const additionalContext = JSON.parse(rawAdditionalContext);
|
|
437
|
-
logger.decorate(additionalContext);
|
|
438
|
-
}
|
|
439
|
-
catch (error) {
|
|
440
|
-
logger.warn(`Failed parsing header ${ADDITIONAL_CONTEXT_HEADER}: ${rawAdditionalContext}`);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
next();
|
|
444
|
-
};
|
|
445
|
-
|
|
446
|
-
const CREDENTIALS_HEADER = 'X-Unito-Credentials';
|
|
447
|
-
const middleware$8 = (req, res, next) => {
|
|
448
|
-
const credentialsHeader = req.header(CREDENTIALS_HEADER);
|
|
449
|
-
if (credentialsHeader) {
|
|
450
|
-
let credentials;
|
|
451
|
-
try {
|
|
452
|
-
credentials = JSON.parse(Buffer.from(credentialsHeader, 'base64').toString('utf8'));
|
|
453
|
-
}
|
|
454
|
-
catch {
|
|
455
|
-
throw new BadRequestError(`Malformed HTTP header ${CREDENTIALS_HEADER}`);
|
|
456
|
-
}
|
|
457
|
-
res.locals.credentials = credentials;
|
|
458
|
-
}
|
|
459
|
-
next();
|
|
460
|
-
};
|
|
461
|
-
|
|
462
|
-
const OPERATION_DEADLINE_HEADER = 'X-Unito-Operation-Deadline';
|
|
463
|
-
const middleware$7 = (req, res, next) => {
|
|
464
|
-
const operationDeadlineHeader = Number(req.header(OPERATION_DEADLINE_HEADER));
|
|
465
|
-
if (operationDeadlineHeader) {
|
|
466
|
-
// `operationDeadlineHeader` represents a timestamp in the future, in seconds.
|
|
467
|
-
// We need to convert it to a number of milliseconds.
|
|
468
|
-
const deadline = operationDeadlineHeader * 1000 - Date.now();
|
|
469
|
-
if (deadline > 0) {
|
|
470
|
-
res.locals.signal = AbortSignal.timeout(deadline);
|
|
471
|
-
}
|
|
472
|
-
else {
|
|
473
|
-
throw new TimeoutError('Request already timed out upon reception');
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
else {
|
|
477
|
-
// Default to 20s, which is the maximum time frame allowed for an operation by Unito.
|
|
478
|
-
res.locals.signal = AbortSignal.timeout(20000);
|
|
479
|
-
}
|
|
480
|
-
next();
|
|
481
|
-
};
|
|
482
|
-
|
|
483
|
-
const SECRETS_HEADER = 'X-Unito-Secrets';
|
|
484
|
-
const middleware$6 = (req, res, next) => {
|
|
485
|
-
const secretsHeader = req.header(SECRETS_HEADER);
|
|
486
|
-
if (secretsHeader) {
|
|
487
|
-
let secrets;
|
|
488
|
-
try {
|
|
489
|
-
secrets = JSON.parse(Buffer.from(secretsHeader, 'base64').toString('utf8'));
|
|
490
|
-
}
|
|
491
|
-
catch {
|
|
492
|
-
throw new BadRequestError(`Malformed HTTP header ${SECRETS_HEADER}`);
|
|
493
|
-
}
|
|
494
|
-
res.locals.secrets = secrets;
|
|
495
|
-
}
|
|
496
|
-
next();
|
|
497
|
-
};
|
|
498
|
-
|
|
499
|
-
// The operators are ordered by their symbol length, in descending order.
|
|
500
|
-
// This is necessary because the symbol of an operator can be
|
|
501
|
-
// a subset of the symbol of another operator.
|
|
502
|
-
//
|
|
503
|
-
// For example, the symbol "=" (EQUAL) is a subset of the symbol "!=" (NOT_EQUAL).
|
|
504
|
-
const ORDERED_OPERATORS = Object.values(integrationApi.OperatorType).sort((o1, o2) => o1.length - o2.length);
|
|
505
|
-
const middleware$5 = (req, res, next) => {
|
|
506
|
-
const rawFilters = req.query.filter;
|
|
507
|
-
res.locals.filters = [];
|
|
508
|
-
if (typeof rawFilters === 'string') {
|
|
509
|
-
for (const rawFilter of rawFilters.split(',')) {
|
|
510
|
-
for (const operator of ORDERED_OPERATORS) {
|
|
511
|
-
if (rawFilter.includes(operator)) {
|
|
512
|
-
const [field, valuesRaw] = rawFilter.split(operator, 2);
|
|
513
|
-
const values = valuesRaw ? valuesRaw.split('|').map(decodeURIComponent) : [];
|
|
514
|
-
res.locals.filters.push({ field: field, operator, values });
|
|
515
|
-
break;
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
next();
|
|
521
|
-
};
|
|
522
|
-
|
|
523
|
-
const middleware$4 = (req, res, next) => {
|
|
524
|
-
const rawSelect = req.query.select;
|
|
525
|
-
if (typeof rawSelect === 'string') {
|
|
526
|
-
res.locals.selects = rawSelect.split(',');
|
|
527
|
-
}
|
|
528
|
-
else {
|
|
529
|
-
res.locals.selects = [];
|
|
530
|
-
}
|
|
531
|
-
next();
|
|
532
|
-
};
|
|
533
|
-
|
|
534
|
-
const middleware$3 = (err, _req, res, next) => {
|
|
535
|
-
if (res.headersSent) {
|
|
536
|
-
return next(err);
|
|
537
|
-
}
|
|
538
|
-
let error;
|
|
539
|
-
if (err instanceof HttpError) {
|
|
540
|
-
error = {
|
|
541
|
-
code: err.status.toString(),
|
|
542
|
-
message: err.message,
|
|
543
|
-
details: {
|
|
544
|
-
stack: err.stack,
|
|
545
|
-
},
|
|
546
|
-
};
|
|
547
|
-
}
|
|
548
|
-
else {
|
|
549
|
-
error = {
|
|
550
|
-
code: '500',
|
|
551
|
-
message: 'Oops! Something went wrong',
|
|
552
|
-
originalError: {
|
|
553
|
-
code: err.name,
|
|
554
|
-
message: err.message,
|
|
555
|
-
details: {
|
|
556
|
-
stack: err.stack,
|
|
557
|
-
},
|
|
558
|
-
},
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
|
-
res.locals.error = structuredClone(error);
|
|
562
|
-
// Keep the stack details in development for the Debugger
|
|
563
|
-
if (process.env.NODE_ENV !== 'development') {
|
|
564
|
-
delete error.details;
|
|
565
|
-
delete error.originalError?.details;
|
|
566
|
-
}
|
|
567
|
-
res.status(Number(error.code)).json(error);
|
|
568
|
-
};
|
|
569
|
-
|
|
570
|
-
const middleware$2 = (req, res, next) => {
|
|
571
|
-
if (req.originalUrl !== '/health') {
|
|
572
|
-
res.on('finish', function () {
|
|
573
|
-
const error = res.locals.error;
|
|
574
|
-
const durationInNs = Number(process.hrtime.bigint() - res.locals.requestStartTime);
|
|
575
|
-
const durationInMs = (durationInNs / 1_000_000) | 0;
|
|
576
|
-
const message = `${req.method} ${req.originalUrl} ${res.statusCode} - ${durationInMs} ms`;
|
|
577
|
-
const metadata = {
|
|
578
|
-
duration: durationInNs,
|
|
579
|
-
// Use reserved and standard attributes of Datadog
|
|
580
|
-
// https://app.datadoghq.com/logs/pipelines/standard-attributes
|
|
581
|
-
http: { method: req.method, status_code: res.statusCode, url_details: { path: req.originalUrl } },
|
|
582
|
-
...(error
|
|
583
|
-
? {
|
|
584
|
-
error: {
|
|
585
|
-
kind: error.message,
|
|
586
|
-
stack: (error.originalError?.details?.stack ?? error.details?.stack),
|
|
587
|
-
message: error.originalError?.message ?? error.message,
|
|
588
|
-
},
|
|
589
|
-
}
|
|
590
|
-
: {}),
|
|
591
|
-
};
|
|
592
|
-
if ([404, 429].includes(res.statusCode)) {
|
|
593
|
-
res.locals.logger.warn(message, metadata);
|
|
594
|
-
}
|
|
595
|
-
else if (res.statusCode >= 400) {
|
|
596
|
-
res.locals.logger.error(message, metadata);
|
|
597
|
-
}
|
|
598
|
-
else {
|
|
599
|
-
res.locals.logger.info(message, metadata);
|
|
600
|
-
}
|
|
601
|
-
});
|
|
602
|
-
}
|
|
603
|
-
next();
|
|
604
|
-
};
|
|
605
|
-
|
|
606
|
-
const middleware$1 = (req, res, _next) => {
|
|
607
|
-
const error = {
|
|
608
|
-
code: '404',
|
|
609
|
-
message: `Path ${req.path} not found.`,
|
|
610
|
-
};
|
|
611
|
-
res.status(404).json(error);
|
|
612
|
-
};
|
|
613
|
-
|
|
614
|
-
const middleware = (_, res, next) => {
|
|
615
|
-
res.locals.requestStartTime = process.hrtime.bigint();
|
|
616
|
-
next();
|
|
617
|
-
};
|
|
618
|
-
|
|
619
436
|
function assertValidPath(path) {
|
|
620
437
|
if (!path.startsWith('/')) {
|
|
621
438
|
throw new InvalidHandler(`The provided path '${path}' is invalid. All paths must start with a '/'.`);
|
|
@@ -948,6 +765,201 @@ class Handler {
|
|
|
948
765
|
}
|
|
949
766
|
}
|
|
950
767
|
|
|
768
|
+
function extractCorrelationId(req, res, next) {
|
|
769
|
+
res.locals.correlationId = req.header('X-Unito-Correlation-Id') ?? crypto.randomUUID();
|
|
770
|
+
next();
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const CREDENTIALS_HEADER = 'X-Unito-Credentials';
|
|
774
|
+
function extractCredentials(req, res, next) {
|
|
775
|
+
const credentialsHeader = req.header(CREDENTIALS_HEADER);
|
|
776
|
+
if (credentialsHeader) {
|
|
777
|
+
let credentials;
|
|
778
|
+
try {
|
|
779
|
+
credentials = JSON.parse(Buffer.from(credentialsHeader, 'base64').toString('utf8'));
|
|
780
|
+
}
|
|
781
|
+
catch {
|
|
782
|
+
throw new BadRequestError(`Malformed HTTP header ${CREDENTIALS_HEADER}`);
|
|
783
|
+
}
|
|
784
|
+
res.locals.credentials = credentials;
|
|
785
|
+
}
|
|
786
|
+
next();
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function onError(err, _req, res, next) {
|
|
790
|
+
if (res.headersSent) {
|
|
791
|
+
return next(err);
|
|
792
|
+
}
|
|
793
|
+
let error;
|
|
794
|
+
if (err instanceof HttpError) {
|
|
795
|
+
error = {
|
|
796
|
+
code: err.status.toString(),
|
|
797
|
+
message: err.message,
|
|
798
|
+
details: {
|
|
799
|
+
stack: err.stack,
|
|
800
|
+
},
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
else {
|
|
804
|
+
error = {
|
|
805
|
+
code: '500',
|
|
806
|
+
message: 'Oops! Something went wrong',
|
|
807
|
+
originalError: {
|
|
808
|
+
code: err.name,
|
|
809
|
+
message: err.message,
|
|
810
|
+
details: {
|
|
811
|
+
stack: err.stack,
|
|
812
|
+
},
|
|
813
|
+
},
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
res.locals.error = structuredClone(error);
|
|
817
|
+
// Keep the stack details in development for the Debugger
|
|
818
|
+
if (process.env.NODE_ENV !== 'development') {
|
|
819
|
+
delete error.details;
|
|
820
|
+
delete error.originalError?.details;
|
|
821
|
+
}
|
|
822
|
+
res.status(Number(error.code)).json(error);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// The operators are ordered by their symbol length, in descending order.
|
|
826
|
+
// This is necessary because the symbol of an operator can be
|
|
827
|
+
// a subset of the symbol of another operator.
|
|
828
|
+
//
|
|
829
|
+
// For example, the symbol "=" (EQUAL) is a subset of the symbol "!=" (NOT_EQUAL).
|
|
830
|
+
const ORDERED_OPERATORS = Object.values(integrationApi.OperatorType).sort((o1, o2) => o2.length - o1.length);
|
|
831
|
+
function extractFilters(req, res, next) {
|
|
832
|
+
const rawFilters = req.query.filter;
|
|
833
|
+
res.locals.filters = [];
|
|
834
|
+
if (typeof rawFilters === 'string') {
|
|
835
|
+
for (const rawFilter of rawFilters.split(',')) {
|
|
836
|
+
for (const operator of ORDERED_OPERATORS) {
|
|
837
|
+
if (rawFilter.includes(operator)) {
|
|
838
|
+
const [field, valuesRaw] = rawFilter.split(operator, 2);
|
|
839
|
+
const values = valuesRaw ? valuesRaw.split('|').map(decodeURIComponent) : [];
|
|
840
|
+
res.locals.filters.push({ field: field, operator, values });
|
|
841
|
+
break;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
next();
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function onFinish(req, res, next) {
|
|
850
|
+
if (req.originalUrl !== '/health') {
|
|
851
|
+
res.on('finish', function () {
|
|
852
|
+
const error = res.locals.error;
|
|
853
|
+
const durationInNs = Number(process.hrtime.bigint() - res.locals.requestStartTime);
|
|
854
|
+
const durationInMs = (durationInNs / 1_000_000) | 0;
|
|
855
|
+
const message = `${req.method} ${req.originalUrl} ${res.statusCode} - ${durationInMs} ms`;
|
|
856
|
+
const metadata = {
|
|
857
|
+
duration: durationInNs,
|
|
858
|
+
// Use reserved and standard attributes of Datadog
|
|
859
|
+
// https://app.datadoghq.com/logs/pipelines/standard-attributes
|
|
860
|
+
http: { method: req.method, status_code: res.statusCode, url_details: { path: req.originalUrl } },
|
|
861
|
+
...(error
|
|
862
|
+
? {
|
|
863
|
+
error: {
|
|
864
|
+
kind: error.message,
|
|
865
|
+
stack: (error.originalError?.details?.stack ?? error.details?.stack),
|
|
866
|
+
message: error.originalError?.message ?? error.message,
|
|
867
|
+
},
|
|
868
|
+
}
|
|
869
|
+
: {}),
|
|
870
|
+
};
|
|
871
|
+
if ([404, 429].includes(res.statusCode)) {
|
|
872
|
+
res.locals.logger.warn(message, metadata);
|
|
873
|
+
}
|
|
874
|
+
else if (res.statusCode >= 400) {
|
|
875
|
+
res.locals.logger.error(message, metadata);
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
res.locals.logger.info(message, metadata);
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
next();
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function notFound(req, res, _next) {
|
|
886
|
+
const error = {
|
|
887
|
+
code: '404',
|
|
888
|
+
message: `Path ${req.path} not found.`,
|
|
889
|
+
};
|
|
890
|
+
res.status(404).json(error);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const ADDITIONAL_CONTEXT_HEADER = 'X-Unito-Additional-Logging-Context';
|
|
894
|
+
function injectLogger(req, res, next) {
|
|
895
|
+
const logger = new Logger({ correlation_id: res.locals.correlationId });
|
|
896
|
+
res.locals.logger = logger;
|
|
897
|
+
const rawAdditionalContext = req.header(ADDITIONAL_CONTEXT_HEADER);
|
|
898
|
+
if (typeof rawAdditionalContext === 'string') {
|
|
899
|
+
try {
|
|
900
|
+
const additionalContext = JSON.parse(rawAdditionalContext);
|
|
901
|
+
logger.decorate(additionalContext);
|
|
902
|
+
}
|
|
903
|
+
catch (error) {
|
|
904
|
+
logger.warn(`Failed parsing header ${ADDITIONAL_CONTEXT_HEADER}: ${rawAdditionalContext}`);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
next();
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function start(_, res, next) {
|
|
911
|
+
res.locals.requestStartTime = process.hrtime.bigint();
|
|
912
|
+
next();
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const SECRETS_HEADER = 'X-Unito-Secrets';
|
|
916
|
+
function extractSecrets(req, res, next) {
|
|
917
|
+
const secretsHeader = req.header(SECRETS_HEADER);
|
|
918
|
+
if (secretsHeader) {
|
|
919
|
+
let secrets;
|
|
920
|
+
try {
|
|
921
|
+
secrets = JSON.parse(Buffer.from(secretsHeader, 'base64').toString('utf8'));
|
|
922
|
+
}
|
|
923
|
+
catch {
|
|
924
|
+
throw new BadRequestError(`Malformed HTTP header ${SECRETS_HEADER}`);
|
|
925
|
+
}
|
|
926
|
+
res.locals.secrets = secrets;
|
|
927
|
+
}
|
|
928
|
+
next();
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function extractSelects(req, res, next) {
|
|
932
|
+
const rawSelect = req.query.select;
|
|
933
|
+
if (typeof rawSelect === 'string') {
|
|
934
|
+
res.locals.selects = rawSelect.split(',');
|
|
935
|
+
}
|
|
936
|
+
else {
|
|
937
|
+
res.locals.selects = [];
|
|
938
|
+
}
|
|
939
|
+
next();
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const OPERATION_DEADLINE_HEADER = 'X-Unito-Operation-Deadline';
|
|
943
|
+
function extractOperationDeadline(req, res, next) {
|
|
944
|
+
const operationDeadlineHeader = Number(req.header(OPERATION_DEADLINE_HEADER));
|
|
945
|
+
if (operationDeadlineHeader) {
|
|
946
|
+
// `operationDeadlineHeader` represents a timestamp in the future, in seconds.
|
|
947
|
+
// We need to convert it to a number of milliseconds.
|
|
948
|
+
const deadline = operationDeadlineHeader * 1000 - Date.now();
|
|
949
|
+
if (deadline > 0) {
|
|
950
|
+
res.locals.signal = AbortSignal.timeout(deadline);
|
|
951
|
+
}
|
|
952
|
+
else {
|
|
953
|
+
throw new TimeoutError('Request already timed out upon reception');
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
else {
|
|
957
|
+
// Default to 20s, which is the maximum time frame allowed for an operation by Unito.
|
|
958
|
+
res.locals.signal = AbortSignal.timeout(20000);
|
|
959
|
+
}
|
|
960
|
+
next();
|
|
961
|
+
}
|
|
962
|
+
|
|
951
963
|
function printErrorMessage(message) {
|
|
952
964
|
console.error();
|
|
953
965
|
console.error(`\x1b[31m Oops! Something went wrong! \x1b[0m`);
|
|
@@ -1049,11 +1061,11 @@ class Integration {
|
|
|
1049
1061
|
app.set('query parser', 'extended');
|
|
1050
1062
|
app.use(express.json());
|
|
1051
1063
|
// Must be one of the first handlers (to catch all the errors).
|
|
1052
|
-
app.use(
|
|
1064
|
+
app.use(onFinish);
|
|
1053
1065
|
// Instantiate internal middlewares.
|
|
1054
|
-
app.use(
|
|
1055
|
-
app.use(
|
|
1056
|
-
app.use(
|
|
1066
|
+
app.use(start);
|
|
1067
|
+
app.use(extractCorrelationId);
|
|
1068
|
+
app.use(injectLogger);
|
|
1057
1069
|
// Making sure we log all incoming requests (except to '/health'), prior any processing.
|
|
1058
1070
|
app.use((req, res, next) => {
|
|
1059
1071
|
if (req.originalUrl !== '/health') {
|
|
@@ -1063,11 +1075,11 @@ class Integration {
|
|
|
1063
1075
|
});
|
|
1064
1076
|
// Instantiate application middlewares. These can throw, so they have an implicit dependency on the internal
|
|
1065
1077
|
// middlewares such as the logger, the correlationId, and the error handling.
|
|
1066
|
-
app.use(
|
|
1067
|
-
app.use(
|
|
1068
|
-
app.use(
|
|
1069
|
-
app.use(
|
|
1070
|
-
app.use(
|
|
1078
|
+
app.use(extractCredentials);
|
|
1079
|
+
app.use(extractSecrets);
|
|
1080
|
+
app.use(extractFilters);
|
|
1081
|
+
app.use(extractSelects);
|
|
1082
|
+
app.use(extractOperationDeadline);
|
|
1071
1083
|
// Load handlers as needed.
|
|
1072
1084
|
if (this.handlers.length) {
|
|
1073
1085
|
for (const handler of this.handlers) {
|
|
@@ -1083,9 +1095,9 @@ class Integration {
|
|
|
1083
1095
|
process.exit(1);
|
|
1084
1096
|
}
|
|
1085
1097
|
// Must be the (last - 1) handler.
|
|
1086
|
-
app.use(
|
|
1098
|
+
app.use(onError);
|
|
1087
1099
|
// Must be the last handler.
|
|
1088
|
-
app.use(
|
|
1100
|
+
app.use(notFound);
|
|
1089
1101
|
// Start the server.
|
|
1090
1102
|
this.instance = app.listen(this.port, () => console.info(`Server started on port ${this.port}.`));
|
|
1091
1103
|
}
|
|
@@ -1410,7 +1422,7 @@ class Provider {
|
|
|
1410
1422
|
* @param fields The schema of the item against which the filters are applied
|
|
1411
1423
|
* @returns The validated filters
|
|
1412
1424
|
*/
|
|
1413
|
-
|
|
1425
|
+
function getApplicableFilters(context, fields) {
|
|
1414
1426
|
const applicableFilters = [];
|
|
1415
1427
|
for (const filter of context.filters) {
|
|
1416
1428
|
let field = undefined;
|
|
@@ -1427,7 +1439,7 @@ const getApplicableFilters = (context, fields) => {
|
|
|
1427
1439
|
}
|
|
1428
1440
|
}
|
|
1429
1441
|
return applicableFilters;
|
|
1430
|
-
}
|
|
1442
|
+
}
|
|
1431
1443
|
|
|
1432
1444
|
exports.Api = integrationApi__namespace;
|
|
1433
1445
|
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;
|
|
@@ -21,5 +21,5 @@ export type Filter = {
|
|
|
21
21
|
operator: OperatorType;
|
|
22
22
|
values: string[] | undefined;
|
|
23
23
|
};
|
|
24
|
-
declare
|
|
25
|
-
export default
|
|
24
|
+
declare function extractFilters(req: Request, res: Response, next: NextFunction): void;
|
|
25
|
+
export default extractFilters;
|