@unito/integration-sdk 1.0.15 → 1.0.16
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.d.ts +10 -1
- package/dist/src/handler.js +34 -0
- package/dist/src/index.cjs +89 -4
- package/dist/src/resources/context.d.ts +4 -0
- package/dist/src/resources/provider.d.ts +3 -0
- package/dist/src/resources/provider.js +55 -4
- package/package.json +5 -2
- package/src/handler.ts +49 -0
- package/src/resources/context.ts +7 -1
- package/src/resources/provider.ts +66 -9
package/dist/src/handler.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import * as API from '@unito/integration-api';
|
|
3
|
-
import { GetBlobContext, GetItemContext, GetCollectionContext, CreateItemContext, UpdateItemContext, DeleteItemContext, GetCredentialAccountContext, ParseWebhooksContext, UpdateWebhookSubscriptionsContext, AcknowledgeWebhooksContext } from './resources/context.js';
|
|
3
|
+
import { GetBlobContext, GetItemContext, GetCollectionContext, CreateBlobContext, CreateItemContext, UpdateItemContext, DeleteItemContext, GetCredentialAccountContext, ParseWebhooksContext, UpdateWebhookSubscriptionsContext, AcknowledgeWebhooksContext } from './resources/context.js';
|
|
4
4
|
/**
|
|
5
5
|
* Handler called to get an individual item.
|
|
6
6
|
*
|
|
@@ -15,6 +15,13 @@ export type GetItemHandler = (context: GetItemContext<any, any>) => Promise<API.
|
|
|
15
15
|
* @return An {@link API.Collection} containing requested items and a link to the next page, if applicable.
|
|
16
16
|
*/
|
|
17
17
|
export type GetCollectionHandler = (context: GetCollectionContext<any, any>) => Promise<API.Collection>;
|
|
18
|
+
/**
|
|
19
|
+
* Handler called to create an item.
|
|
20
|
+
*
|
|
21
|
+
* @param context {@link CreateBlobContext}
|
|
22
|
+
* @returns An {@link API.Item} containing a path to the created item.
|
|
23
|
+
*/
|
|
24
|
+
export type CreateBlobHandler = (context: CreateBlobContext<any, any>) => Promise<API.Item>;
|
|
18
25
|
/**
|
|
19
26
|
* Handler called to create an item.
|
|
20
27
|
*
|
|
@@ -86,12 +93,14 @@ export type AcknowledgeWebhooksHandler = (context: AcknowledgeWebhooksContext<an
|
|
|
86
93
|
export type ItemHandlers = {
|
|
87
94
|
getItem?: GetItemHandler;
|
|
88
95
|
getCollection?: GetCollectionHandler;
|
|
96
|
+
createBlob?: CreateBlobHandler;
|
|
89
97
|
createItem?: CreateItemHandler;
|
|
90
98
|
updateItem?: UpdateItemHandler;
|
|
91
99
|
deleteItem?: DeleteItemHandler;
|
|
92
100
|
};
|
|
93
101
|
export type BlobHandlers = {
|
|
94
102
|
getBlob: GetBlobHandler;
|
|
103
|
+
createBlob?: CreateBlobHandler;
|
|
95
104
|
};
|
|
96
105
|
export type CredentialAccountHandlers = {
|
|
97
106
|
getCredentialAccount: GetCredentialAccountHandler;
|
package/dist/src/handler.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import { InvalidHandler } from './errors.js';
|
|
3
3
|
import { UnauthorizedError, BadRequestError } from './httpErrors.js';
|
|
4
|
+
import busboy from 'busboy';
|
|
4
5
|
function assertValidPath(path) {
|
|
5
6
|
if (!path.startsWith('/')) {
|
|
6
7
|
throw new InvalidHandler(`The provided path '${path}' is invalid. All paths must start with a '/'.`);
|
|
@@ -121,6 +122,39 @@ export class Handler {
|
|
|
121
122
|
res.status(201).send(createItemSummary);
|
|
122
123
|
});
|
|
123
124
|
}
|
|
125
|
+
if (this.handlers.createBlob) {
|
|
126
|
+
const handler = this.handlers.createBlob;
|
|
127
|
+
console.debug(` Enabling createBlob at POST ${this.pathWithIdentifier}`);
|
|
128
|
+
router.post(this.path, async (req, res) => {
|
|
129
|
+
if (!res.locals.credentials) {
|
|
130
|
+
throw new UnauthorizedError();
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Some of the integrations, servicenow for example,
|
|
134
|
+
* will need to add more information to the form data that is being passed to the upload attachment handler.
|
|
135
|
+
* This is why we need to use busboy to parse the form data, extract the information about the file and pass it to the handler.
|
|
136
|
+
*/
|
|
137
|
+
const bb = busboy({ headers: req.headers });
|
|
138
|
+
bb.on('file', async (_name, file, info) => {
|
|
139
|
+
const createdBlob = await handler({
|
|
140
|
+
credentials: res.locals.credentials,
|
|
141
|
+
secrets: res.locals.secrets,
|
|
142
|
+
body: {
|
|
143
|
+
file: file,
|
|
144
|
+
mimeType: info.mimeType,
|
|
145
|
+
encoding: info.encoding,
|
|
146
|
+
filename: info.filename,
|
|
147
|
+
},
|
|
148
|
+
logger: res.locals.logger,
|
|
149
|
+
signal: res.locals.signal,
|
|
150
|
+
params: req.params,
|
|
151
|
+
query: req.query,
|
|
152
|
+
});
|
|
153
|
+
res.status(201).send(createdBlob);
|
|
154
|
+
});
|
|
155
|
+
req.pipe(bb);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
124
158
|
if (this.handlers.getItem) {
|
|
125
159
|
const handler = this.handlers.getItem;
|
|
126
160
|
console.debug(` Enabling getItem at GET ${this.pathWithIdentifier}`);
|
package/dist/src/index.cjs
CHANGED
|
@@ -4,6 +4,8 @@ var integrationApi = require('@unito/integration-api');
|
|
|
4
4
|
var cachette = require('cachette');
|
|
5
5
|
var crypto = require('crypto');
|
|
6
6
|
var express = require('express');
|
|
7
|
+
var busboy = require('busboy');
|
|
8
|
+
var https = require('https');
|
|
7
9
|
|
|
8
10
|
function _interopNamespaceDefault(e) {
|
|
9
11
|
var n = Object.create(null);
|
|
@@ -667,6 +669,39 @@ class Handler {
|
|
|
667
669
|
res.status(201).send(createItemSummary);
|
|
668
670
|
});
|
|
669
671
|
}
|
|
672
|
+
if (this.handlers.createBlob) {
|
|
673
|
+
const handler = this.handlers.createBlob;
|
|
674
|
+
console.debug(` Enabling createBlob at POST ${this.pathWithIdentifier}`);
|
|
675
|
+
router.post(this.path, async (req, res) => {
|
|
676
|
+
if (!res.locals.credentials) {
|
|
677
|
+
throw new UnauthorizedError();
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Some of the integrations, servicenow for example,
|
|
681
|
+
* will need to add more information to the form data that is being passed to the upload attachment handler.
|
|
682
|
+
* This is why we need to use busboy to parse the form data, extract the information about the file and pass it to the handler.
|
|
683
|
+
*/
|
|
684
|
+
const bb = busboy({ headers: req.headers });
|
|
685
|
+
bb.on('file', async (_name, file, info) => {
|
|
686
|
+
const createdBlob = await handler({
|
|
687
|
+
credentials: res.locals.credentials,
|
|
688
|
+
secrets: res.locals.secrets,
|
|
689
|
+
body: {
|
|
690
|
+
file: file,
|
|
691
|
+
mimeType: info.mimeType,
|
|
692
|
+
encoding: info.encoding,
|
|
693
|
+
filename: info.filename,
|
|
694
|
+
},
|
|
695
|
+
logger: res.locals.logger,
|
|
696
|
+
signal: res.locals.signal,
|
|
697
|
+
params: req.params,
|
|
698
|
+
query: req.query,
|
|
699
|
+
});
|
|
700
|
+
res.status(201).send(createdBlob);
|
|
701
|
+
});
|
|
702
|
+
req.pipe(bb);
|
|
703
|
+
});
|
|
704
|
+
}
|
|
670
705
|
if (this.handlers.getItem) {
|
|
671
706
|
const handler = this.handlers.getItem;
|
|
672
707
|
console.debug(` Enabling getItem at GET ${this.pathWithIdentifier}`);
|
|
@@ -1080,6 +1115,52 @@ class Provider {
|
|
|
1080
1115
|
},
|
|
1081
1116
|
});
|
|
1082
1117
|
}
|
|
1118
|
+
async postForm(endpoint, form, options) {
|
|
1119
|
+
const { url: providerUrl, headers: providerHeaders } = this.prepareRequest(options);
|
|
1120
|
+
const absoluteUrl = this.generateAbsoluteUrl(providerUrl, endpoint, options.queryParams);
|
|
1121
|
+
const headers = { ...form.getHeaders(), ...providerHeaders, ...options.additionnalheaders };
|
|
1122
|
+
const reqOptions = {
|
|
1123
|
+
method: 'POST',
|
|
1124
|
+
headers,
|
|
1125
|
+
};
|
|
1126
|
+
/**
|
|
1127
|
+
* For some obscure reason we can't use the fetch API to send a form data, so we have to use the native https module
|
|
1128
|
+
* It seems that there is a miscalculation of the Content-Length headers that generates an error :
|
|
1129
|
+
* --> headers length is different from the actual body length
|
|
1130
|
+
* The goto solution recommended across the internet for this, is to simply drop the header.
|
|
1131
|
+
* However, some integrations like Servicenow, will not accept the request if it doesn't contain that header
|
|
1132
|
+
*/
|
|
1133
|
+
const callToProvider = async () => {
|
|
1134
|
+
return new Promise((resolve, reject) => {
|
|
1135
|
+
try {
|
|
1136
|
+
const request = https.request(absoluteUrl, reqOptions, response => {
|
|
1137
|
+
response.setEncoding('utf8');
|
|
1138
|
+
let responseBody = '';
|
|
1139
|
+
response.on('data', chunk => {
|
|
1140
|
+
responseBody += chunk;
|
|
1141
|
+
});
|
|
1142
|
+
response.on('end', () => {
|
|
1143
|
+
try {
|
|
1144
|
+
const body = JSON.parse(responseBody);
|
|
1145
|
+
resolve({ status: 201, headers: response.headers, body });
|
|
1146
|
+
}
|
|
1147
|
+
catch (error) {
|
|
1148
|
+
reject(this.handleError(500, `Failed to parse response body: "${error}"`));
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
});
|
|
1152
|
+
request.on('error', error => {
|
|
1153
|
+
reject(this.handleError(400, `Error while calling the provider: "${error}"`));
|
|
1154
|
+
});
|
|
1155
|
+
form.pipe(request);
|
|
1156
|
+
}
|
|
1157
|
+
catch (error) {
|
|
1158
|
+
reject(this.handleError(500, `Unexpected error while calling the provider: "${error}"`));
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
};
|
|
1162
|
+
return this.rateLimiter ? this.rateLimiter(options, callToProvider) : callToProvider();
|
|
1163
|
+
}
|
|
1083
1164
|
/**
|
|
1084
1165
|
* Performs a PUT request to the provider.
|
|
1085
1166
|
*
|
|
@@ -1144,8 +1225,7 @@ class Provider {
|
|
|
1144
1225
|
},
|
|
1145
1226
|
});
|
|
1146
1227
|
}
|
|
1147
|
-
|
|
1148
|
-
const { url: providerUrl, headers: providerHeaders } = this.prepareRequest(options);
|
|
1228
|
+
generateAbsoluteUrl(providerUrl, endpoint, queryParams) {
|
|
1149
1229
|
let absoluteUrl;
|
|
1150
1230
|
if (/^https?:\/\//.test(endpoint)) {
|
|
1151
1231
|
absoluteUrl = endpoint;
|
|
@@ -1153,9 +1233,14 @@ class Provider {
|
|
|
1153
1233
|
else {
|
|
1154
1234
|
absoluteUrl = [providerUrl, endpoint.charAt(0) === '/' ? endpoint.substring(1) : endpoint].join('/');
|
|
1155
1235
|
}
|
|
1156
|
-
if (
|
|
1157
|
-
absoluteUrl = `${absoluteUrl}?${new URLSearchParams(
|
|
1236
|
+
if (queryParams) {
|
|
1237
|
+
absoluteUrl = `${absoluteUrl}?${new URLSearchParams(queryParams)}`;
|
|
1158
1238
|
}
|
|
1239
|
+
return absoluteUrl;
|
|
1240
|
+
}
|
|
1241
|
+
async fetchWrapper(endpoint, body, options) {
|
|
1242
|
+
const { url: providerUrl, headers: providerHeaders } = this.prepareRequest(options);
|
|
1243
|
+
const absoluteUrl = this.generateAbsoluteUrl(providerUrl, endpoint, options.queryParams);
|
|
1159
1244
|
const headers = { ...options.defaultHeaders, ...providerHeaders, ...options.additionnalheaders };
|
|
1160
1245
|
let stringifiedBody = null;
|
|
1161
1246
|
if (body) {
|
|
@@ -10,6 +10,7 @@ type Query = {
|
|
|
10
10
|
[key: string]: undefined | string | string[] | Query | Query[];
|
|
11
11
|
};
|
|
12
12
|
type CreateItemBody = API.CreateItemRequestPayload;
|
|
13
|
+
type CreateBlobBody = API.CreateBlobRequestPayload;
|
|
13
14
|
type UpdateItemBody = API.UpdateItemRequestPayload;
|
|
14
15
|
/**
|
|
15
16
|
* The base context object is passed to every handler function.
|
|
@@ -103,6 +104,9 @@ export type GetCollectionContext<P extends Maybe<Params> = Empty, Q extends Quer
|
|
|
103
104
|
export type CreateItemContext<P extends Maybe<Params> = Empty, Q extends Maybe<Query> = Empty, B extends CreateItemBody = API.CreateItemRequestPayload> = Context<P, Q> & {
|
|
104
105
|
body: B;
|
|
105
106
|
};
|
|
107
|
+
export type CreateBlobContext<P extends Maybe<Params> = Empty, Q extends Maybe<Query> = Empty, B extends CreateBlobBody = API.CreateBlobRequestPayload> = Context<P, Q> & {
|
|
108
|
+
body: B;
|
|
109
|
+
};
|
|
106
110
|
export type UpdateItemContext<P extends Maybe<Params> = Empty, Q extends Maybe<Query> = Empty, B extends UpdateItemBody = API.UpdateItemRequestPayload> = Context<P, Q> & {
|
|
107
111
|
body: B;
|
|
108
112
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import FormData from 'form-data';
|
|
1
2
|
import * as HttpErrors from '../httpErrors.js';
|
|
2
3
|
import { Credentials } from '../middlewares/credentials.js';
|
|
3
4
|
import Logger from '../resources/logger.js';
|
|
@@ -143,6 +144,7 @@ export declare class Provider {
|
|
|
143
144
|
* @returns The {@link Response} extracted from the provider.
|
|
144
145
|
*/
|
|
145
146
|
post<T>(endpoint: string, body: Record<string, unknown>, options: RequestOptions): Promise<Response<T>>;
|
|
147
|
+
postForm<T>(endpoint: string, form: FormData, options: RequestOptions): Promise<Response<T>>;
|
|
146
148
|
/**
|
|
147
149
|
* Performs a PUT request to the provider.
|
|
148
150
|
*
|
|
@@ -181,6 +183,7 @@ export declare class Provider {
|
|
|
181
183
|
* @returns The {@link Response} extracted from the provider.
|
|
182
184
|
*/
|
|
183
185
|
delete<T = undefined>(endpoint: string, options: RequestOptions): Promise<Response<T>>;
|
|
186
|
+
private generateAbsoluteUrl;
|
|
184
187
|
private fetchWrapper;
|
|
185
188
|
private handleError;
|
|
186
189
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import https from 'https';
|
|
1
2
|
import { buildHttpError } from '../errors.js';
|
|
2
3
|
/**
|
|
3
4
|
* The Provider class is a wrapper around the fetch function to call a provider's HTTP API.
|
|
@@ -108,6 +109,52 @@ export class Provider {
|
|
|
108
109
|
},
|
|
109
110
|
});
|
|
110
111
|
}
|
|
112
|
+
async postForm(endpoint, form, options) {
|
|
113
|
+
const { url: providerUrl, headers: providerHeaders } = this.prepareRequest(options);
|
|
114
|
+
const absoluteUrl = this.generateAbsoluteUrl(providerUrl, endpoint, options.queryParams);
|
|
115
|
+
const headers = { ...form.getHeaders(), ...providerHeaders, ...options.additionnalheaders };
|
|
116
|
+
const reqOptions = {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
headers,
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* For some obscure reason we can't use the fetch API to send a form data, so we have to use the native https module
|
|
122
|
+
* It seems that there is a miscalculation of the Content-Length headers that generates an error :
|
|
123
|
+
* --> headers length is different from the actual body length
|
|
124
|
+
* The goto solution recommended across the internet for this, is to simply drop the header.
|
|
125
|
+
* However, some integrations like Servicenow, will not accept the request if it doesn't contain that header
|
|
126
|
+
*/
|
|
127
|
+
const callToProvider = async () => {
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
try {
|
|
130
|
+
const request = https.request(absoluteUrl, reqOptions, response => {
|
|
131
|
+
response.setEncoding('utf8');
|
|
132
|
+
let responseBody = '';
|
|
133
|
+
response.on('data', chunk => {
|
|
134
|
+
responseBody += chunk;
|
|
135
|
+
});
|
|
136
|
+
response.on('end', () => {
|
|
137
|
+
try {
|
|
138
|
+
const body = JSON.parse(responseBody);
|
|
139
|
+
resolve({ status: 201, headers: response.headers, body });
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
reject(this.handleError(500, `Failed to parse response body: "${error}"`));
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
request.on('error', error => {
|
|
147
|
+
reject(this.handleError(400, `Error while calling the provider: "${error}"`));
|
|
148
|
+
});
|
|
149
|
+
form.pipe(request);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
reject(this.handleError(500, `Unexpected error while calling the provider: "${error}"`));
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
return this.rateLimiter ? this.rateLimiter(options, callToProvider) : callToProvider();
|
|
157
|
+
}
|
|
111
158
|
/**
|
|
112
159
|
* Performs a PUT request to the provider.
|
|
113
160
|
*
|
|
@@ -172,8 +219,7 @@ export class Provider {
|
|
|
172
219
|
},
|
|
173
220
|
});
|
|
174
221
|
}
|
|
175
|
-
|
|
176
|
-
const { url: providerUrl, headers: providerHeaders } = this.prepareRequest(options);
|
|
222
|
+
generateAbsoluteUrl(providerUrl, endpoint, queryParams) {
|
|
177
223
|
let absoluteUrl;
|
|
178
224
|
if (/^https?:\/\//.test(endpoint)) {
|
|
179
225
|
absoluteUrl = endpoint;
|
|
@@ -181,9 +227,14 @@ export class Provider {
|
|
|
181
227
|
else {
|
|
182
228
|
absoluteUrl = [providerUrl, endpoint.charAt(0) === '/' ? endpoint.substring(1) : endpoint].join('/');
|
|
183
229
|
}
|
|
184
|
-
if (
|
|
185
|
-
absoluteUrl = `${absoluteUrl}?${new URLSearchParams(
|
|
230
|
+
if (queryParams) {
|
|
231
|
+
absoluteUrl = `${absoluteUrl}?${new URLSearchParams(queryParams)}`;
|
|
186
232
|
}
|
|
233
|
+
return absoluteUrl;
|
|
234
|
+
}
|
|
235
|
+
async fetchWrapper(endpoint, body, options) {
|
|
236
|
+
const { url: providerUrl, headers: providerHeaders } = this.prepareRequest(options);
|
|
237
|
+
const absoluteUrl = this.generateAbsoluteUrl(providerUrl, endpoint, options.queryParams);
|
|
187
238
|
const headers = { ...options.defaultHeaders, ...providerHeaders, ...options.additionnalheaders };
|
|
188
239
|
let stringifiedBody = null;
|
|
189
240
|
if (body) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unito/integration-sdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
4
4
|
"description": "Integration SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "dist/src/index.d.ts",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"ci:test": "npm run test"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
+
"@types/busboy": "^1.5.4",
|
|
36
37
|
"@types/express": "4.x",
|
|
37
38
|
"@types/node": "20.x",
|
|
38
39
|
"@typescript-eslint/eslint-plugin": "7.x",
|
|
@@ -46,8 +47,10 @@
|
|
|
46
47
|
},
|
|
47
48
|
"dependencies": {
|
|
48
49
|
"@unito/integration-api": "0.x",
|
|
50
|
+
"busboy": "^1.6.0",
|
|
49
51
|
"cachette": "2.x",
|
|
50
|
-
"express": "^5.0.0-beta.3"
|
|
52
|
+
"express": "^5.0.0-beta.3",
|
|
53
|
+
"form-data": "^4.0.0"
|
|
51
54
|
},
|
|
52
55
|
"keywords": [
|
|
53
56
|
"typescript",
|
package/src/handler.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
GetBlobContext,
|
|
7
7
|
GetItemContext,
|
|
8
8
|
GetCollectionContext,
|
|
9
|
+
CreateBlobContext,
|
|
9
10
|
CreateItemContext,
|
|
10
11
|
UpdateItemContext,
|
|
11
12
|
DeleteItemContext,
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
AcknowledgeWebhooksContext,
|
|
16
17
|
} from './resources/context.js';
|
|
17
18
|
|
|
19
|
+
import busboy, { FileInfo } from 'busboy';
|
|
18
20
|
/**
|
|
19
21
|
* Handler called to get an individual item.
|
|
20
22
|
*
|
|
@@ -31,6 +33,14 @@ export type GetItemHandler = (context: GetItemContext<any, any>) => Promise<API.
|
|
|
31
33
|
*/
|
|
32
34
|
export type GetCollectionHandler = (context: GetCollectionContext<any, any>) => Promise<API.Collection>;
|
|
33
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Handler called to create an item.
|
|
38
|
+
*
|
|
39
|
+
* @param context {@link CreateBlobContext}
|
|
40
|
+
* @returns An {@link API.Item} containing a path to the created item.
|
|
41
|
+
*/
|
|
42
|
+
export type CreateBlobHandler = (context: CreateBlobContext<any, any>) => Promise<API.Item>;
|
|
43
|
+
|
|
34
44
|
/**
|
|
35
45
|
* Handler called to create an item.
|
|
36
46
|
*
|
|
@@ -118,6 +128,7 @@ export type AcknowledgeWebhooksHandler = (
|
|
|
118
128
|
export type ItemHandlers = {
|
|
119
129
|
getItem?: GetItemHandler;
|
|
120
130
|
getCollection?: GetCollectionHandler;
|
|
131
|
+
createBlob?: CreateBlobHandler;
|
|
121
132
|
createItem?: CreateItemHandler;
|
|
122
133
|
updateItem?: UpdateItemHandler;
|
|
123
134
|
deleteItem?: DeleteItemHandler;
|
|
@@ -125,6 +136,7 @@ export type ItemHandlers = {
|
|
|
125
136
|
|
|
126
137
|
export type BlobHandlers = {
|
|
127
138
|
getBlob: GetBlobHandler;
|
|
139
|
+
createBlob?: CreateBlobHandler;
|
|
128
140
|
};
|
|
129
141
|
|
|
130
142
|
export type CredentialAccountHandlers = {
|
|
@@ -323,6 +335,43 @@ export class Handler {
|
|
|
323
335
|
});
|
|
324
336
|
}
|
|
325
337
|
|
|
338
|
+
if (this.handlers.createBlob) {
|
|
339
|
+
const handler = this.handlers.createBlob;
|
|
340
|
+
|
|
341
|
+
console.debug(` Enabling createBlob at POST ${this.pathWithIdentifier}`);
|
|
342
|
+
|
|
343
|
+
router.post(this.path, async (req, res) => {
|
|
344
|
+
if (!res.locals.credentials) {
|
|
345
|
+
throw new UnauthorizedError();
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Some of the integrations, servicenow for example,
|
|
349
|
+
* will need to add more information to the form data that is being passed to the upload attachment handler.
|
|
350
|
+
* This is why we need to use busboy to parse the form data, extract the information about the file and pass it to the handler.
|
|
351
|
+
*/
|
|
352
|
+
const bb = busboy({ headers: req.headers });
|
|
353
|
+
bb.on('file', async (_name, file, info: FileInfo) => {
|
|
354
|
+
const createdBlob = await handler({
|
|
355
|
+
credentials: res.locals.credentials,
|
|
356
|
+
secrets: res.locals.secrets,
|
|
357
|
+
body: {
|
|
358
|
+
file: file,
|
|
359
|
+
mimeType: info.mimeType,
|
|
360
|
+
encoding: info.encoding,
|
|
361
|
+
filename: info.filename,
|
|
362
|
+
},
|
|
363
|
+
logger: res.locals.logger,
|
|
364
|
+
signal: res.locals.signal,
|
|
365
|
+
params: req.params,
|
|
366
|
+
query: req.query,
|
|
367
|
+
});
|
|
368
|
+
res.status(201).send(createdBlob);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
req.pipe(bb);
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
326
375
|
if (this.handlers.getItem) {
|
|
327
376
|
const handler = this.handlers.getItem;
|
|
328
377
|
|
package/src/resources/context.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/* c8 ignore start */
|
|
2
2
|
import * as API from '@unito/integration-api';
|
|
3
|
-
|
|
4
3
|
import Logger from './logger.js';
|
|
5
4
|
import { Credentials } from '../middlewares/credentials.js';
|
|
6
5
|
import { Secrets } from 'src/middlewares/secrets.js';
|
|
@@ -13,6 +12,7 @@ type Query = {
|
|
|
13
12
|
[key: string]: undefined | string | string[] | Query | Query[];
|
|
14
13
|
};
|
|
15
14
|
type CreateItemBody = API.CreateItemRequestPayload;
|
|
15
|
+
type CreateBlobBody = API.CreateBlobRequestPayload;
|
|
16
16
|
type UpdateItemBody = API.UpdateItemRequestPayload;
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -114,6 +114,12 @@ export type CreateItemContext<
|
|
|
114
114
|
B extends CreateItemBody = API.CreateItemRequestPayload,
|
|
115
115
|
> = Context<P, Q> & { body: B };
|
|
116
116
|
|
|
117
|
+
export type CreateBlobContext<
|
|
118
|
+
P extends Maybe<Params> = Empty,
|
|
119
|
+
Q extends Maybe<Query> = Empty,
|
|
120
|
+
B extends CreateBlobBody = API.CreateBlobRequestPayload,
|
|
121
|
+
> = Context<P, Q> & { body: B };
|
|
122
|
+
|
|
117
123
|
export type UpdateItemContext<
|
|
118
124
|
P extends Maybe<Params> = Empty,
|
|
119
125
|
Q extends Maybe<Query> = Empty,
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import https from 'https';
|
|
2
|
+
import FormData from 'form-data';
|
|
3
|
+
|
|
1
4
|
import { buildHttpError } from '../errors.js';
|
|
2
5
|
import * as HttpErrors from '../httpErrors.js';
|
|
3
6
|
import { Credentials } from '../middlewares/credentials.js';
|
|
@@ -177,6 +180,57 @@ export class Provider {
|
|
|
177
180
|
});
|
|
178
181
|
}
|
|
179
182
|
|
|
183
|
+
public async postForm<T>(endpoint: string, form: FormData, options: RequestOptions): Promise<Response<T>> {
|
|
184
|
+
const { url: providerUrl, headers: providerHeaders } = this.prepareRequest(options);
|
|
185
|
+
const absoluteUrl = this.generateAbsoluteUrl(providerUrl, endpoint, options.queryParams);
|
|
186
|
+
const headers = { ...form.getHeaders(), ...providerHeaders, ...options.additionnalheaders };
|
|
187
|
+
|
|
188
|
+
const reqOptions = {
|
|
189
|
+
method: 'POST',
|
|
190
|
+
headers,
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* For some obscure reason we can't use the fetch API to send a form data, so we have to use the native https module
|
|
194
|
+
* It seems that there is a miscalculation of the Content-Length headers that generates an error :
|
|
195
|
+
* --> headers length is different from the actual body length
|
|
196
|
+
* The goto solution recommended across the internet for this, is to simply drop the header.
|
|
197
|
+
* However, some integrations like Servicenow, will not accept the request if it doesn't contain that header
|
|
198
|
+
*/
|
|
199
|
+
const callToProvider = async (): Promise<Response<T>> => {
|
|
200
|
+
return new Promise((resolve, reject) => {
|
|
201
|
+
try {
|
|
202
|
+
const request = https.request(absoluteUrl, reqOptions, response => {
|
|
203
|
+
response.setEncoding('utf8');
|
|
204
|
+
let responseBody = '';
|
|
205
|
+
|
|
206
|
+
response.on('data', chunk => {
|
|
207
|
+
responseBody += chunk;
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
response.on('end', () => {
|
|
211
|
+
try {
|
|
212
|
+
const body = JSON.parse(responseBody) as T;
|
|
213
|
+
resolve({ status: 201, headers: response.headers as unknown as Headers, body });
|
|
214
|
+
} catch (error) {
|
|
215
|
+
reject(this.handleError(500, `Failed to parse response body: "${error}"`));
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
request.on('error', error => {
|
|
221
|
+
reject(this.handleError(400, `Error while calling the provider: "${error}"`));
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
form.pipe(request);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
reject(this.handleError(500, `Unexpected error while calling the provider: "${error}"`));
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
return this.rateLimiter ? this.rateLimiter(options, callToProvider) : callToProvider();
|
|
232
|
+
}
|
|
233
|
+
|
|
180
234
|
/**
|
|
181
235
|
* Performs a PUT request to the provider.
|
|
182
236
|
*
|
|
@@ -248,13 +302,7 @@ export class Provider {
|
|
|
248
302
|
});
|
|
249
303
|
}
|
|
250
304
|
|
|
251
|
-
private
|
|
252
|
-
endpoint: string,
|
|
253
|
-
body: Record<string, unknown> | null,
|
|
254
|
-
options: RequestOptions & { defaultHeaders: { 'Content-Type'?: string; Accept?: string }; method: string },
|
|
255
|
-
): Promise<Response<T>> {
|
|
256
|
-
const { url: providerUrl, headers: providerHeaders } = this.prepareRequest(options);
|
|
257
|
-
|
|
305
|
+
private generateAbsoluteUrl(providerUrl: string, endpoint: string, queryParams?: { [key: string]: string }): string {
|
|
258
306
|
let absoluteUrl;
|
|
259
307
|
|
|
260
308
|
if (/^https?:\/\//.test(endpoint)) {
|
|
@@ -263,10 +311,19 @@ export class Provider {
|
|
|
263
311
|
absoluteUrl = [providerUrl, endpoint.charAt(0) === '/' ? endpoint.substring(1) : endpoint].join('/');
|
|
264
312
|
}
|
|
265
313
|
|
|
266
|
-
if (
|
|
267
|
-
absoluteUrl = `${absoluteUrl}?${new URLSearchParams(
|
|
314
|
+
if (queryParams) {
|
|
315
|
+
absoluteUrl = `${absoluteUrl}?${new URLSearchParams(queryParams)}`;
|
|
268
316
|
}
|
|
317
|
+
return absoluteUrl;
|
|
318
|
+
}
|
|
269
319
|
|
|
320
|
+
private async fetchWrapper<T>(
|
|
321
|
+
endpoint: string,
|
|
322
|
+
body: Record<string, unknown> | null,
|
|
323
|
+
options: RequestOptions & { defaultHeaders: { 'Content-Type'?: string; Accept?: string }; method: string },
|
|
324
|
+
): Promise<Response<T>> {
|
|
325
|
+
const { url: providerUrl, headers: providerHeaders } = this.prepareRequest(options);
|
|
326
|
+
const absoluteUrl = this.generateAbsoluteUrl(providerUrl, endpoint, options.queryParams);
|
|
270
327
|
const headers = { ...options.defaultHeaders, ...providerHeaders, ...options.additionnalheaders };
|
|
271
328
|
|
|
272
329
|
let stringifiedBody: string | null = null;
|