@unito/integration-sdk
Version:
Integration SDK
163 lines (159 loc) • 7.42 kB
JavaScript
import express from 'express';
import { InvalidHandler } from './errors.js';
import { Handler } from './handler.js';
import correlationIdMiddleware from './middlewares/correlationId.js';
import credentialsMiddleware from './middlewares/credentials.js';
import errorsMiddleware from './middlewares/errors.js';
import filtersMiddleware from './middlewares/filters.js';
import finishMiddleware from './middlewares/finish.js';
import notFoundMiddleware from './middlewares/notFound.js';
import loggerMiddleware from './middlewares/logger.js';
import startMiddleware from './middlewares/start.js';
import secretsMiddleware from './middlewares/secrets.js';
import searchMiddleware from './middlewares/search.js';
import selectsMiddleware from './middlewares/selects.js';
import relationsMiddleware from './middlewares/relations.js';
import signalMiddleware from './middlewares/signal.js';
import healthMiddleware from './middlewares/health.js';
function printErrorMessage(message) {
console.error();
console.error(`\x1b[31m Oops! Something went wrong! \x1b[0m`);
console.error(message);
}
/**
* Main class for the Integration SDK providing an abstraction layer between the Integration's Graph definition
* and the underlying HTTP server.
*
* An `Integration` instance can have multiple handlers configured to handle different routes. Upon receiving a request,
* the Integration will parse the request to extract meaninful information, match the request to the appropriate handler
* method and forward that information in the form a {@link Context} object.
* The Integration also offer standardized error handling and logging to help you build a robust
* and reliable Integration.
*
* See our {@link https://dev.unito.io/docs/ | documentation} for more examples on how to build an integration.
*/
export default class Integration {
handlers;
instance = undefined;
port;
/**
* Creates a new Integration instance with default port set to 9200.
*
* @param options The {@link Options} to configure the Integration instance. Can be used to override the default port.
*/
constructor(options = {}) {
this.port = options.port || (process.env.PORT ? parseInt(process.env.PORT, 10) : 9200);
this.handlers = [];
}
/**
* Adds a group of common handlers to the integration.
*
* Handlers added to the integration can be one of the following:
* - `ItemHandlers`: A group of handlers defining the implementation of the Operations available for a given item.
* - `CredentialAccountHandlers`: A handler returning the CredentialAccount linked to the caller's credentials.
* - `ParseWebhookHandlers`: A handler parsing the content of an incoming webhook.
* - `WebhookSubscriptionHandlers`: A handler subscribing or unsubscribing to a particular webhook.
* - `AcknowledgeWebhookHandlers`: A handler acknowledging the reception of a webhook.
*
* To accomodate the fact that ItemHandlers may specify multiple operations, some at the collection level, some at the
* item level, we need a way to define the route for each of these operations.
* To achieve this, we assume that if the last part of the path is a variable, then it is the item identifier.
*
* @example The following path: `/trainer/:trainerId/pokemons/:pokemonId` will lead to the following
* routes:
* - getCollection will be called for `GET /trainer/:trainerId/pokemons/` requests
* - getItem will be called for `GET /trainer/:trainerId/pokemons/:pokemonId` requests
* - createItem will be called for `POST /trainer/:trainerId/pokemons/` requests
* - updateItem will be called for `PATCH /trainer/:trainerId/pokemons/:pokemonId` requests
* - deleteItem will be called for `DELETE /trainer/:trainerId/pokemons/:pokemonId` requests
*
* @param path The path to be used as Route for the handlers.
* @param handlers The Handlers definition.
*/
addHandler(path, handlers) {
if (this.instance) {
printErrorMessage(`
It seems like you're trying to add a handler after the server has already started. This is probably
a mistake as calling the start() function essentially starts the server and ignore any further change.
To fix this error, move all your addHandler() calls before the start() function.
`);
process.exit(1);
}
try {
this.handlers.push(new Handler(path, handlers));
}
catch (error) {
if (error instanceof InvalidHandler) {
printErrorMessage(`
It seems like you're trying to add an invalid handler. The exact error message is:
> ${error.message}
You must address this issue before trying again.
`);
}
else {
printErrorMessage(`
An unexpected error happened as we were trying to add your handler. The exact error message is;
> ${error.message}
`);
}
process.exit(1);
}
}
/**
* Starts the server and listens on the specified port (default to 9200).
*
* @remarks
* This function should be called after all the handlers have been added to the integration
* and any other configuration is completed.
*/
start() {
// Express Server initialization
const app = express();
// Parse query strings with https://github.com/ljharb/qs.
app.set('query parser', 'simple');
app.use(express.json());
// Must be one of the first handlers (to catch all the errors).
app.use(finishMiddleware);
// Instantiate internal middlewares.
app.use(startMiddleware);
app.use(correlationIdMiddleware);
app.use(loggerMiddleware);
// Making sure we log all incoming requests (except to '/health'), prior any processing.
app.use((req, res, next) => {
if (req.originalUrl !== '/health') {
res.locals.logger.info(`Initializing request for ${req.originalUrl}`);
}
next();
});
// Instantiate application middlewares. These can throw, so they have an implicit dependency on the internal
// middlewares such as the logger, the correlationId, and the error handling.
app.get('/health', healthMiddleware);
app.use(credentialsMiddleware);
app.use(secretsMiddleware);
app.use(filtersMiddleware);
app.use(searchMiddleware);
app.use(selectsMiddleware);
app.use(relationsMiddleware);
app.use(signalMiddleware);
// Load handlers as needed.
if (this.handlers.length) {
for (const handler of this.handlers) {
app.use(handler.generate());
}
}
else {
printErrorMessage(`
It seems like you're trying to start the server without any handler. This is probably a mistake as the
server wouldn't expose any route. To fix this error, add at least one handler before calling the start()
function.
`);
process.exit(1);
}
// Must be the (last - 1) handler.
app.use(errorsMiddleware);
// Must be the last handler.
app.use(notFoundMiddleware);
// Start the server.
this.instance = app.listen(this.port, () => console.info(`Server started on port ${this.port}.`));
}
}