@unito/integration-sdk 0.1.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.
Files changed (142) hide show
  1. package/.eslintrc.cjs +27 -0
  2. package/.nvmrc +1 -0
  3. package/.prettierignore +2 -0
  4. package/.prettierrc +7 -0
  5. package/LICENSE +3 -0
  6. package/README.md +16 -0
  7. package/dist/src/api/index.d.ts +2 -0
  8. package/dist/src/api/index.d.ts.map +1 -0
  9. package/dist/src/api/index.js +2 -0
  10. package/dist/src/api/index.js.map +1 -0
  11. package/dist/src/app/errors/HTTPError.d.ts +5 -0
  12. package/dist/src/app/errors/HTTPError.d.ts.map +1 -0
  13. package/dist/src/app/errors/HTTPError.js +8 -0
  14. package/dist/src/app/errors/HTTPError.js.map +1 -0
  15. package/dist/src/app/errors/HTTPNotFoundError.d.ts +5 -0
  16. package/dist/src/app/errors/HTTPNotFoundError.d.ts.map +1 -0
  17. package/dist/src/app/errors/HTTPNotFoundError.js +7 -0
  18. package/dist/src/app/errors/HTTPNotFoundError.js.map +1 -0
  19. package/dist/src/app/errors/HTTPUnprocessableEntityError.d.ts +5 -0
  20. package/dist/src/app/errors/HTTPUnprocessableEntityError.d.ts.map +1 -0
  21. package/dist/src/app/errors/HTTPUnprocessableEntityError.js +7 -0
  22. package/dist/src/app/errors/HTTPUnprocessableEntityError.js.map +1 -0
  23. package/dist/src/app/errors/index.d.ts +4 -0
  24. package/dist/src/app/errors/index.d.ts.map +1 -0
  25. package/dist/src/app/errors/index.js +4 -0
  26. package/dist/src/app/errors/index.js.map +1 -0
  27. package/dist/src/app/index.d.ts +6 -0
  28. package/dist/src/app/index.d.ts.map +1 -0
  29. package/dist/src/app/index.js +80 -0
  30. package/dist/src/app/index.js.map +1 -0
  31. package/dist/src/app/integration.d.ts +5 -0
  32. package/dist/src/app/integration.d.ts.map +1 -0
  33. package/dist/src/app/integration.js +86 -0
  34. package/dist/src/app/integration.js.map +1 -0
  35. package/dist/src/app/itemNode.d.ts +8 -0
  36. package/dist/src/app/itemNode.d.ts.map +1 -0
  37. package/dist/src/app/itemNode.js +13 -0
  38. package/dist/src/app/itemNode.js.map +1 -0
  39. package/dist/src/app/middlewares/withCorrelationId.d.ts +11 -0
  40. package/dist/src/app/middlewares/withCorrelationId.d.ts.map +1 -0
  41. package/dist/src/app/middlewares/withCorrelationId.js +8 -0
  42. package/dist/src/app/middlewares/withCorrelationId.js.map +1 -0
  43. package/dist/src/app/middlewares/withLogger.d.ts +12 -0
  44. package/dist/src/app/middlewares/withLogger.d.ts.map +1 -0
  45. package/dist/src/app/middlewares/withLogger.js +18 -0
  46. package/dist/src/app/middlewares/withLogger.js.map +1 -0
  47. package/dist/src/errors.d.ts +15 -0
  48. package/dist/src/errors.js +39 -0
  49. package/dist/src/handler.d.ts +67 -0
  50. package/dist/src/handler.js +224 -0
  51. package/dist/src/httpErrors.d.ts +22 -0
  52. package/dist/src/httpErrors.js +37 -0
  53. package/dist/src/index.d.ts +6 -0
  54. package/dist/src/index.d.ts.map +1 -0
  55. package/dist/src/index.js +6 -0
  56. package/dist/src/index.js.map +1 -0
  57. package/dist/src/integration.d.ts +9 -0
  58. package/dist/src/integration.js +108 -0
  59. package/dist/src/middlewares/correlationId.d.ts +10 -0
  60. package/dist/src/middlewares/correlationId.js +6 -0
  61. package/dist/src/middlewares/credentials.d.ts +11 -0
  62. package/dist/src/middlewares/credentials.js +17 -0
  63. package/dist/src/middlewares/errors.d.ts +3 -0
  64. package/dist/src/middlewares/errors.js +22 -0
  65. package/dist/src/middlewares/filters.d.ts +16 -0
  66. package/dist/src/middlewares/filters.js +25 -0
  67. package/dist/src/middlewares/finish.d.ts +11 -0
  68. package/dist/src/middlewares/finish.js +10 -0
  69. package/dist/src/middlewares/logger.d.ts +12 -0
  70. package/dist/src/middlewares/logger.js +18 -0
  71. package/dist/src/middlewares/notFound.d.ts +3 -0
  72. package/dist/src/middlewares/notFound.js +8 -0
  73. package/dist/src/middlewares/selects.d.ts +10 -0
  74. package/dist/src/middlewares/selects.js +11 -0
  75. package/dist/src/resources/cache.d.ts +4 -0
  76. package/dist/src/resources/cache.d.ts.map +1 -0
  77. package/dist/src/resources/cache.js +25 -0
  78. package/dist/src/resources/cache.js.map +1 -0
  79. package/dist/src/resources/context.d.ts +78 -0
  80. package/dist/src/resources/context.js +2 -0
  81. package/dist/src/resources/logger.d.ts +14 -0
  82. package/dist/src/resources/logger.d.ts.map +1 -0
  83. package/dist/src/resources/logger.js +46 -0
  84. package/dist/src/resources/logger.js.map +1 -0
  85. package/dist/src/resources/provider.d.ts +36 -0
  86. package/dist/src/resources/provider.js +76 -0
  87. package/dist/test/errors.test.d.ts +1 -0
  88. package/dist/test/errors.test.js +16 -0
  89. package/dist/test/handler.test.d.ts +1 -0
  90. package/dist/test/handler.test.js +146 -0
  91. package/dist/test/middlewares/correlationId.test.d.ts +1 -0
  92. package/dist/test/middlewares/correlationId.test.js +19 -0
  93. package/dist/test/middlewares/credentials.test.d.ts +1 -0
  94. package/dist/test/middlewares/credentials.test.js +37 -0
  95. package/dist/test/middlewares/errors.test.d.ts +1 -0
  96. package/dist/test/middlewares/errors.test.js +64 -0
  97. package/dist/test/middlewares/filters.test.d.ts +1 -0
  98. package/dist/test/middlewares/filters.test.js +26 -0
  99. package/dist/test/middlewares/finish.test.d.ts +1 -0
  100. package/dist/test/middlewares/finish.test.js +68 -0
  101. package/dist/test/middlewares/logger.test.d.ts +1 -0
  102. package/dist/test/middlewares/logger.test.js +41 -0
  103. package/dist/test/middlewares/notFound.test.d.ts +1 -0
  104. package/dist/test/middlewares/notFound.test.js +27 -0
  105. package/dist/test/middlewares/selects.test.d.ts +1 -0
  106. package/dist/test/middlewares/selects.test.js +21 -0
  107. package/dist/test/resources/cache.test.d.ts +1 -0
  108. package/dist/test/resources/cache.test.js +25 -0
  109. package/dist/test/resources/logger.test.d.ts +1 -0
  110. package/dist/test/resources/logger.test.js +67 -0
  111. package/dist/tsconfig.tsbuildinfo +1 -0
  112. package/package.json +59 -0
  113. package/src/errors.ts +34 -0
  114. package/src/handler.ts +404 -0
  115. package/src/httpErrors.ts +44 -0
  116. package/src/index.ts +10 -0
  117. package/src/integration.ts +129 -0
  118. package/src/middlewares/correlationId.ts +19 -0
  119. package/src/middlewares/credentials.ts +35 -0
  120. package/src/middlewares/errors.ts +30 -0
  121. package/src/middlewares/filters.ts +51 -0
  122. package/src/middlewares/finish.ts +24 -0
  123. package/src/middlewares/logger.ts +36 -0
  124. package/src/middlewares/notFound.ts +13 -0
  125. package/src/middlewares/selects.ts +31 -0
  126. package/src/resources/cache.ts +34 -0
  127. package/src/resources/context.ts +113 -0
  128. package/src/resources/logger.ts +57 -0
  129. package/src/resources/provider.ts +120 -0
  130. package/test/errors.test.ts +17 -0
  131. package/test/handler.test.ts +178 -0
  132. package/test/middlewares/correlationId.test.ts +26 -0
  133. package/test/middlewares/credentials.test.ts +52 -0
  134. package/test/middlewares/errors.test.ts +78 -0
  135. package/test/middlewares/filters.test.ts +39 -0
  136. package/test/middlewares/finish.test.ts +81 -0
  137. package/test/middlewares/logger.test.ts +57 -0
  138. package/test/middlewares/notFound.test.ts +32 -0
  139. package/test/middlewares/selects.test.ts +34 -0
  140. package/test/resources/cache.test.ts +31 -0
  141. package/test/resources/logger.test.ts +80 -0
  142. package/tsconfig.json +29 -0
