@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.
- package/dist/src/handler.js +1 -0
- package/dist/src/index.cjs +10 -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.d.ts +4 -3
- package/dist/src/resources/provider.js +2 -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 +33 -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 +10 -9
- package/test/middlewares/relations.test.ts +35 -0
- package/test/resources/provider.test.ts +41 -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) {
|
|
@@ -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
|
}
|
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;
|
|
@@ -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:
|
|
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:
|
|
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:
|
|
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
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<
|
|
@@ -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:
|
|
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:
|
|
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:
|
|
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 (
|
|
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,
|