@unito/integration-sdk 2.3.1 → 2.3.3
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/index.cjs +29 -2
- package/dist/src/middlewares/credentials.d.ts +17 -1
- package/dist/src/resources/logger.d.ts +1 -1
- package/dist/src/resources/logger.js +14 -2
- package/dist/src/resources/provider.js +15 -0
- package/dist/test/resources/logger.test.js +21 -2
- package/dist/test/resources/provider.test.js +19 -7
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +1 -1
- package/src/middlewares/credentials.ts +17 -1
- package/src/resources/logger.ts +16 -3
- package/src/resources/provider.ts +21 -0
- package/test/resources/logger.test.ts +21 -2
- package/test/resources/provider.test.ts +27 -7
package/dist/src/index.cjs
CHANGED
|
@@ -170,7 +170,16 @@ class Logger {
|
|
|
170
170
|
static snakifyKeys(value) {
|
|
171
171
|
const result = {};
|
|
172
172
|
for (const key in value) {
|
|
173
|
-
|
|
173
|
+
let deepValue;
|
|
174
|
+
if (Array.isArray(value[key])) {
|
|
175
|
+
deepValue = value[key].map(item => this.snakifyKeys(item));
|
|
176
|
+
}
|
|
177
|
+
else if (typeof value[key] === 'object' && value[key] !== null) {
|
|
178
|
+
deepValue = this.snakifyKeys(value[key]);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
deepValue = value[key];
|
|
182
|
+
}
|
|
174
183
|
const snakifiedKey = key.replace(/[\w](?<!_)([A-Z])/g, k => `${k[0]}_${k[1]}`).toLowerCase();
|
|
175
184
|
result[snakifiedKey] = deepValue;
|
|
176
185
|
}
|
|
@@ -183,7 +192,10 @@ class Logger {
|
|
|
183
192
|
prunedMetadata[key] = '[REDACTED]';
|
|
184
193
|
(topLevelMeta ?? prunedMetadata).has_sensitive_attribute = true;
|
|
185
194
|
}
|
|
186
|
-
else if (
|
|
195
|
+
else if (Array.isArray(metadata[key])) {
|
|
196
|
+
prunedMetadata[key] = metadata[key].map(value => Logger.pruneSensitiveMetadata(value, topLevelMeta ?? prunedMetadata));
|
|
197
|
+
}
|
|
198
|
+
else if (typeof metadata[key] === 'object' && metadata[key] !== null) {
|
|
187
199
|
prunedMetadata[key] = Logger.pruneSensitiveMetadata(metadata[key], topLevelMeta ?? prunedMetadata);
|
|
188
200
|
}
|
|
189
201
|
else {
|
|
@@ -1415,6 +1427,7 @@ class Provider {
|
|
|
1415
1427
|
}
|
|
1416
1428
|
}
|
|
1417
1429
|
const callToProvider = async () => {
|
|
1430
|
+
const beforeRequestTimestamp = process.hrtime.bigint();
|
|
1418
1431
|
let response;
|
|
1419
1432
|
try {
|
|
1420
1433
|
response = await fetch(absoluteUrl, {
|
|
@@ -1436,6 +1449,20 @@ class Provider {
|
|
|
1436
1449
|
}
|
|
1437
1450
|
throw this.handleError(500, 'Unexpected error while calling the provider - this is not normal, investigate', options);
|
|
1438
1451
|
}
|
|
1452
|
+
const afterRequestTimestamp = process.hrtime.bigint();
|
|
1453
|
+
const requestDurationInNS = Number(afterRequestTimestamp - beforeRequestTimestamp);
|
|
1454
|
+
const requestDurationInMs = (requestDurationInNS / 1_000_000) | 0;
|
|
1455
|
+
options.logger.info(`Connector API Request ${options.method} ${absoluteUrl} ${response.status} - ${requestDurationInMs} ms`, {
|
|
1456
|
+
duration: requestDurationInNS,
|
|
1457
|
+
http: {
|
|
1458
|
+
method: options.method,
|
|
1459
|
+
status_code: response.status,
|
|
1460
|
+
content_type: headers['Content-Type'],
|
|
1461
|
+
url_details: {
|
|
1462
|
+
path: absoluteUrl,
|
|
1463
|
+
},
|
|
1464
|
+
},
|
|
1465
|
+
});
|
|
1439
1466
|
if (response.status >= 400) {
|
|
1440
1467
|
const textResult = await response.text();
|
|
1441
1468
|
throw this.handleError(response.status, textResult, options);
|
|
@@ -6,9 +6,25 @@ declare global {
|
|
|
6
6
|
}
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* The credentials object passed to every handler function.
|
|
11
|
+
*
|
|
12
|
+
* It contains the parsed credentials payload associated with the request through the X-Unito-Credentials header.
|
|
13
|
+
*/
|
|
9
14
|
export type Credentials = {
|
|
15
|
+
/**
|
|
16
|
+
* The access token for the provider.
|
|
17
|
+
*/
|
|
10
18
|
accessToken?: string;
|
|
11
|
-
|
|
19
|
+
/**
|
|
20
|
+
* The id of the unito credential record.
|
|
21
|
+
*
|
|
22
|
+
* This is not available on the initial call to /me before the creation of the credential.
|
|
23
|
+
*/
|
|
24
|
+
unitoCredentialId?: string;
|
|
25
|
+
/**
|
|
26
|
+
* The id of the unito user record.
|
|
27
|
+
*/
|
|
12
28
|
unitoUserId?: string;
|
|
13
29
|
[keys: string]: unknown;
|
|
14
30
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
type PrimitiveValue = undefined | null | string | string[] | number | number[] | boolean | boolean[];
|
|
2
2
|
type Value = {
|
|
3
|
-
[key: string]: PrimitiveValue | Value;
|
|
3
|
+
[key: string]: PrimitiveValue | Value | PrimitiveValue[] | Value[];
|
|
4
4
|
};
|
|
5
5
|
export type Metadata = Value & {
|
|
6
6
|
message?: never;
|
|
@@ -142,7 +142,16 @@ export default class Logger {
|
|
|
142
142
|
static snakifyKeys(value) {
|
|
143
143
|
const result = {};
|
|
144
144
|
for (const key in value) {
|
|
145
|
-
|
|
145
|
+
let deepValue;
|
|
146
|
+
if (Array.isArray(value[key])) {
|
|
147
|
+
deepValue = value[key].map(item => this.snakifyKeys(item));
|
|
148
|
+
}
|
|
149
|
+
else if (typeof value[key] === 'object' && value[key] !== null) {
|
|
150
|
+
deepValue = this.snakifyKeys(value[key]);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
deepValue = value[key];
|
|
154
|
+
}
|
|
146
155
|
const snakifiedKey = key.replace(/[\w](?<!_)([A-Z])/g, k => `${k[0]}_${k[1]}`).toLowerCase();
|
|
147
156
|
result[snakifiedKey] = deepValue;
|
|
148
157
|
}
|
|
@@ -155,7 +164,10 @@ export default class Logger {
|
|
|
155
164
|
prunedMetadata[key] = '[REDACTED]';
|
|
156
165
|
(topLevelMeta ?? prunedMetadata).has_sensitive_attribute = true;
|
|
157
166
|
}
|
|
158
|
-
else if (
|
|
167
|
+
else if (Array.isArray(metadata[key])) {
|
|
168
|
+
prunedMetadata[key] = metadata[key].map(value => Logger.pruneSensitiveMetadata(value, topLevelMeta ?? prunedMetadata));
|
|
169
|
+
}
|
|
170
|
+
else if (typeof metadata[key] === 'object' && metadata[key] !== null) {
|
|
159
171
|
prunedMetadata[key] = Logger.pruneSensitiveMetadata(metadata[key], topLevelMeta ?? prunedMetadata);
|
|
160
172
|
}
|
|
161
173
|
else {
|
|
@@ -284,6 +284,7 @@ export class Provider {
|
|
|
284
284
|
}
|
|
285
285
|
}
|
|
286
286
|
const callToProvider = async () => {
|
|
287
|
+
const beforeRequestTimestamp = process.hrtime.bigint();
|
|
287
288
|
let response;
|
|
288
289
|
try {
|
|
289
290
|
response = await fetch(absoluteUrl, {
|
|
@@ -305,6 +306,20 @@ export class Provider {
|
|
|
305
306
|
}
|
|
306
307
|
throw this.handleError(500, 'Unexpected error while calling the provider - this is not normal, investigate', options);
|
|
307
308
|
}
|
|
309
|
+
const afterRequestTimestamp = process.hrtime.bigint();
|
|
310
|
+
const requestDurationInNS = Number(afterRequestTimestamp - beforeRequestTimestamp);
|
|
311
|
+
const requestDurationInMs = (requestDurationInNS / 1_000_000) | 0;
|
|
312
|
+
options.logger.info(`Connector API Request ${options.method} ${absoluteUrl} ${response.status} - ${requestDurationInMs} ms`, {
|
|
313
|
+
duration: requestDurationInNS,
|
|
314
|
+
http: {
|
|
315
|
+
method: options.method,
|
|
316
|
+
status_code: response.status,
|
|
317
|
+
content_type: headers['Content-Type'],
|
|
318
|
+
url_details: {
|
|
319
|
+
path: absoluteUrl,
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
});
|
|
308
323
|
if (response.status >= 400) {
|
|
309
324
|
const textResult = await response.text();
|
|
310
325
|
throw this.handleError(response.status, textResult, options);
|
|
@@ -106,7 +106,14 @@ describe('Logger', () => {
|
|
|
106
106
|
it('snakify keys of Message and Metadata', testContext => {
|
|
107
107
|
const logSpy = testContext.mock.method(global.console, 'log', () => { });
|
|
108
108
|
assert.strictEqual(logSpy.mock.calls.length, 0);
|
|
109
|
-
const metadata = {
|
|
109
|
+
const metadata = {
|
|
110
|
+
correlationId: '123456789',
|
|
111
|
+
http: { method: 'GET', statusCode: 200 },
|
|
112
|
+
links: [
|
|
113
|
+
{ id: 1, organizationId: 'a' },
|
|
114
|
+
{ id: 2, organizationId: 'b' },
|
|
115
|
+
],
|
|
116
|
+
};
|
|
110
117
|
const logger = new Logger(metadata);
|
|
111
118
|
logger.log('test', { errorContext: { errorCode: 200, errorMessage: 'Page Not Found' } });
|
|
112
119
|
assert.strictEqual(logSpy.mock.calls.length, 1);
|
|
@@ -117,8 +124,12 @@ describe('Logger', () => {
|
|
|
117
124
|
assert.equal(actual['status'], 'log');
|
|
118
125
|
assert.deepEqual(actual['http'], { method: 'GET', status_code: 200 });
|
|
119
126
|
assert.deepEqual(actual['error_context'], { error_code: 200, error_message: 'Page Not Found' });
|
|
127
|
+
assert.deepEqual(actual['links'], [
|
|
128
|
+
{ id: 1, organization_id: 'a' },
|
|
129
|
+
{ id: 2, organization_id: 'b' },
|
|
130
|
+
]);
|
|
120
131
|
});
|
|
121
|
-
it('prunes sensitive Metadata', testContext => {
|
|
132
|
+
it('prunes sensitive Metadata', { only: true }, testContext => {
|
|
122
133
|
const logSpy = testContext.mock.method(global.console, 'log', () => { });
|
|
123
134
|
assert.strictEqual(logSpy.mock.calls.length, 0);
|
|
124
135
|
const metadata = {
|
|
@@ -126,6 +137,10 @@ describe('Logger', () => {
|
|
|
126
137
|
http: { method: 'GET', statusCode: 200, jwt: 'deepSecret' },
|
|
127
138
|
user: { contact: { email: 'deep_deep_deep@email.address', firstName: 'should be snakify then hidden' } },
|
|
128
139
|
access_token: 'secret',
|
|
140
|
+
items: [
|
|
141
|
+
{ id: 1, organizationId: 'a' },
|
|
142
|
+
{ id: 2, organizationId: 'b' },
|
|
143
|
+
],
|
|
129
144
|
};
|
|
130
145
|
const logger = new Logger(metadata);
|
|
131
146
|
logger.log('test', { errorContext: { errorCode: 200, errorMessage: 'Page Not Found' } });
|
|
@@ -139,6 +154,10 @@ describe('Logger', () => {
|
|
|
139
154
|
assert.equal(actual['access_token'], '[REDACTED]');
|
|
140
155
|
assert.deepEqual(actual['http'], { method: 'GET', status_code: 200, jwt: '[REDACTED]' });
|
|
141
156
|
assert.deepEqual(actual['user']['contact'], { email: '[REDACTED]', first_name: '[REDACTED]' });
|
|
157
|
+
assert.deepEqual(actual['items'], [
|
|
158
|
+
{ id: 1, organization_id: 'a' },
|
|
159
|
+
{ id: 2, organization_id: 'b' },
|
|
160
|
+
]);
|
|
142
161
|
});
|
|
143
162
|
it(`NULL_LOGGER should not log`, testContext => {
|
|
144
163
|
const logger = NULL_LOGGER;
|
|
@@ -50,7 +50,7 @@ describe('Provider', () => {
|
|
|
50
50
|
]);
|
|
51
51
|
assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { data: 'value' } });
|
|
52
52
|
});
|
|
53
|
-
it('
|
|
53
|
+
it('accepts text/html type response', async (context) => {
|
|
54
54
|
const response = new Response('', {
|
|
55
55
|
status: 200,
|
|
56
56
|
headers: { 'Content-Type': 'text/html; charset=UTF-8' },
|
|
@@ -79,7 +79,7 @@ describe('Provider', () => {
|
|
|
79
79
|
]);
|
|
80
80
|
assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: '' });
|
|
81
81
|
});
|
|
82
|
-
it('
|
|
82
|
+
it('accepts application/schema+json type response', async (context) => {
|
|
83
83
|
const response = new Response('{"data": "value"}', {
|
|
84
84
|
status: 200,
|
|
85
85
|
headers: { 'Content-Type': 'application/schema+json; charset=UTF-8' },
|
|
@@ -108,7 +108,7 @@ describe('Provider', () => {
|
|
|
108
108
|
]);
|
|
109
109
|
assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { data: 'value' } });
|
|
110
110
|
});
|
|
111
|
-
it('
|
|
111
|
+
it('accepts application/swagger+json type response', async (context) => {
|
|
112
112
|
const response = new Response('{"data": "value"}', {
|
|
113
113
|
status: 200,
|
|
114
114
|
headers: { 'Content-Type': 'application/swagger+json; charset=UTF-8' },
|
|
@@ -137,7 +137,7 @@ describe('Provider', () => {
|
|
|
137
137
|
]);
|
|
138
138
|
assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { data: 'value' } });
|
|
139
139
|
});
|
|
140
|
-
it('
|
|
140
|
+
it('accepts application/vnd.oracle.resource+json type response', async (context) => {
|
|
141
141
|
const response = new Response('{"data": "value"}', {
|
|
142
142
|
status: 200,
|
|
143
143
|
headers: { 'Content-Type': 'application/vnd.oracle.resource+json; type=collection; charset=UTF-8' },
|
|
@@ -166,7 +166,7 @@ describe('Provider', () => {
|
|
|
166
166
|
]);
|
|
167
167
|
assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { data: 'value' } });
|
|
168
168
|
});
|
|
169
|
-
it('
|
|
169
|
+
it('returns the raw response body if specified', async (context) => {
|
|
170
170
|
const response = new Response(`IMAGINE A HUGE PAYLOAD`, {
|
|
171
171
|
status: 200,
|
|
172
172
|
headers: { 'Content-Type': 'image/png' },
|
|
@@ -244,7 +244,7 @@ describe('Provider', () => {
|
|
|
244
244
|
]);
|
|
245
245
|
assert.deepEqual(actualResponse, { status: 201, headers: response.headers, body: { data: 'value' } });
|
|
246
246
|
});
|
|
247
|
-
it('
|
|
247
|
+
it('accepts an array as body for post request', async (context) => {
|
|
248
248
|
const response = new Response('{"data": "value"}', {
|
|
249
249
|
status: 201,
|
|
250
250
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -485,7 +485,7 @@ describe('Provider', () => {
|
|
|
485
485
|
assert.ok(error instanceof HttpErrors.HttpError);
|
|
486
486
|
assert.equal(error.message, 'Weird provider behavior');
|
|
487
487
|
});
|
|
488
|
-
it('
|
|
488
|
+
it('contains the credential in the custom error handler', async (context) => {
|
|
489
489
|
const provider = new Provider({
|
|
490
490
|
prepareRequest: requestOptions => {
|
|
491
491
|
return {
|
|
@@ -761,4 +761,16 @@ describe('Provider', () => {
|
|
|
761
761
|
assert.ok(error instanceof HttpErrors.RateLimitExceededError);
|
|
762
762
|
assert.equal(error.message, 'response body');
|
|
763
763
|
});
|
|
764
|
+
it('logs provider requests', async (context) => {
|
|
765
|
+
const response = new Response(undefined, { status: 201 });
|
|
766
|
+
context.mock.method(global, 'fetch', () => Promise.resolve(response));
|
|
767
|
+
const loggerStub = context.mock.method(logger, 'info');
|
|
768
|
+
await provider.get('/endpoint/123', {
|
|
769
|
+
credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
|
|
770
|
+
signal: new AbortController().signal,
|
|
771
|
+
logger: logger,
|
|
772
|
+
});
|
|
773
|
+
assert.equal(loggerStub.mock.callCount(), 1);
|
|
774
|
+
assert.match(String(loggerStub.mock.calls[0]?.arguments[0]), /Connector API Request GET www.myApi.com\/endpoint\/123 201 - \d+ ms/);
|
|
775
|
+
});
|
|
764
776
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["../src/errors.ts","../src/handler.ts","../src/helpers.ts","../src/httpErrors.ts","../src/index.ts","../src/integration.ts","../src/middlewares/correlationId.ts","../src/middlewares/credentials.ts","../src/middlewares/errors.ts","../src/middlewares/filters.ts","../src/middlewares/finish.ts","../src/middlewares/health.ts","../src/middlewares/logger.ts","../src/middlewares/notFound.ts","../src/middlewares/relations.ts","../src/middlewares/search.ts","../src/middlewares/secrets.ts","../src/middlewares/selects.ts","../src/middlewares/signal.ts","../src/middlewares/start.ts","../src/resources/cache.ts","../src/resources/context.ts","../src/resources/logger.ts","../src/resources/provider.ts","../test/errors.test.ts","../test/handler.test.ts","../test/helpers.test.ts","../test/integration.test.ts","../test/middlewares/correlationId.test.ts","../test/middlewares/credentials.test.ts","../test/middlewares/errors.test.ts","../test/middlewares/filters.test.ts","../test/middlewares/finish.test.ts","../test/middlewares/health.test.ts","../test/middlewares/logger.test.ts","../test/middlewares/notFound.test.ts","../test/middlewares/relations.test.ts","../test/middlewares/search.test.ts","../test/middlewares/secrets.test.ts","../test/middlewares/selects.test.ts","../test/middlewares/signal.test.ts","../test/middlewares/start.test.ts","../test/resources/cache.test.ts","../test/resources/logger.test.ts","../test/resources/provider.test.ts"],"version":"5.8.3"}
|
package/package.json
CHANGED
|
@@ -10,9 +10,25 @@ declare global {
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* The credentials object passed to every handler function.
|
|
15
|
+
*
|
|
16
|
+
* It contains the parsed credentials payload associated with the request through the X-Unito-Credentials header.
|
|
17
|
+
*/
|
|
13
18
|
export type Credentials = {
|
|
19
|
+
/**
|
|
20
|
+
* The access token for the provider.
|
|
21
|
+
*/
|
|
14
22
|
accessToken?: string;
|
|
15
|
-
|
|
23
|
+
/**
|
|
24
|
+
* The id of the unito credential record.
|
|
25
|
+
*
|
|
26
|
+
* This is not available on the initial call to /me before the creation of the credential.
|
|
27
|
+
*/
|
|
28
|
+
unitoCredentialId?: string;
|
|
29
|
+
/**
|
|
30
|
+
* The id of the unito user record.
|
|
31
|
+
*/
|
|
16
32
|
unitoUserId?: string;
|
|
17
33
|
[keys: string]: unknown;
|
|
18
34
|
};
|
package/src/resources/logger.ts
CHANGED
|
@@ -8,7 +8,7 @@ const enum LogLevel {
|
|
|
8
8
|
|
|
9
9
|
type PrimitiveValue = undefined | null | string | string[] | number | number[] | boolean | boolean[];
|
|
10
10
|
type Value = {
|
|
11
|
-
[key: string]: PrimitiveValue | Value;
|
|
11
|
+
[key: string]: PrimitiveValue | Value | PrimitiveValue[] | Value[];
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
export type Metadata = Value & { message?: never };
|
|
@@ -172,7 +172,16 @@ export default class Logger {
|
|
|
172
172
|
const result: Value = {};
|
|
173
173
|
|
|
174
174
|
for (const key in value) {
|
|
175
|
-
|
|
175
|
+
let deepValue;
|
|
176
|
+
|
|
177
|
+
if (Array.isArray(value[key])) {
|
|
178
|
+
deepValue = value[key].map(item => this.snakifyKeys(item as Value));
|
|
179
|
+
} else if (typeof value[key] === 'object' && value[key] !== null) {
|
|
180
|
+
deepValue = this.snakifyKeys(value[key] as Value);
|
|
181
|
+
} else {
|
|
182
|
+
deepValue = value[key];
|
|
183
|
+
}
|
|
184
|
+
|
|
176
185
|
const snakifiedKey = key.replace(/[\w](?<!_)([A-Z])/g, k => `${k[0]}_${k[1]}`).toLowerCase();
|
|
177
186
|
result[snakifiedKey] = deepValue;
|
|
178
187
|
}
|
|
@@ -187,7 +196,11 @@ export default class Logger {
|
|
|
187
196
|
if (LOGMETA_BLACKLIST.includes(key)) {
|
|
188
197
|
prunedMetadata[key] = '[REDACTED]';
|
|
189
198
|
(topLevelMeta ?? prunedMetadata).has_sensitive_attribute = true;
|
|
190
|
-
} else if (
|
|
199
|
+
} else if (Array.isArray(metadata[key])) {
|
|
200
|
+
prunedMetadata[key] = metadata[key].map(value =>
|
|
201
|
+
Logger.pruneSensitiveMetadata(value as Value, topLevelMeta ?? prunedMetadata),
|
|
202
|
+
);
|
|
203
|
+
} else if (typeof metadata[key] === 'object' && metadata[key] !== null) {
|
|
191
204
|
prunedMetadata[key] = Logger.pruneSensitiveMetadata(metadata[key] as Value, topLevelMeta ?? prunedMetadata);
|
|
192
205
|
} else {
|
|
193
206
|
prunedMetadata[key] = metadata[key];
|
|
@@ -388,6 +388,8 @@ export class Provider {
|
|
|
388
388
|
}
|
|
389
389
|
|
|
390
390
|
const callToProvider = async (): Promise<Response<T>> => {
|
|
391
|
+
const beforeRequestTimestamp = process.hrtime.bigint();
|
|
392
|
+
|
|
391
393
|
let response: globalThis.Response;
|
|
392
394
|
|
|
393
395
|
try {
|
|
@@ -420,6 +422,25 @@ export class Provider {
|
|
|
420
422
|
);
|
|
421
423
|
}
|
|
422
424
|
|
|
425
|
+
const afterRequestTimestamp = process.hrtime.bigint();
|
|
426
|
+
const requestDurationInNS = Number(afterRequestTimestamp - beforeRequestTimestamp);
|
|
427
|
+
const requestDurationInMs = (requestDurationInNS / 1_000_000) | 0;
|
|
428
|
+
|
|
429
|
+
options.logger.info(
|
|
430
|
+
`Connector API Request ${options.method} ${absoluteUrl} ${response.status} - ${requestDurationInMs} ms`,
|
|
431
|
+
{
|
|
432
|
+
duration: requestDurationInNS,
|
|
433
|
+
http: {
|
|
434
|
+
method: options.method,
|
|
435
|
+
status_code: response.status,
|
|
436
|
+
content_type: headers['Content-Type'],
|
|
437
|
+
url_details: {
|
|
438
|
+
path: absoluteUrl,
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
);
|
|
443
|
+
|
|
423
444
|
if (response.status >= 400) {
|
|
424
445
|
const textResult = await response.text();
|
|
425
446
|
throw this.handleError(response.status, textResult, options);
|
|
@@ -129,7 +129,14 @@ describe('Logger', () => {
|
|
|
129
129
|
const logSpy = testContext.mock.method(global.console, 'log', () => {});
|
|
130
130
|
assert.strictEqual(logSpy.mock.calls.length, 0);
|
|
131
131
|
|
|
132
|
-
const metadata = {
|
|
132
|
+
const metadata = {
|
|
133
|
+
correlationId: '123456789',
|
|
134
|
+
http: { method: 'GET', statusCode: 200 },
|
|
135
|
+
links: [
|
|
136
|
+
{ id: 1, organizationId: 'a' },
|
|
137
|
+
{ id: 2, organizationId: 'b' },
|
|
138
|
+
],
|
|
139
|
+
};
|
|
133
140
|
const logger = new Logger(metadata);
|
|
134
141
|
logger.log('test', { errorContext: { errorCode: 200, errorMessage: 'Page Not Found' } });
|
|
135
142
|
assert.strictEqual(logSpy.mock.calls.length, 1);
|
|
@@ -142,9 +149,13 @@ describe('Logger', () => {
|
|
|
142
149
|
assert.equal(actual['status'], 'log');
|
|
143
150
|
assert.deepEqual(actual['http'], { method: 'GET', status_code: 200 });
|
|
144
151
|
assert.deepEqual(actual['error_context'], { error_code: 200, error_message: 'Page Not Found' });
|
|
152
|
+
assert.deepEqual(actual['links'], [
|
|
153
|
+
{ id: 1, organization_id: 'a' },
|
|
154
|
+
{ id: 2, organization_id: 'b' },
|
|
155
|
+
]);
|
|
145
156
|
});
|
|
146
157
|
|
|
147
|
-
it('prunes sensitive Metadata', testContext => {
|
|
158
|
+
it('prunes sensitive Metadata', { only: true }, testContext => {
|
|
148
159
|
const logSpy = testContext.mock.method(global.console, 'log', () => {});
|
|
149
160
|
assert.strictEqual(logSpy.mock.calls.length, 0);
|
|
150
161
|
|
|
@@ -153,6 +164,10 @@ describe('Logger', () => {
|
|
|
153
164
|
http: { method: 'GET', statusCode: 200, jwt: 'deepSecret' },
|
|
154
165
|
user: { contact: { email: 'deep_deep_deep@email.address', firstName: 'should be snakify then hidden' } },
|
|
155
166
|
access_token: 'secret',
|
|
167
|
+
items: [
|
|
168
|
+
{ id: 1, organizationId: 'a' },
|
|
169
|
+
{ id: 2, organizationId: 'b' },
|
|
170
|
+
],
|
|
156
171
|
};
|
|
157
172
|
const logger = new Logger(metadata);
|
|
158
173
|
logger.log('test', { errorContext: { errorCode: 200, errorMessage: 'Page Not Found' } });
|
|
@@ -167,6 +182,10 @@ describe('Logger', () => {
|
|
|
167
182
|
assert.equal(actual['access_token'], '[REDACTED]');
|
|
168
183
|
assert.deepEqual(actual['http'], { method: 'GET', status_code: 200, jwt: '[REDACTED]' });
|
|
169
184
|
assert.deepEqual(actual['user']['contact'], { email: '[REDACTED]', first_name: '[REDACTED]' });
|
|
185
|
+
assert.deepEqual(actual['items'], [
|
|
186
|
+
{ id: 1, organization_id: 'a' },
|
|
187
|
+
{ id: 2, organization_id: 'b' },
|
|
188
|
+
]);
|
|
170
189
|
});
|
|
171
190
|
|
|
172
191
|
it(`NULL_LOGGER should not log`, testContext => {
|
|
@@ -59,7 +59,7 @@ describe('Provider', () => {
|
|
|
59
59
|
assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { data: 'value' } });
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
it('
|
|
62
|
+
it('accepts text/html type response', async context => {
|
|
63
63
|
const response = new Response('', {
|
|
64
64
|
status: 200,
|
|
65
65
|
headers: { 'Content-Type': 'text/html; charset=UTF-8' },
|
|
@@ -94,7 +94,7 @@ describe('Provider', () => {
|
|
|
94
94
|
assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: '' });
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
-
it('
|
|
97
|
+
it('accepts application/schema+json type response', async context => {
|
|
98
98
|
const response = new Response('{"data": "value"}', {
|
|
99
99
|
status: 200,
|
|
100
100
|
headers: { 'Content-Type': 'application/schema+json; charset=UTF-8' },
|
|
@@ -129,7 +129,7 @@ describe('Provider', () => {
|
|
|
129
129
|
assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { data: 'value' } });
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
-
it('
|
|
132
|
+
it('accepts application/swagger+json type response', async context => {
|
|
133
133
|
const response = new Response('{"data": "value"}', {
|
|
134
134
|
status: 200,
|
|
135
135
|
headers: { 'Content-Type': 'application/swagger+json; charset=UTF-8' },
|
|
@@ -164,7 +164,7 @@ describe('Provider', () => {
|
|
|
164
164
|
assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { data: 'value' } });
|
|
165
165
|
});
|
|
166
166
|
|
|
167
|
-
it('
|
|
167
|
+
it('accepts application/vnd.oracle.resource+json type response', async context => {
|
|
168
168
|
const response = new Response('{"data": "value"}', {
|
|
169
169
|
status: 200,
|
|
170
170
|
headers: { 'Content-Type': 'application/vnd.oracle.resource+json; type=collection; charset=UTF-8' },
|
|
@@ -199,7 +199,7 @@ describe('Provider', () => {
|
|
|
199
199
|
assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { data: 'value' } });
|
|
200
200
|
});
|
|
201
201
|
|
|
202
|
-
it('
|
|
202
|
+
it('returns the raw response body if specified', async context => {
|
|
203
203
|
const response = new Response(`IMAGINE A HUGE PAYLOAD`, {
|
|
204
204
|
status: 200,
|
|
205
205
|
headers: { 'Content-Type': 'image/png' },
|
|
@@ -293,7 +293,7 @@ describe('Provider', () => {
|
|
|
293
293
|
assert.deepEqual(actualResponse, { status: 201, headers: response.headers, body: { data: 'value' } });
|
|
294
294
|
});
|
|
295
295
|
|
|
296
|
-
it('
|
|
296
|
+
it('accepts an array as body for post request', async context => {
|
|
297
297
|
const response = new Response('{"data": "value"}', {
|
|
298
298
|
status: 201,
|
|
299
299
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -580,7 +580,7 @@ describe('Provider', () => {
|
|
|
580
580
|
assert.equal(error.message, 'Weird provider behavior');
|
|
581
581
|
});
|
|
582
582
|
|
|
583
|
-
it('
|
|
583
|
+
it('contains the credential in the custom error handler', async context => {
|
|
584
584
|
const provider = new Provider({
|
|
585
585
|
prepareRequest: requestOptions => {
|
|
586
586
|
return {
|
|
@@ -911,4 +911,24 @@ describe('Provider', () => {
|
|
|
911
911
|
assert.ok(error instanceof HttpErrors.RateLimitExceededError);
|
|
912
912
|
assert.equal(error.message, 'response body');
|
|
913
913
|
});
|
|
914
|
+
|
|
915
|
+
it('logs provider requests', async context => {
|
|
916
|
+
const response = new Response(undefined, { status: 201 });
|
|
917
|
+
|
|
918
|
+
context.mock.method(global, 'fetch', () => Promise.resolve(response));
|
|
919
|
+
|
|
920
|
+
const loggerStub = context.mock.method(logger, 'info');
|
|
921
|
+
|
|
922
|
+
await provider.get('/endpoint/123', {
|
|
923
|
+
credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
|
|
924
|
+
signal: new AbortController().signal,
|
|
925
|
+
logger: logger,
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
assert.equal(loggerStub.mock.callCount(), 1);
|
|
929
|
+
assert.match(
|
|
930
|
+
String(loggerStub.mock.calls[0]?.arguments[0]),
|
|
931
|
+
/Connector API Request GET www.myApi.com\/endpoint\/123 201 - \d+ ms/,
|
|
932
|
+
);
|
|
933
|
+
});
|
|
914
934
|
});
|