@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.
@@ -98,6 +98,7 @@ export class Handler {
98
98
  signal: res.locals.signal,
99
99
  params: req.params,
100
100
  query: req.query,
101
+ relations: res.locals.relations,
101
102
  });
102
103
  res.status(200).send(collection);
103
104
  });
@@ -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;
@@ -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;
@@ -0,0 +1,6 @@
1
+ function extractRelations(req, res, next) {
2
+ const rawRelations = req.query.relations;
3
+ res.locals.relations = typeof rawRelations === 'string' ? rawRelations.split(',') : [];
4
+ next();
5
+ }
6
+ 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unito/integration-sdk",
3
- "version": "1.4.3",
3
+ "version": "1.5.0",
4
4
  "description": "Integration SDK",
5
5
  "type": "module",
6
6
  "types": "dist/src/index.d.ts",
package/src/handler.ts CHANGED
@@ -303,6 +303,7 @@ export class Handler {
303
303
  signal: res.locals.signal,
304
304
  params: req.params,
305
305
  query: req.query,
306
+ relations: res.locals.relations,
306
307
  });
307
308
 
308
309
  res.status(200).send(collection);
@@ -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;
@@ -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,