UNPKG

@unito/integration-sdk

Version:

Integration SDK

338 lines (337 loc) 15.1 kB
import { Router } from 'express'; import { InvalidHandler } from './errors.js'; import { HttpError, UnauthorizedError, BadRequestError } from './httpErrors.js'; import busboy from 'busboy'; function assertValidPath(path) { if (!path.startsWith('/')) { throw new InvalidHandler(`The provided path '${path}' is invalid. All paths must start with a '/'.`); } if (path.length > 1 && path.endsWith('/')) { throw new InvalidHandler(`The provided path '${path}' is invalid. Paths must not end with a '/'.`); } } function assertValidConfiguration(path, pathWithIdentifier, handlers) { if (path === pathWithIdentifier) { const individualHandlers = ['getItem', 'updateItem', 'deleteItem']; const collectionHandlers = ['getCollection', 'createItem']; const hasIndividualHandlers = individualHandlers.some(handler => handler in handlers); const hasCollectionHandlers = collectionHandlers.some(handler => handler in handlers); if (hasIndividualHandlers && hasCollectionHandlers) { throw new InvalidHandler(`The provided path '${path}' doesn't differentiate between individual and collection level operation, so you cannot define both.`); } } } function parsePath(path) { const pathParts = path.split('/'); const lastPart = pathParts.at(-1); if (pathParts.length > 1 && lastPart && lastPart.startsWith(':')) { pathParts.pop(); } return { pathWithIdentifier: path, path: pathParts.join('/') }; } function assertCreateItemRequestPayload(body) { if (typeof body !== 'object' || body === null) { throw new BadRequestError('Invalid CreateItemRequestPayload'); } } function assertUpdateItemRequestPayload(body) { if (typeof body !== 'object' || body === null) { throw new BadRequestError('Invalid UpdateItemRequestPayload'); } } function assertWebhookParseRequestPayload(body) { if (typeof body !== 'object' || body === null) { throw new BadRequestError('Invalid WebhookParseRequestPayload'); } if (!('payload' in body) || typeof body.payload !== 'string') { throw new BadRequestError("Missing required 'payload' property in WebhookParseRequestPayload"); } if (!('url' in body) || typeof body.url !== 'string') { throw new BadRequestError("Missing required 'url' property in WebhookParseRequestPayload"); } } function assertWebhookSubscriptionRequestPayload(body) { if (typeof body !== 'object' || body === null) { throw new BadRequestError('Invalid WebhookSubscriptionRequestPayload'); } if (!('itemPath' in body) || typeof body.itemPath !== 'string') { throw new BadRequestError("Missing required 'itemPath' property in WebhookSubscriptionRequestPayload"); } if (!('targetUrl' in body) || typeof body.targetUrl !== 'string') { throw new BadRequestError("Missing required 'targetUrl' property in WebhookSubscriptionRequestPayload"); } if (!('action' in body) || typeof body.action !== 'string') { throw new BadRequestError("Missing required 'action' property in WebhookSubscriptionRequestPayload"); } if (!['start', 'stop'].includes(body.action)) { throw new BadRequestError("Invalid value for 'action' property in WebhookSubscriptionRequestPayload"); } } export class Handler { path; pathWithIdentifier; handlers; constructor(inputPath, handlers) { assertValidPath(inputPath); const { pathWithIdentifier, path } = parsePath(inputPath); assertValidConfiguration(path, pathWithIdentifier, handlers); this.pathWithIdentifier = pathWithIdentifier; this.path = path; this.handlers = handlers; } generate() { const router = Router({ caseSensitive: true }); console.debug(`\x1b[33mMounting handler at path ${this.pathWithIdentifier}`); if (this.handlers.getCollection) { const handler = this.handlers.getCollection; console.debug(` Enabling getCollection at GET ${this.path}`); router.get(this.path, async (req, res) => { if (!res.locals.credentials) { throw new UnauthorizedError(); } const collection = await handler({ credentials: res.locals.credentials, secrets: res.locals.secrets, search: res.locals.search, selects: res.locals.selects, filters: res.locals.filters, logger: res.locals.logger, signal: res.locals.signal, params: req.params, query: req.query, relations: res.locals.relations, }); res.status(200).send(collection); }); } if (this.handlers.createItem) { const handler = this.handlers.createItem; console.debug(` Enabling createItem at POST ${this.path}`); router.post(this.path, async (req, res) => { if (!res.locals.credentials) { throw new UnauthorizedError(); } assertCreateItemRequestPayload(req.body); const createItemSummary = await handler({ credentials: res.locals.credentials, secrets: res.locals.secrets, body: req.body, logger: res.locals.logger, signal: res.locals.signal, params: req.params, query: req.query, }); res.status(201).send(createItemSummary); }); } if (this.handlers.createBlob) { const handler = this.handlers.createBlob; console.debug(` Enabling createBlob at POST ${this.pathWithIdentifier}`); router.post(this.path, async (req, res) => { if (!res.locals.credentials) { throw new UnauthorizedError(); } /** * Some of the integrations, servicenow for example, * will need to add more information to the form data that is being passed to the upload attachment handler. * 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. */ const bb = busboy({ headers: req.headers }); bb.on('file', async (_name, file, info) => { try { const createdBlob = await handler({ credentials: res.locals.credentials, secrets: res.locals.secrets, body: { file: file, mimeType: info.mimeType, encoding: info.encoding, filename: info.filename, }, logger: res.locals.logger, signal: res.locals.signal, params: req.params, query: req.query, }); res.status(201).send(createdBlob); } catch (error) { if (error instanceof HttpError) { res.status(error.status).send(error); } else { res.status(500).send({ message: `Error creating the blob: ${error}` }); } } }); bb.on('error', error => { res.status(500).send({ message: `Error parsing the form data: ${error}` }); }); req.pipe(bb); }); } if (this.handlers.getItem) { const handler = this.handlers.getItem; console.debug(` Enabling getItem at GET ${this.pathWithIdentifier}`); router.get(this.pathWithIdentifier, async (req, res) => { if (!res.locals.credentials) { throw new UnauthorizedError(); } const item = await handler({ credentials: res.locals.credentials, secrets: res.locals.secrets, logger: res.locals.logger, signal: res.locals.signal, params: req.params, query: req.query, }); res.status(200).send(item); }); } if (this.handlers.updateItem) { const handler = this.handlers.updateItem; console.debug(` Enabling updateItem at PATCH ${this.pathWithIdentifier}`); router.patch(this.pathWithIdentifier, async (req, res) => { if (!res.locals.credentials) { throw new UnauthorizedError(); } assertUpdateItemRequestPayload(req.body); const item = await handler({ credentials: res.locals.credentials, secrets: res.locals.secrets, body: req.body, logger: res.locals.logger, signal: res.locals.signal, params: req.params, query: req.query, }); res.status(200).send(item); }); } if (this.handlers.deleteItem) { const handler = this.handlers.deleteItem; console.debug(` Enabling deleteItem at DELETE ${this.pathWithIdentifier}`); router.delete(this.pathWithIdentifier, async (req, res) => { if (!res.locals.credentials) { throw new UnauthorizedError(); } await handler({ credentials: res.locals.credentials, secrets: res.locals.secrets, logger: res.locals.logger, signal: res.locals.signal, params: req.params, query: req.query, }); res.status(204).send(null); }); } if (this.handlers.getBlob) { console.debug(` Enabling getBlob at GET ${this.pathWithIdentifier}`); const handler = this.handlers.getBlob; router.get(this.pathWithIdentifier, async (req, res) => { if (!res.locals.credentials) { throw new UnauthorizedError(); } const blob = await handler({ credentials: res.locals.credentials, secrets: res.locals.secrets, logger: res.locals.logger, signal: res.locals.signal, params: req.params, query: req.query, }); res.writeHead(200, { 'Content-Type': 'application/octet-stream', }); const reader = blob.getReader(); let isDone = false; try { while (!isDone) { const chunk = await reader.read(); isDone = chunk.done; if (chunk.value) { res.write(chunk.value); } } } finally { reader.releaseLock(); } res.end(); }); } if (this.handlers.getCredentialAccount) { const handler = this.handlers.getCredentialAccount; console.debug(` Enabling getCredentialAccount at GET ${this.pathWithIdentifier}`); router.get(this.pathWithIdentifier, async (req, res) => { if (!res.locals.credentials) { throw new UnauthorizedError(); } const credentialAccount = await handler({ credentials: res.locals.credentials, secrets: res.locals.secrets, logger: res.locals.logger, signal: res.locals.signal, params: req.params, query: req.query, }); res.status(200).send(credentialAccount); }); } if (this.handlers.acknowledgeWebhooks) { const handler = this.handlers.acknowledgeWebhooks; console.debug(` Enabling acknowledgeWebhooks at POST ${this.pathWithIdentifier}`); router.post(this.pathWithIdentifier, async (req, res) => { assertWebhookParseRequestPayload(req.body); const response = await handler({ secrets: res.locals.secrets, logger: res.locals.logger, signal: res.locals.signal, params: req.params, query: req.query, body: req.body, }); res.status(200).send(response); }); } if (this.handlers.parseWebhooks) { const handler = this.handlers.parseWebhooks; console.debug(` Enabling parseWebhooks at POST ${this.pathWithIdentifier}`); router.post(this.pathWithIdentifier, async (req, res) => { assertWebhookParseRequestPayload(req.body); const response = await handler({ secrets: res.locals.secrets, logger: res.locals.logger, signal: res.locals.signal, params: req.params, query: req.query, body: req.body, }); res.status(200).send(response); }); } if (this.handlers.updateWebhookSubscriptions) { const handler = this.handlers.updateWebhookSubscriptions; console.debug(` Enabling updateWebhookSubscriptions at PUT ${this.pathWithIdentifier}`); router.put(this.pathWithIdentifier, async (req, res) => { if (!res.locals.credentials) { throw new UnauthorizedError(); } assertWebhookSubscriptionRequestPayload(req.body); const response = await handler({ secrets: res.locals.secrets, credentials: res.locals.credentials, body: req.body, logger: res.locals.logger, signal: res.locals.signal, params: req.params, query: req.query, }); res.status(204).send(response); }); } console.debug(`\x1b[0m`); return router; } }