package/src/handler.ts ADDED
@@ -0,0 +1,404 @@
1
+ import { Router } from 'express';
2
+ import * as API from '@unito/integration-api';
3
+ import { InvalidHandler } from './errors.js';
4
+ import { UnauthorizedError, BadRequestError } from './httpErrors.js';
5
+ import {
6
+ GetItemContext,
7
+ GetCollectionContext,
8
+ CreateItemContext,
9
+ UpdateItemContext,
10
+ DeleteItemContext,
11
+ GetCredentialAccountContext,
12
+ ParseWebhooksContext,
13
+ UpdateWebhookSubscriptionsContext,
14
+ AckknowledgeWebhooksContext,
15
+ } from './resources/context.js';
16
+
17
+ /**
18
+ * Handler called to get an individual item.
19
+ */
20
+ export type GetItemHandler = (context: GetItemContext<any, any>) => Promise<API.Item>;
21
+
22
+ /**
23
+ * Handler called to retrieve a collection of items.
24
+ */
25
+ export type GetCollectionHandler = (context: GetCollectionContext<any, any>) => Promise<API.Collection>;
26
+
27
+ /**
28
+ * Handler called to create an item.
29
+ */
30
+ export type CreateItemHandler = (context: CreateItemContext<any, any, any>) => Promise<API.ItemSummary>;
31
+
32
+ /**
33
+ * Handler called to update an item.
34
+ */
35
+ export type UpdateItemHandler = (context: UpdateItemContext<any, any, any>) => Promise<API.Item>;
36
+
37
+ /**
38
+ * Handler called to delete an item.
39
+ */
40
+ export type DeleteItemHandler = (context: DeleteItemContext<any, any>) => Promise<void>;
41
+
42
+ /**
43
+ * Handler called to retrieve the account details associated with the credentials.
44
+ */
45
+ export type GetCredentialAccountHandler = (
46
+ context: GetCredentialAccountContext<any, any>,
47
+ ) => Promise<API.CredentialAccount>;
48
+
49
+ /**
50
+ * Handler called to parse the content of an incoming webhook.
51
+ */
52
+ export type ParseWebhooksHandler = (
53
+ context: ParseWebhooksContext<any, any, any>,
54
+ ) => Promise<API.WebhookParseResponsePayload>;
55
+
56
+ /**
57
+ * Handler called to subscribe or unsubscribe to a particular webhook.
58
+ */
59
+ export type UpdateWebhookSubscriptionsHandler = (
60
+ context: UpdateWebhookSubscriptionsContext<any, any, any>,
61
+ ) => Promise<void>;
62
+
63
+ /**
64
+ * Handler called to acknowledge the reception of a webhook.
65
+ */
66
+ export type AckknowledgeWebhooksHandler = (
67
+ context: AckknowledgeWebhooksContext<any, any, any>,
68
+ ) => Promise<API.WebhookAcknowledgeResponsePayload>;
69
+
70
+ type ItemHandlers = {
71
+ getItem?: GetItemHandler;
72
+ getCollection?: GetCollectionHandler;
73
+ createItem?: CreateItemHandler;
74
+ updateItem?: UpdateItemHandler;
75
+ deleteItem?: DeleteItemHandler;
76
+ };
77
+
78
+ type CredentialAccountHandlers = {
79
+ getCredentialAccount: GetCredentialAccountHandler;
80
+ };
81
+
82
+ type ParseWebhookHandlers = {
83
+ parseWebhooks: ParseWebhooksHandler;
84
+ };
85
+
86
+ type WebhookSubscriptionHandlers = {
87
+ updateWebhookSubscriptions: UpdateWebhookSubscriptionsHandler;
88
+ };
89
+
90
+ type AcknowledgeWebhookHandlers = {
91
+ acknowledgeWebhooks?: AckknowledgeWebhooksHandler;
92
+ };
93
+
94
+ export type HandlersInput =
95
+ | ItemHandlers
96
+ | CredentialAccountHandlers
97
+ | ParseWebhookHandlers
98
+ | WebhookSubscriptionHandlers
99
+ | AcknowledgeWebhookHandlers;
100
+
101
+ type Handlers = Partial<
102
+ ItemHandlers &
103
+ CredentialAccountHandlers &
104
+ ParseWebhookHandlers &
105
+ WebhookSubscriptionHandlers &
106
+ AcknowledgeWebhookHandlers
107
+ >;
108
+
109
+ type Path = string & { __brand: 'Path' };
110
+
111
+ function assertValidPath(path: string): asserts path is Path {
112
+ if (!path.startsWith('/')) {
113
+ throw new InvalidHandler(`The provided path '${path}' is invalid. All paths must start with a '/'.`);
114
+ }
115
+
116
+ if (path.length > 1 && path.endsWith('/')) {
117
+ throw new InvalidHandler(`The provided path '${path}' is invalid. Paths must not end with a '/'.`);
118
+ }
119
+ }
120
+
121
+ function parsePath(path: Path) {
122
+ const pathParts = path.split('/');
123
+
124
+ const lastPart = pathParts.at(-1);
125
+
126
+ if (pathParts.length > 1 && lastPart && lastPart.startsWith(':')) {
127
+ pathParts.pop();
128
+ }
129
+
130
+ return { pathWithIdentifier: path, path: pathParts.join('/') as Path };
131
+ }
132
+
133
+ function assertCreateItemRequestPayload(body: unknown): asserts body is API.CreateItemRequestPayload {
134
+ if (typeof body !== 'object' || body === null) {
135
+ throw new BadRequestError('Invalid CreateItemRequestPayload');
136
+ }
137
+ }
138
+
139
+ function assertUpdateItemRequestPayload(body: unknown): asserts body is API.UpdateItemRequestPayload {
140
+ if (typeof body !== 'object' || body === null) {
141
+ throw new BadRequestError('Invalid UpdateItemRequestPayload');
142
+ }
143
+ }
144
+
145
+ function assertWebhookParseRequestPayload(body: unknown): asserts body is API.WebhookParseRequestPayload {
146
+ if (typeof body !== 'object' || body === null) {
147
+ throw new BadRequestError('Invalid WebhookParseRequestPayload');
148
+ }
149
+
150
+ if (!('payload' in body) || body.payload !== 'string') {
151
+ throw new BadRequestError("Missing required 'payload' property in WebhookParseRequestPayload");
152
+ }
153
+
154
+ if (!('url' in body) || body.url !== 'string') {
155
+ throw new BadRequestError("Missing required 'url' property in WebhookParseRequestPayload");
156
+ }
157
+ }
158
+
159
+ function assertWebhookSubscriptionRequestPayload(body: unknown): asserts body is API.WebhookSubscriptionRequestPayload {
160
+ if (typeof body !== 'object' || body === null) {
161
+ throw new BadRequestError('Invalid WebhookSubscriptionRequestPayload');
162
+ }
163
+
164
+ if (!('itemPath' in body) || body.itemPath !== 'string') {
165
+ throw new BadRequestError("Missing required 'itemPath' property in WebhookSubscriptionRequestPayload");
166
+ }
167
+
168
+ if (!('targetUrl' in body) || body.targetUrl !== 'string') {
169
+ throw new BadRequestError("Missing required 'targetUrl' property in WebhookSubscriptionRequestPayload");
170
+ }
171
+
172
+ if (!('action' in body) || body.action !== 'string') {
173
+ throw new BadRequestError("Missing required 'action' property in WebhookSubscriptionRequestPayload");
174
+ }
175
+
176
+ if (!['start', 'stop'].includes(body.action)) {
177
+ throw new BadRequestError("Invalid value for 'action' property in WebhookSubscriptionRequestPayload");
178
+ }
179
+ }
180
+
181
+ export class Handler {
182
+ private path: Path;
183
+ private pathWithIdentifier: Path;
184
+
185
+ private handlers: Handlers;
186
+
187
+ constructor(inputPath: string, handlers: HandlersInput) {
188
+ // Build paths.
189
+ assertValidPath(inputPath);
190
+
191
+ const { pathWithIdentifier, path } = parsePath(inputPath);
192
+
193
+ this.pathWithIdentifier = pathWithIdentifier;
194
+ this.path = path;
195
+
196
+ this.handlers = handlers;
197
+ }
198
+
199
+ public generate(): Router {
200
+ const router: Router = Router({ caseSensitive: true });
201
+
202
+ console.debug(`\x1b[33mMounting handler at path ${this.pathWithIdentifier}`);
203
+
204
+ if (this.handlers.getCollection) {
205
+ const handler = this.handlers.getCollection;
206
+
207
+ console.debug(` Enabling GET ${this.path}`);
208
+
209
+ router.get(this.path, async (req, res) => {
210
+ if (!res.locals.credentials) {
211
+ throw new UnauthorizedError();
212
+ }
213
+
214
+ const collection = await handler({
215
+ credentials: res.locals.credentials,
216
+ selects: res.locals.selects,
217
+ filters: res.locals.filters,
218
+ logger: res.locals.logger,
219
+ params: req.params,
220
+ query: req.query,
221
+ });
222
+
223
+ res.status(200).send(collection);
224
+ });
225
+ }
226
+
227
+ if (this.handlers.createItem) {
228
+ const handler = this.handlers.createItem;
229
+
230
+ console.debug(` Enabling POST ${this.path}`);
231
+
232
+ router.post(this.path, async (req, res) => {
233
+ if (!res.locals.credentials) {
234
+ throw new UnauthorizedError();
235
+ }
236
+
237
+ assertCreateItemRequestPayload(req.body);
238
+
239
+ const createItemSummary = await handler({
240
+ credentials: res.locals.credentials,
241
+ body: req.body,
242
+ logger: res.locals.logger,
243
+ params: req.params,
244
+ query: req.query,
245
+ });
246
+
247
+ res.status(201).send(createItemSummary);
248
+ });
249
+ }
250
+
251
+ if (this.handlers.getItem) {
252
+ const handler = this.handlers.getItem;
253
+
254
+ console.debug(` Enabling GET ${this.pathWithIdentifier}`);
255
+
256
+ router.get(this.pathWithIdentifier, async (req, res) => {
257
+ if (!res.locals.credentials) {
258
+ throw new UnauthorizedError();
259
+ }
260
+
261
+ const item = await handler({
262
+ credentials: res.locals.credentials,
263
+ logger: res.locals.logger,
264
+ params: req.params,
265
+ query: req.query,
266
+ });
267
+
268
+ res.status(200).send(item);
269
+ });
270
+ }
271
+
272
+ if (this.handlers.updateItem) {
273
+ const handler = this.handlers.updateItem;
274
+
275
+ console.debug(` Enabling PATCH ${this.pathWithIdentifier}`);
276
+
277
+ router.patch(this.pathWithIdentifier, async (req, res) => {
278
+ if (!res.locals.credentials) {
279
+ throw new UnauthorizedError();
280
+ }
281
+
282
+ assertUpdateItemRequestPayload(req.body);
283
+
284
+ const item = await handler({
285
+ credentials: res.locals.credentials,
286
+ body: req.body,
287
+ logger: res.locals.logger,
288
+ params: req.params,
289
+ query: req.query,
290
+ });
291
+
292
+ res.status(200).send(item);
293
+ });
294
+ }
295
+
296
+ if (this.handlers.deleteItem) {
297
+ const handler = this.handlers.deleteItem;
298
+
299
+ console.debug(` Enabling DELETE ${this.pathWithIdentifier}`);
300
+
301
+ router.delete(this.pathWithIdentifier, async (req, res) => {
302
+ if (!res.locals.credentials) {
303
+ throw new UnauthorizedError();
304
+ }
305
+
306
+ await handler({
307
+ credentials: res.locals.credentials,
308
+ logger: res.locals.logger,
309
+ params: req.params,
310
+ query: req.query,
311
+ });
312
+
313
+ res.status(204).send(null);
314
+ });
315
+ }
316
+
317
+ if (this.handlers.getCredentialAccount) {
318
+ const handler = this.handlers.getCredentialAccount;
319
+
320
+ console.debug(` Enabling GET ${this.pathWithIdentifier}`);
321
+
322
+ router.get(this.pathWithIdentifier, async (req, res) => {
323
+ if (!res.locals.credentials) {
324
+ throw new UnauthorizedError();
325
+ }
326
+
327
+ const credentialAccount = await handler({
328
+ credentials: res.locals.credentials,
329
+ logger: res.locals.logger,
330
+ params: req.params,
331
+ query: req.query,
332
+ });
333
+
334
+ res.status(200).send(credentialAccount);
335
+ });
336
+ }
337
+
338
+ if (this.handlers.acknowledgeWebhooks) {
339
+ const handler = this.handlers.acknowledgeWebhooks;
340
+
341
+ console.debug(` Enabling POST ${this.pathWithIdentifier}`);
342
+
343
+ router.post(this.pathWithIdentifier, async (req, res) => {
344
+ assertWebhookParseRequestPayload(req.body);
345
+
346
+ const response = await handler({
347
+ logger: res.locals.logger,
348
+ params: req.params,
349
+ query: req.query,
350
+ body: req.body,
351
+ });
352
+
353
+ res.status(200).send(response);
354
+ });
355
+ }
356
+
357
+ if (this.handlers.parseWebhooks) {
358
+ const handler = this.handlers.parseWebhooks;
359
+
360
+ console.debug(` Enabling POST ${this.pathWithIdentifier}`);
361
+
362
+ router.post(this.pathWithIdentifier, async (req, res) => {
363
+ assertWebhookParseRequestPayload(req.body);
364
+
365
+ const response = await handler({
366
+ logger: res.locals.logger,
367
+ params: req.params,
368
+ query: req.query,
369
+ body: req.body,
370
+ });
371
+
372
+ res.status(200).send(response);
373
+ });
374
+ }
375
+
376
+ if (this.handlers.updateWebhookSubscriptions) {
377
+ const handler = this.handlers.updateWebhookSubscriptions;
378
+
379
+ console.debug(` Enabling PUT ${this.pathWithIdentifier}`);
380
+
381
+ router.put(this.pathWithIdentifier, async (req, res) => {
382
+ if (!res.locals.credentials) {
383
+ throw new UnauthorizedError();
384
+ }
385
+
386
+ assertWebhookSubscriptionRequestPayload(req.body);
387
+
388
+ const response = await handler({
389
+ credentials: res.locals.credentials,
390
+ body: req.body,
391
+ logger: res.locals.logger,
392
+ params: req.params,
393
+ query: req.query,
394
+ });
395
+
396
+ res.status(204).send(response);
397
+ });
398
+ }
399
+
400
+ console.debug(`\x1b[0m`);
401
+
402
+ return router;
403
+ }
404
+ }
@@ -0,0 +1,44 @@
1
+ export class HttpError extends Error {
2
+ readonly status: number;
3
+
4
+ constructor(message: string, status: number) {
5
+ super(message);
6
+ this.status = status;
7
+ }
8
+ }
9
+
10
+ export class BadRequestError extends HttpError {
11
+ constructor(message?: string) {
12
+ super(message || 'Bad request', 400);
13
+ }
14
+ }
15
+
16
+ export class UnauthorizedError extends HttpError {
17
+ constructor(message?: string) {
18
+ super(message || 'Unauthorized', 401);
19
+ }
20
+ }
21
+
22
+ export class NotFoundError extends HttpError {
23
+ constructor(message?: string) {
24
+ super(message || 'Not found', 404);
25
+ }
26
+ }
27
+
28
+ export class TimeoutError extends HttpError {
29
+ constructor(message?: string) {
30
+ super(message || 'Not found', 408);
31
+ }
32
+ }
33
+
34
+ export class UnprocessableEntityError extends HttpError {
35
+ constructor(message?: string) {
36
+ super(message || 'Unprocessable Entity', 422);
37
+ }
38
+ }
39
+
40
+ export class RateLimitExceededError extends HttpError {
41
+ constructor(message?: string) {
42
+ super(message || 'Unprocessable Entity', 429);
43
+ }
44
+ }
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ export * as Api from '@unito/integration-api';
2
+ export { default as Integration } from './integration.js';
3
+ export * from './handler.js';
4
+ export {
5
+ Provider,
6
+ type Response as ProviderResponse,
7
+ type RequestOptions as ProviderRequestOptions,
8
+ } from './resources/provider.js';
9
+ export * as HttpErrors from './httpErrors.js';
10
+ export * from './resources/context.js';
@@ -0,0 +1,129 @@
1
+ import { Server, IncomingMessage, ServerResponse } from 'http';
2
+ import express from 'express';
3
+
4
+ import { InvalidHandler } from './errors.js';
5
+ import correlationIdMiddleware from './middlewares/correlationId.js';
6
+ import loggerMiddleware from './middlewares/logger.js';
7
+ import credentialsMiddleware from './middlewares/credentials.js';
8
+ import selectsMiddleware from './middlewares/selects.js';
9
+ import errorsMiddleware from './middlewares/errors.js';
10
+ import finishMiddleware from './middlewares/finish.js';
11
+ import notFoundMiddleware from './middlewares/notFound.js';
12
+ import { initializeCache, Cache } from './resources/cache.js';
13
+ import { HandlersInput, Handler } from './handler.js';
14
+
15
+ function printErrorMessage(message: string) {
16
+ console.error();
17
+ console.error(`\x1b[31m Oups! Something went wrong! \x1b[0m`);
18
+ console.error(message);
19
+ }
20
+
21
+ export default class Integration {
22
+ private handlers: Handler[];
23
+
24
+ private instance: Server<typeof IncomingMessage, typeof ServerResponse> | undefined = undefined;
25
+
26
+ private cache: Cache | undefined = undefined;
27
+
28
+ constructor() {
29
+ this.handlers = [];
30
+ }
31
+
32
+ public addHandler(path: string, handlers: HandlersInput): void {
33
+ if (this.instance) {
34
+ printErrorMessage(`
35
+ It seems like you're trying to add a handler after the server has already started. This is probably
36
+ a mistake as calling the start() function essentially starts the server and ignore any further change.
37
+ To fix this error, move all your addHandler() calls before the start() function.
38
+ `);
39
+ process.exit(1);
40
+ }
41
+
42
+ try {
43
+ this.handlers.push(new Handler(path, handlers));
44
+ } catch (error) {
45
+ if (error instanceof InvalidHandler) {
46
+ printErrorMessage(`
47
+ It seems like you're trying to add an invalid handler. The exact error message is:
48
+
49
+ > ${error.message}
50
+
51
+ You must address this issue before trying again.
52
+ `);
53
+ } else {
54
+ printErrorMessage(`
55
+ An unexpected error happened as we were trying to add your handler. The exact error message is;
56
+
57
+ > ${(error as Error).message}
58
+ `);
59
+ }
60
+ process.exit(1);
61
+ }
62
+ }
63
+
64
+ public start() {
65
+ // Initialize the cache.
66
+ this.cache = initializeCache();
67
+
68
+ // Express Server initialization
69
+ const app: express.Application = express();
70
+
71
+ // Parse query strings with https://github.com/ljharb/qs.
72
+ app.set('query parser', 'extended');
73
+
74
+ app.use(express.json());
75
+
76
+ // Must be one of the first handlers (to catch all the errors).
77
+ app.use(finishMiddleware);
78
+
79
+ app.use(correlationIdMiddleware);
80
+ app.use(loggerMiddleware);
81
+ app.use(credentialsMiddleware);
82
+ app.use(selectsMiddleware);
83
+
84
+ // Load handlers as needed.
85
+ if (this.handlers.length) {
86
+ for (const handler of this.handlers) {
87
+ app.use(handler.generate());
88
+ }
89
+ } else {
90
+ printErrorMessage(`
91
+ It seems like you're trying to start the server without any handler. This is probably a mistake as the
92
+ server wouldn't expose any route. To fix this error, add at least one handler before calling the start()
93
+ function.
94
+ `);
95
+ process.exit(1);
96
+ }
97
+
98
+ // Must be the (last - 1) handler.
99
+ app.use(errorsMiddleware);
100
+
101
+ // Must be the last handler.
102
+ app.use(notFoundMiddleware);
103
+
104
+ this.instance = app.listen(process.env.PORT || 9200, () =>
105
+ console.info(`Server started on port ${process.env.PORT || 9200}.`),
106
+ );
107
+
108
+ // Trap exit signals.
109
+ ['SIGTERM', 'SIGINT', 'SIGUSR2'].forEach(signalType => {
110
+ process.once(signalType, async () => {
111
+ console.info(`Received termination signal ${signalType}. Exiting.`);
112
+
113
+ try {
114
+ if (this.instance) {
115
+ this.instance.close();
116
+ }
117
+
118
+ if (this.cache) {
119
+ await this.cache.quit();
120
+ }
121
+ } catch (e) {
122
+ console.error('Failed to gracefully exit', e);
123
+ }
124
+
125
+ process.exit();
126
+ });
127
+ });
128
+ }
129
+ }
@@ -0,0 +1,19 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import * as uuid from 'uuid';
3
+
4
+ declare global {
5
+ // eslint-disable-next-line @typescript-eslint/no-namespace
6
+ namespace Express {
7
+ interface Locals {
8
+ correlationId: string;
9
+ }
10
+ }
11
+ }
12
+
13
+ const middleware = (req: Request, res: Response, next: NextFunction) => {
14
+ res.locals.correlationId = req.header('X-Unito-Correlation-Id') ?? uuid.v4();
15
+
16
+ next();
17
+ };
18
+
19
+ export default middleware;
@@ -0,0 +1,35 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { BadRequestError } from '../httpErrors.js';
3
+
4
+ declare global {
5
+ // eslint-disable-next-line @typescript-eslint/no-namespace
6
+ namespace Express {
7
+ interface Locals {
8
+ credentials: Credentials;
9
+ }
10
+ }
11
+ }
12
+
13
+ export type Credentials = Record<string, unknown>;
14
+
15
+ const CREDENTIALS_HEADER = 'X-Unito-Credentials';
16
+
17
+ const middleware = (req: Request, res: Response, next: NextFunction) => {
18
+ const credentialsHeader = req.header(CREDENTIALS_HEADER);
19
+
20
+ if (credentialsHeader) {
21
+ let credentials: Credentials;
22
+
23
+ try {
24
+ credentials = JSON.parse(Buffer.from(credentialsHeader, 'base64').toString('utf8'));
25
+ } catch {
26
+ throw new BadRequestError(`Malformed HTTP header ${CREDENTIALS_HEADER}`);
27
+ }
28
+
29
+ res.locals.credentials = credentials;
30
+ }
31
+
32
+ next();
33
+ };
34
+
35
+ export default middleware;
@@ -0,0 +1,30 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { Error as APIError } from '@unito/integration-api';
3
+ import { HttpError } from '../httpErrors.js';
4
+
5
+ const middleware = (err: Error, _req: Request, res: Response, next: NextFunction) => {
6
+ if (res.headersSent) {
7
+ return next(err);
8
+ }
9
+
10
+ if (process.env.NODE_ENV !== 'production') {
11
+ res.locals.logger.error(err);
12
+ }
13
+
14
+ if (err instanceof HttpError) {
15
+ return res.status(err.status).json(err.message);
16
+ }
17
+
18
+ const originalError: APIError = {
19
+ code: err.name,
20
+ message: err.message,
21
+ };
22
+
23
+ res.status(500).json({
24
+ code: '500',
25
+ message: 'Oops! Something went wrong',
26
+ originalError,
27
+ } as APIError);
28
+ };
29
+
30
+ export default middleware;