@unito/integration-sdk 1.4.3 → 1.5.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/handler.js +1 -0
- package/dist/src/index.cjs +13 -1
- package/dist/src/integration.js +2 -0
- package/dist/src/middlewares/relations.d.ts +10 -0
- package/dist/src/middlewares/relations.js +6 -0
- package/dist/src/resources/context.d.ts +10 -0
- package/dist/src/resources/provider.js +5 -1
- package/dist/test/middlewares/relations.test.d.ts +1 -0
- package/dist/test/middlewares/relations.test.js +21 -0
- package/dist/test/resources/provider.test.js +16 -0
- package/package.json +1 -1
- package/src/handler.ts +1 -0
- package/src/integration.ts +2 -0
- package/src/middlewares/relations.ts +27 -0
- package/src/resources/context.ts +10 -0
- package/src/resources/provider.ts +4 -1
- package/test/middlewares/relations.test.ts +35 -0
- package/test/resources/provider.test.ts +24 -0
package/dist/src/handler.js
CHANGED
package/dist/src/index.cjs
CHANGED
|
@@ -529,6 +529,7 @@ class Handler {
|
|
|
529
529
|
signal: res.locals.signal,
|
|
530
530
|
params: req.params,
|
|
531
531
|
query: req.query,
|
|
532
|
+
relations: res.locals.relations,
|
|
532
533
|
});
|
|
533
534
|
res.status(200).send(collection);
|
|
534
535
|
});
|
|
@@ -939,6 +940,12 @@ function extractSelects(req, res, next) {
|
|
|
939
940
|
next();
|
|
940
941
|
}
|
|
941
942
|
|
|
943
|
+
function extractRelations(req, res, next) {
|
|
944
|
+
const rawRelations = req.query.relations;
|
|
945
|
+
res.locals.relations = typeof rawRelations === 'string' ? rawRelations.split(',') : [];
|
|
946
|
+
next();
|
|
947
|
+
}
|
|
948
|
+
|
|
942
949
|
const OPERATION_DEADLINE_HEADER = 'X-Unito-Operation-Deadline';
|
|
943
950
|
function extractOperationDeadline(req, res, next) {
|
|
944
951
|
const operationDeadlineHeader = Number(req.header(OPERATION_DEADLINE_HEADER));
|
|
@@ -1075,6 +1082,7 @@ class Integration {
|
|
|
1075
1082
|
app.use(extractSecrets);
|
|
1076
1083
|
app.use(extractFilters);
|
|
1077
1084
|
app.use(extractSelects);
|
|
1085
|
+
app.use(extractRelations);
|
|
1078
1086
|
app.use(extractOperationDeadline);
|
|
1079
1087
|
// Load handlers as needed.
|
|
1080
1088
|
if (this.handlers.length) {
|
|
@@ -1374,6 +1382,10 @@ class Provider {
|
|
|
1374
1382
|
const textResult = await response.text();
|
|
1375
1383
|
throw this.handleError(response.status, textResult, options);
|
|
1376
1384
|
}
|
|
1385
|
+
else if (response.status === 204 || response.body === null) {
|
|
1386
|
+
// No content: return without inspecting the body
|
|
1387
|
+
return { status: response.status, headers: response.headers, body: undefined };
|
|
1388
|
+
}
|
|
1377
1389
|
const responseContentType = response.headers.get('content-type');
|
|
1378
1390
|
let body;
|
|
1379
1391
|
if (options.rawBody || headers.Accept === 'application/octet-stream') {
|
|
@@ -1387,7 +1399,7 @@ class Provider {
|
|
|
1387
1399
|
if (responseContentType && !responseContentType.includes('application/json')) {
|
|
1388
1400
|
const textResult = await response.text();
|
|
1389
1401
|
throw this.handleError(500, `Unsupported content-type, expected 'application/json' but got '${responseContentType}'.
|
|
1390
|
-
Original response (${response.status}): ${textResult}`, options);
|
|
1402
|
+
Original response (${response.status}): "${textResult}"`, options);
|
|
1391
1403
|
}
|
|
1392
1404
|
try {
|
|
1393
1405
|
body = response.body ? await response.json() : undefined;
|
package/dist/src/integration.js
CHANGED
|
@@ -11,6 +11,7 @@ import loggerMiddleware from './middlewares/logger.js';
|
|
|
11
11
|
import startMiddleware from './middlewares/start.js';
|
|
12
12
|
import secretsMiddleware from './middlewares/secrets.js';
|
|
13
13
|
import selectsMiddleware from './middlewares/selects.js';
|
|
14
|
+
import relationsMiddleware from './middlewares/relations.js';
|
|
14
15
|
import signalMiddleware from './middlewares/signal.js';
|
|
15
16
|
function printErrorMessage(message) {
|
|
16
17
|
console.error();
|
|
@@ -131,6 +132,7 @@ export default class Integration {
|
|
|
131
132
|
app.use(secretsMiddleware);
|
|
132
133
|
app.use(filtersMiddleware);
|
|
133
134
|
app.use(selectsMiddleware);
|
|
135
|
+
app.use(relationsMiddleware);
|
|
134
136
|
app.use(signalMiddleware);
|
|
135
137
|
// Load handlers as needed.
|
|
136
138
|
if (this.handlers.length) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
declare global {
|
|
3
|
+
namespace Express {
|
|
4
|
+
interface Locals {
|
|
5
|
+
relations: string[];
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
declare function extractRelations(req: Request, res: Response, next: NextFunction): void;
|
|
10
|
+
export default extractRelations;
|
|
@@ -100,6 +100,16 @@ export type GetCollectionContext<P extends Maybe<Params> = Empty, Q extends Quer
|
|
|
100
100
|
* ['name', 'department.name']
|
|
101
101
|
*/
|
|
102
102
|
selects: string[];
|
|
103
|
+
/**
|
|
104
|
+
* Parsed relations query param yielding a list of relation for which the path should be returned.
|
|
105
|
+
*
|
|
106
|
+
* Given a relations query param:
|
|
107
|
+
* `relations=items,subitems`
|
|
108
|
+
*
|
|
109
|
+
* Context.relations will be:
|
|
110
|
+
* ['items', 'subitems']
|
|
111
|
+
*/
|
|
112
|
+
relations: string[];
|
|
103
113
|
};
|
|
104
114
|
export type CreateItemContext<P extends Maybe<Params> = Empty, Q extends Maybe<Query> = Empty, B extends CreateItemBody = API.CreateItemRequestPayload> = Context<P, Q> & {
|
|
105
115
|
body: B;
|
|
@@ -275,6 +275,10 @@ export class Provider {
|
|
|
275
275
|
const textResult = await response.text();
|
|
276
276
|
throw this.handleError(response.status, textResult, options);
|
|
277
277
|
}
|
|
278
|
+
else if (response.status === 204 || response.body === null) {
|
|
279
|
+
// No content: return without inspecting the body
|
|
280
|
+
return { status: response.status, headers: response.headers, body: undefined };
|
|
281
|
+
}
|
|
278
282
|
const responseContentType = response.headers.get('content-type');
|
|
279
283
|
let body;
|
|
280
284
|
if (options.rawBody || headers.Accept === 'application/octet-stream') {
|
|
@@ -288,7 +292,7 @@ export class Provider {
|
|
|
288
292
|
if (responseContentType && !responseContentType.includes('application/json')) {
|
|
289
293
|
const textResult = await response.text();
|
|
290
294
|
throw this.handleError(500, `Unsupported content-type, expected 'application/json' but got '${responseContentType}'.
|
|
291
|
-
Original response (${response.status}): ${textResult}`, options);
|
|
295
|
+
Original response (${response.status}): "${textResult}"`, options);
|
|
292
296
|
}
|
|
293
297
|
try {
|
|
294
298
|
body = response.body ? await response.json() : undefined;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import extractRelations from '../../src/middlewares/relations.js';
|
|
4
|
+
describe('relations middleware', () => {
|
|
5
|
+
it('data', () => {
|
|
6
|
+
const request = { query: { relations: 'foo,bar,baz' } };
|
|
7
|
+
const response = { locals: {} };
|
|
8
|
+
extractRelations(request, response, () => { });
|
|
9
|
+
assert.deepEqual(response.locals, {
|
|
10
|
+
relations: ['foo', 'bar', 'baz'],
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
it('no data', () => {
|
|
14
|
+
const request = { query: {} };
|
|
15
|
+
const response = { locals: {} };
|
|
16
|
+
extractRelations(request, response, () => { });
|
|
17
|
+
assert.deepEqual(response.locals, {
|
|
18
|
+
relations: [],
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -452,6 +452,22 @@ describe('Provider', () => {
|
|
|
452
452
|
assert.ok(providerResponse);
|
|
453
453
|
assert.ok(providerResponse.body instanceof ReadableStream);
|
|
454
454
|
});
|
|
455
|
+
it('returns successfully on unexpected content-type response with no body', async (context) => {
|
|
456
|
+
const response = new Response(null, {
|
|
457
|
+
status: 201,
|
|
458
|
+
headers: { 'Content-Type': 'html/text' },
|
|
459
|
+
});
|
|
460
|
+
context.mock.method(global, 'fetch', () => Promise.resolve(response));
|
|
461
|
+
const providerResponse = await provider.post('/endpoint/123', {}, {
|
|
462
|
+
credentials: { apiKey: 'apikey#1111' },
|
|
463
|
+
logger: logger,
|
|
464
|
+
signal: new AbortController().signal,
|
|
465
|
+
});
|
|
466
|
+
assert.ok(providerResponse);
|
|
467
|
+
assert.strictEqual(providerResponse.status, response.status);
|
|
468
|
+
assert.strictEqual(providerResponse.headers, response.headers);
|
|
469
|
+
assert.strictEqual(providerResponse.body, undefined);
|
|
470
|
+
});
|
|
455
471
|
it('throws on invalid json response', async (context) => {
|
|
456
472
|
const response = new Response('{invalidJSON}', {
|
|
457
473
|
status: 200,
|
package/package.json
CHANGED
package/src/handler.ts
CHANGED
package/src/integration.ts
CHANGED
|
@@ -13,6 +13,7 @@ import loggerMiddleware from './middlewares/logger.js';
|
|
|
13
13
|
import startMiddleware from './middlewares/start.js';
|
|
14
14
|
import secretsMiddleware from './middlewares/secrets.js';
|
|
15
15
|
import selectsMiddleware from './middlewares/selects.js';
|
|
16
|
+
import relationsMiddleware from './middlewares/relations.js';
|
|
16
17
|
import signalMiddleware from './middlewares/signal.js';
|
|
17
18
|
|
|
18
19
|
function printErrorMessage(message: string) {
|
|
@@ -149,6 +150,7 @@ export default class Integration {
|
|
|
149
150
|
app.use(secretsMiddleware);
|
|
150
151
|
app.use(filtersMiddleware);
|
|
151
152
|
app.use(selectsMiddleware);
|
|
153
|
+
app.use(relationsMiddleware);
|
|
152
154
|
app.use(signalMiddleware);
|
|
153
155
|
|
|
154
156
|
// Load handlers as needed.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
|
|
3
|
+
declare global {
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
5
|
+
namespace Express {
|
|
6
|
+
interface Locals {
|
|
7
|
+
// When the query params contains...
|
|
8
|
+
//
|
|
9
|
+
// relations=items,subitems
|
|
10
|
+
//
|
|
11
|
+
// ... it becomes available as follow in handlers:
|
|
12
|
+
//
|
|
13
|
+
// ['items', 'subitems']
|
|
14
|
+
relations: string[];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function extractRelations(req: Request, res: Response, next: NextFunction) {
|
|
20
|
+
const rawRelations = req.query.relations;
|
|
21
|
+
|
|
22
|
+
res.locals.relations = typeof rawRelations === 'string' ? rawRelations.split(',') : [];
|
|
23
|
+
|
|
24
|
+
next();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default extractRelations;
|
package/src/resources/context.ts
CHANGED
|
@@ -106,6 +106,16 @@ export type GetCollectionContext<P extends Maybe<Params> = Empty, Q extends Quer
|
|
|
106
106
|
* ['name', 'department.name']
|
|
107
107
|
*/
|
|
108
108
|
selects: string[];
|
|
109
|
+
/**
|
|
110
|
+
* Parsed relations query param yielding a list of relation for which the path should be returned.
|
|
111
|
+
*
|
|
112
|
+
* Given a relations query param:
|
|
113
|
+
* `relations=items,subitems`
|
|
114
|
+
*
|
|
115
|
+
* Context.relations will be:
|
|
116
|
+
* ['items', 'subitems']
|
|
117
|
+
*/
|
|
118
|
+
relations: string[];
|
|
109
119
|
};
|
|
110
120
|
|
|
111
121
|
export type CreateItemContext<
|
|
@@ -390,6 +390,9 @@ export class Provider {
|
|
|
390
390
|
if (response.status >= 400) {
|
|
391
391
|
const textResult = await response.text();
|
|
392
392
|
throw this.handleError(response.status, textResult, options);
|
|
393
|
+
} else if (response.status === 204 || response.body === null) {
|
|
394
|
+
// No content: return without inspecting the body
|
|
395
|
+
return { status: response.status, headers: response.headers, body: undefined as unknown as T };
|
|
393
396
|
}
|
|
394
397
|
|
|
395
398
|
const responseContentType = response.headers.get('content-type');
|
|
@@ -407,7 +410,7 @@ export class Provider {
|
|
|
407
410
|
throw this.handleError(
|
|
408
411
|
500,
|
|
409
412
|
`Unsupported content-type, expected 'application/json' but got '${responseContentType}'.
|
|
410
|
-
Original response (${response.status}): ${textResult}`,
|
|
413
|
+
Original response (${response.status}): "${textResult}"`,
|
|
411
414
|
options,
|
|
412
415
|
);
|
|
413
416
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { describe, it } from 'node:test';
|
|
4
|
+
|
|
5
|
+
import extractRelations from '../../src/middlewares/relations.js';
|
|
6
|
+
|
|
7
|
+
describe('relations middleware', () => {
|
|
8
|
+
it('data', () => {
|
|
9
|
+
const request = { query: { relations: 'foo,bar,baz' } } as express.Request<
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
+
any,
|
|
12
|
+
object,
|
|
13
|
+
object,
|
|
14
|
+
{ relations: string }
|
|
15
|
+
>;
|
|
16
|
+
const response = { locals: {} } as express.Response;
|
|
17
|
+
|
|
18
|
+
extractRelations(request, response, () => {});
|
|
19
|
+
|
|
20
|
+
assert.deepEqual(response.locals, {
|
|
21
|
+
relations: ['foo', 'bar', 'baz'],
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('no data', () => {
|
|
26
|
+
const request = { query: {} } as express.Request;
|
|
27
|
+
const response = { locals: {} } as express.Response;
|
|
28
|
+
|
|
29
|
+
extractRelations(request, response, () => {});
|
|
30
|
+
|
|
31
|
+
assert.deepEqual(response.locals, {
|
|
32
|
+
relations: [],
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -539,6 +539,30 @@ describe('Provider', () => {
|
|
|
539
539
|
assert.ok(providerResponse.body instanceof ReadableStream);
|
|
540
540
|
});
|
|
541
541
|
|
|
542
|
+
it('returns successfully on unexpected content-type response with no body', async context => {
|
|
543
|
+
const response = new Response(null, {
|
|
544
|
+
status: 201,
|
|
545
|
+
headers: { 'Content-Type': 'html/text' },
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
context.mock.method(global, 'fetch', () => Promise.resolve(response));
|
|
549
|
+
|
|
550
|
+
const providerResponse = await provider.post(
|
|
551
|
+
'/endpoint/123',
|
|
552
|
+
{},
|
|
553
|
+
{
|
|
554
|
+
credentials: { apiKey: 'apikey#1111' },
|
|
555
|
+
logger: logger,
|
|
556
|
+
signal: new AbortController().signal,
|
|
557
|
+
},
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
assert.ok(providerResponse);
|
|
561
|
+
assert.strictEqual(providerResponse.status, response.status);
|
|
562
|
+
assert.strictEqual(providerResponse.headers, response.headers);
|
|
563
|
+
assert.strictEqual(providerResponse.body, undefined);
|
|
564
|
+
});
|
|
565
|
+
|
|
542
566
|
it('throws on invalid json response', async context => {
|
|
543
567
|
const response = new Response('{invalidJSON}', {
|
|
544
568
|
status: 200,
|