@unito/integration-sdk 1.4.4 → 1.6.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) {
@@ -1344,7 +1352,8 @@ class Provider {
1344
1352
  if (headers['Content-Type'] === 'application/x-www-form-urlencoded') {
1345
1353
  stringifiedBody = new URLSearchParams(body).toString();
1346
1354
  }
1347
- else if (headers['Content-Type'] === 'application/json') {
1355
+ else if (headers['Content-Type'] === 'application/json' ||
1356
+ headers['Content-Type'] === 'application/json-patch+json') {
1348
1357
  stringifiedBody = JSON.stringify(body);
1349
1358
  }
1350
1359
  }
@@ -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;
@@ -60,6 +60,7 @@ export type PreparedRequest = {
60
60
  url: string;
61
61
  headers: Record<string, string>;
62
62
  };
63
+ export type RequestBody = Record<string, unknown> | RequestBody[];
63
64
  /**
64
65
  * The Provider class is a wrapper around the fetch function to call a provider's HTTP API.
65
66
  *
@@ -149,7 +150,7 @@ export declare class Provider {
149
150
  * @param options RequestOptions used to adjust the call made to the provider (use to override default headers).
150
151
  * @returns The {@link Response} extracted from the provider.
151
152
  */
152
- post<T>(endpoint: string, body: Record<string, unknown>, options: RequestOptions): Promise<Response<T>>;
153
+ post<T>(endpoint: string, body: RequestBody, options: RequestOptions): Promise<Response<T>>;
153
154
  postForm<T>(endpoint: string, form: FormData, options: RequestOptions): Promise<Response<T>>;
154
155
  /**
155
156
  * Performs a PUT request to the provider.
@@ -163,7 +164,7 @@ export declare class Provider {
163
164
  * @param options RequestOptions used to adjust the call made to the provider (use to override default headers).
164
165
  * @returns The {@link Response} extracted from the provider.
165
166
  */
166
- put<T>(endpoint: string, body: Record<string, unknown>, options: RequestOptions): Promise<Response<T>>;
167
+ put<T>(endpoint: string, body: RequestBody, options: RequestOptions): Promise<Response<T>>;
167
168
  /**
168
169
  * Performs a PATCH request to the provider.
169
170
  *
@@ -176,7 +177,7 @@ export declare class Provider {
176
177
  * @param options RequestOptions used to adjust the call made to the provider (use to override default headers).
177
178
  * @returns The {@link Response} extracted from the provider.
178
179
  */
179
- patch<T>(endpoint: string, body: Record<string, unknown>, options: RequestOptions): Promise<Response<T>>;
180
+ patch<T>(endpoint: string, body: RequestBody, options: RequestOptions): Promise<Response<T>>;
180
181
  /**
181
182
  * Performs a DELETE request to the provider.
182
183
  *
@@ -245,7 +245,8 @@ export class Provider {
245
245
  if (headers['Content-Type'] === 'application/x-www-form-urlencoded') {
246
246
  stringifiedBody = new URLSearchParams(body).toString();
247
247
  }
248
- else if (headers['Content-Type'] === 'application/json') {
248
+ else if (headers['Content-Type'] === 'application/json' ||
249
+ headers['Content-Type'] === 'application/json-patch+json') {
249
250
  stringifiedBody = JSON.stringify(body);
250
251
  }
251
252
  }
@@ -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
+ });
@@ -157,6 +157,39 @@ describe('Provider', () => {
157
157
  ]);
158
158
  assert.deepEqual(actualResponse, { status: 201, headers: response.headers, body: { data: 'value' } });
159
159
  });
160
+ it('accept an array as body for post request', async (context) => {
161
+ const response = new Response('{"data": "value"}', {
162
+ status: 201,
163
+ headers: { 'Content-Type': 'application/json' },
164
+ });
165
+ const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));
166
+ const actualResponse = await provider.post('/endpoint', [
167
+ { data: '1', data2: '2' },
168
+ { data: '3', data2: '4' },
169
+ ], {
170
+ credentials: { apiKey: 'apikey#1111' },
171
+ logger: logger,
172
+ signal: new AbortController().signal,
173
+ additionnalheaders: { 'Content-Type': 'application/json-patch+json', 'X-Additional-Header': 'value1' },
174
+ });
175
+ assert.equal(fetchMock.mock.calls.length, 1);
176
+ assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [
177
+ 'www.myApi.com/endpoint',
178
+ {
179
+ method: 'POST',
180
+ body: '[{"data":"1","data2":"2"},{"data":"3","data2":"4"}]',
181
+ signal: new AbortController().signal,
182
+ headers: {
183
+ 'Content-Type': 'application/json-patch+json',
184
+ Accept: 'application/json',
185
+ 'X-Custom-Provider-Header': 'value',
186
+ 'X-Provider-Credential-Header': 'apikey#1111',
187
+ 'X-Additional-Header': 'value1',
188
+ },
189
+ },
190
+ ]);
191
+ assert.deepEqual(actualResponse, { status: 201, headers: response.headers, body: { data: 'value' } });
192
+ });
160
193
  it('put with json body', async (context) => {
161
194
  const response = new Response('{"data": "value"}', {
162
195
  status: 201,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unito/integration-sdk",
3
- "version": "1.4.4",
3
+ "version": "1.6.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<
@@ -64,6 +64,8 @@ export type PreparedRequest = {
64
64
  headers: Record<string, string>;
65
65
  };
66
66
 
67
+ export type RequestBody = Record<string, unknown> | RequestBody[];
68
+
67
69
  /**
68
70
  * The Provider class is a wrapper around the fetch function to call a provider's HTTP API.
69
71
  *
@@ -184,7 +186,7 @@ export class Provider {
184
186
  * @param options RequestOptions used to adjust the call made to the provider (use to override default headers).
185
187
  * @returns The {@link Response} extracted from the provider.
186
188
  */
187
- public async post<T>(endpoint: string, body: Record<string, unknown>, options: RequestOptions): Promise<Response<T>> {
189
+ public async post<T>(endpoint: string, body: RequestBody, options: RequestOptions): Promise<Response<T>> {
188
190
  return this.fetchWrapper<T>(endpoint, body, {
189
191
  ...options,
190
192
  method: 'POST',
@@ -261,7 +263,7 @@ export class Provider {
261
263
  * @param options RequestOptions used to adjust the call made to the provider (use to override default headers).
262
264
  * @returns The {@link Response} extracted from the provider.
263
265
  */
264
- public async put<T>(endpoint: string, body: Record<string, unknown>, options: RequestOptions): Promise<Response<T>> {
266
+ public async put<T>(endpoint: string, body: RequestBody, options: RequestOptions): Promise<Response<T>> {
265
267
  return this.fetchWrapper<T>(endpoint, body, {
266
268
  ...options,
267
269
  method: 'PUT',
@@ -284,11 +286,7 @@ export class Provider {
284
286
  * @param options RequestOptions used to adjust the call made to the provider (use to override default headers).
285
287
  * @returns The {@link Response} extracted from the provider.
286
288
  */
287
- public async patch<T>(
288
- endpoint: string,
289
- body: Record<string, unknown>,
290
- options: RequestOptions,
291
- ): Promise<Response<T>> {
289
+ public async patch<T>(endpoint: string, body: RequestBody, options: RequestOptions): Promise<Response<T>> {
292
290
  return this.fetchWrapper<T>(endpoint, body, {
293
291
  ...options,
294
292
  method: 'PATCH',
@@ -337,7 +335,7 @@ export class Provider {
337
335
 
338
336
  private async fetchWrapper<T>(
339
337
  endpoint: string,
340
- body: Record<string, unknown> | null,
338
+ body: RequestBody | null,
341
339
  options: RequestOptions & { defaultHeaders: { 'Content-Type'?: string; Accept?: string }; method: string },
342
340
  ): Promise<Response<T>> {
343
341
  const { url: providerUrl, headers: providerHeaders } = await this.prepareRequest(options);
@@ -349,7 +347,10 @@ export class Provider {
349
347
  if (body) {
350
348
  if (headers['Content-Type'] === 'application/x-www-form-urlencoded') {
351
349
  stringifiedBody = new URLSearchParams(body as Record<string, string>).toString();
352
- } else if (headers['Content-Type'] === 'application/json') {
350
+ } else if (
351
+ headers['Content-Type'] === 'application/json' ||
352
+ headers['Content-Type'] === 'application/json-patch+json'
353
+ ) {
353
354
  stringifiedBody = JSON.stringify(body);
354
355
  }
355
356
  }
@@ -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
+ });
@@ -188,6 +188,47 @@ describe('Provider', () => {
188
188
  assert.deepEqual(actualResponse, { status: 201, headers: response.headers, body: { data: 'value' } });
189
189
  });
190
190
 
191
+ it('accept an array as body for post request', async context => {
192
+ const response = new Response('{"data": "value"}', {
193
+ status: 201,
194
+ headers: { 'Content-Type': 'application/json' },
195
+ });
196
+
197
+ const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response));
198
+
199
+ const actualResponse = await provider.post(
200
+ '/endpoint',
201
+ [
202
+ { data: '1', data2: '2' },
203
+ { data: '3', data2: '4' },
204
+ ],
205
+ {
206
+ credentials: { apiKey: 'apikey#1111' },
207
+ logger: logger,
208
+ signal: new AbortController().signal,
209
+ additionnalheaders: { 'Content-Type': 'application/json-patch+json', 'X-Additional-Header': 'value1' },
210
+ },
211
+ );
212
+
213
+ assert.equal(fetchMock.mock.calls.length, 1);
214
+ assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [
215
+ 'www.myApi.com/endpoint',
216
+ {
217
+ method: 'POST',
218
+ body: '[{"data":"1","data2":"2"},{"data":"3","data2":"4"}]',
219
+ signal: new AbortController().signal,
220
+ headers: {
221
+ 'Content-Type': 'application/json-patch+json',
222
+ Accept: 'application/json',
223
+ 'X-Custom-Provider-Header': 'value',
224
+ 'X-Provider-Credential-Header': 'apikey#1111',
225
+ 'X-Additional-Header': 'value1',
226
+ },
227
+ },
228
+ ]);
229
+ assert.deepEqual(actualResponse, { status: 201, headers: response.headers, body: { data: 'value' } });
230
+ });
231
+
191
232
  it('put with json body', async context => {
192
233
  const response = new Response('{"data": "value"}', {
193
234
  status: 201,