reshuffle
Version:
Reshuffle is a fast, unopinionated, minimalist integration framework
229 lines (191 loc) • 6.93 kB
text/typescript
import express, { Express, Request, Response, NextFunction, RequestHandler } from 'express'
import { nanoid } from 'nanoid'
import * as availableConnectors from './connectors'
import { MemoryStoreAdapter, PersistentStoreAdapter } from './persistency'
import { BaseConnector, BaseHttpConnector, EventConfiguration } from 'reshuffle-base-connector'
import { createLogger } from './Logger'
import { Logger, LoggerOptions } from 'winston'
import http from 'http'
import { promises as fs } from 'fs'
export interface Handler {
handle: (event: any, app: Reshuffle) => void
id: string
}
export default class Reshuffle {
availableConnectors: any
httpDelegates: { [path: string]: any }
port: number
registry: {
connectors: { [url: string]: BaseConnector }
handlers: { [id: string]: Handler[] }
common: { webserver?: Express; persistentStore?: any }
}
httpServer?: http.Server
logger: Logger
middleware: RequestHandler[] = [express.json(), express.urlencoded({ extended: true })]
constructor(loggerOptions?: LoggerOptions) {
this.availableConnectors = availableConnectors
this.port = parseInt(<string>process.env.PORT, 10) || 8000
this.httpDelegates = {}
this.registry = { connectors: {}, handlers: {}, common: {} }
this.logger = createLogger(loggerOptions)
this.logger.info('Reshuffle Initializing')
}
addMiddleware(middleware: RequestHandler) {
this.middleware.push(middleware)
}
private prepareWebServer(): Express {
if (!this.registry.common.webserver) {
this.registry.common.webserver = express()
this.registry.common.webserver.use(this.middleware)
}
return this.registry.common.webserver
}
register(connector: BaseConnector): Reshuffle {
connector.app = this
this.registry.connectors[connector.id] = connector
return this
}
async unregister(connector: BaseConnector): Promise<void> {
await connector.stop()
delete this.registry.connectors[connector.id]
}
getConnector(connectorId: BaseConnector['id']): BaseConnector {
if (!this.registry.connectors[connectorId]) {
this.logger.error(`Could not find connector [id=${connectorId}]`)
}
return this.registry.connectors[connectorId]
}
registerHTTPDelegate(path: string, delegate: BaseHttpConnector): Reshuffle {
this.httpDelegates[path] = this.httpDelegates[path] || new HttpMultiplexer(path)
this.httpDelegates[path].delegates.push(delegate)
return this
}
//we might to add a fine tuned method in the future that just removes one delegete
unregisterHTTPDelegate(path: string): void {
const httpMultiplexer = this.httpDelegates[path]
if (httpMultiplexer) {
httpMultiplexer.delegates = []
}
}
when(eventConfiguration: EventConfiguration, handler: (() => void) | Handler): Reshuffle {
const handlerWrapper =
typeof handler === 'object'
? handler
: {
handle: handler,
id: nanoid(),
}
if (this.registry.handlers[eventConfiguration.id]) {
this.registry.handlers[eventConfiguration.id].push(handlerWrapper)
} else {
this.registry.handlers[eventConfiguration.id] = [handlerWrapper]
}
this.logger.info('Reshuffle Registering event', eventConfiguration.id)
return this
}
start(port?: number, callback?: () => void): void {
this.port = port || this.port
// Start all connectors
Object.values(this.registry.connectors).forEach((connector) => connector.start())
// Start the webserver if we have http delegates
if (Object.keys(this.httpDelegates).length) {
const webserver = this.prepareWebServer()
if (process.env.RESHUFFLE_HEALTH_PATH) {
webserver.use(process.env.RESHUFFLE_HEALTH_PATH, (req, res) =>
res.status(200).send({ ok: true, uptime: process.uptime() }),
)
}
const specificPaths = Object.keys(this.httpDelegates).filter((p) => !p.includes(':'))
const genericPathsOrdered = Object.keys(this.httpDelegates)
.filter((p) => p.includes(':'))
.sort()
.reverse()
// Moves all generic routes (containing :) at the end, with /specific/generic first (e.g. /foo/:id before /:bar)
specificPaths.concat(genericPathsOrdered).forEach((path) => {
const httpMultiplexer = this.httpDelegates[path]
webserver.all(path, httpMultiplexer.handle.bind(httpMultiplexer))
})
webserver.all('/webhooks/*', (req, res) => {
const errorMessage = `Webhook not registered`
this.logger.info(`${errorMessage} for ${req.method} ${req.url}`)
return res.status(501).send(errorMessage)
})
webserver.all('*', (req, res) => {
const errorMessage = `No handler registered for ${req.method} ${req.url}`
this.logger.info(errorMessage)
return res.status(501).send(errorMessage)
})
this.httpServer = webserver.listen(this.port, () => {
this.logger.info('Reshuffle Web server listening on port', this.port)
})
}
callback && callback()
}
stopWebServer(): void {
this.httpServer?.close()
}
restart(port?: number): void {
this.stopWebServer()
this.start(port, () => {
this.logger.info('Reshuffle Restarted')
})
}
async handleEvent(eventId: EventConfiguration['id'], event: any): Promise<boolean> {
const eventHandlers = this.registry.handlers[eventId]
if (!eventHandlers || eventHandlers.length === 0) {
return false
}
let handled = true
for (const handler of eventHandlers) {
handled &&= await this.onHandleEvent(handler, event)
}
return handled
}
async onHandleEvent(handler: Handler, event: any): Promise<boolean> {
this.logger.defaultMeta = { handlerId: handler.id }
try {
await handler.handle(event, this)
return true
} catch (error) {
this.logger.error(error.stack)
return false
} finally {
this.logger.defaultMeta = {}
}
}
setPersistentStore(adapter: PersistentStoreAdapter) {
this.registry.common.persistentStore = adapter
return adapter
}
getPersistentStore() {
return this.registry.common.persistentStore || this.setPersistentStore(new MemoryStoreAdapter())
}
getLogger(): Logger {
return this.logger
}
}
export { Reshuffle }
class HttpMultiplexer {
delegates: BaseHttpConnector[]
originalPath: string
constructor(originalPath: string) {
this.originalPath = originalPath
this.delegates = []
}
async handle(req: Request & { originalPath: string }, res: Response, next: NextFunction) {
req.originalPath = this.originalPath
let handled = false
if (this.delegates.length > 0) {
for (const delegate of this.delegates) {
if (handled) {
break
}
handled = await delegate.handle(req, res, next)
}
}
if (!handled) {
next()
}
}
}