fume-fhir-converter
Version:
FHIR-Utilized Mapping Engine - Community
254 lines (226 loc) • 7.79 kB
text/typescript
/**
* © Copyright Outburn Ltd. 2022-2024 All Rights Reserved
* Project name: FUME-COMMUNITY
*/
import cors from 'cors';
import type { NextFunction, Request, RequestHandler, Response } from 'express';
import express from 'express';
import type { Server } from 'http';
import config from './config';
import { fumeFhirPackageId } from './constants';
import { getCache, IAppCacheKeys, initCache, InitCacheConfig } from './helpers/cache';
import * as conformance from './helpers/conformance';
import { FhirClient, setFhirClient } from './helpers/fhirServer';
import { transform } from './helpers/jsonataFunctions';
import { getLogger, setLogger } from './helpers/logger';
import { notFound, routes } from './routes';
import defaultConfig from './serverConfig';
import type {
IAppBinding,
ICacheClass,
IConfig,
IFhirClient,
IFumeServer,
ILogger
} from './types';
export class FumeServer<ConfigType extends IConfig> implements IFumeServer<ConfigType> {
private readonly app: express.Application;
private server?: Server;
/**
* FHIR client instance
* Used to communicate with FHIR server
* Can be overriden by calling
*/
private fhirClient?: IFhirClient;
/**
* Cache configuration
* Allows to register custom cache classes and options
*/
private cacheConfig: Partial<Record<IAppCacheKeys, InitCacheConfig>> = {};
private logger = getLogger();
// default app middleware does nothing - just calls next
private appMiddleware: RequestHandler = (req: Request, res: Response, next: NextFunction) => {
next();
};
constructor () {
this.app = express();
this.app.use(express.urlencoded({ extended: true, limit: '400mb' }));
this.app.use(express.json({ limit: '400mb', type: ['application/json', 'application/fhir+json'] }));
this.app.use(express.text({ limit: '400mb', type: ['text/plain', 'application/vnd.outburn.fume', 'x-application/hl7-v2+er7', 'text/csv', 'application/xml'] }));
this.app.use(cors());
}
public async shutDown (): Promise<void> {
if (this.server) {
this.server.close(() => {
console.log('server closed');
process.exit(0);
});
}
}
/**
* Start the server
* Any extensions to the server should be done before calling this method
* i.e. registering alternative logger, cache class, fhir client and express routes.
* @param serverOptions
*/
public async warmUp (serverOptions?: ConfigType | undefined): Promise<void> {
const options = serverOptions ?? defaultConfig;
this.logger.info('FUME initializing...');
config.setServerConfig(options);
const serverConfig: IConfig = config.getServerConfig();
const { SERVER_PORT, FHIR_SERVER_BASE, FHIR_VERSION, FHIR_PACKAGES, SEARCH_BUNDLE_PAGE_SIZE, FHIR_SERVER_TIMEOUT, SERVER_STATELESS } = serverConfig;
this.logger.info(serverConfig);
// initialize caches
initCache(this.cacheConfig);
this.logger.info('Caches initialized');
this.logger.info(`Default FHIR version is set to ${FHIR_VERSION}`);
// translate fhir version to package id
const fhirVersionCorePackageId = config.getFhirCorePackage();
// download package or throw error
if (fhirVersionCorePackageId) {
const loadRes = await conformance.downloadPackages([fhirVersionCorePackageId]);
if (!loadRes) {
throw new Error(`Errors loading package for FHIR version ${FHIR_VERSION}`);
}
} else {
throw new Error(`FHIR version ${FHIR_VERSION} is unsupported/invalid!`);
};
// load packages
const packageList: string[] = [fhirVersionCorePackageId, fumeFhirPackageId].concat(FHIR_PACKAGES.split(','));
await conformance.downloadPackages(packageList);
// load index of all packages found in global fhir cache (on disk)
await conformance.loadFhirPackageIndex();
// if fhir server defined, load mappings and aliases from it
if (SERVER_STATELESS) {
this.logger.info('Running in stateless mode');
} else {
this.logger.info(`Bundle search size: ${SEARCH_BUNDLE_PAGE_SIZE}`);
this.logger.info(`FHIR Server Timeout: ${FHIR_SERVER_TIMEOUT}`);
this.logger.info(`Loading FUME resources from FHIR server ${FHIR_SERVER_BASE} into cache...`);
if (!this.fhirClient) {
this.registerFhirClient(new FhirClient());
}
const recacheResult = await conformance.recacheFromServer();
if (recacheResult) {
this.logger.info('Successfully loaded cache');
}
};
// mount middleware on application level
// all requests will pass through this function
this.app.use(this.appMiddleware);
// mount routes handler
// if the middleware calls next(), the request handling will proceed here
this.app.use('/', routes);
// catch any routes that are not found
// This allows consumers to extend the server with their own routes
// and still have a default 404 handler
this.app.use(notFound);
this.server = this.app.listen(SERVER_PORT);
this.logger.info(`FUME server is running on port ${SERVER_PORT}`);
}
/**
* @returns express application
*/
public getExpressApp () {
return this.app;
}
/**
* Register application level middleware to intercept all requests
* @param middleware
*/
public registerAppMiddleware (middleware: RequestHandler) {
this.appMiddleware = middleware;
this.logger.info('Registered application middleware...');
};
/**
* Register a logger to replace the default logger
* @param logger
*/
public registerLogger (logger: ILogger) {
this.logger = logger;
setLogger(logger);
};
/**
* Register a class to replace the default cache class
* Cache itself is initialized during warm up
* @param CacheClass
* @param options
*/
public registerCacheClass (
CacheClass: ICacheClass,
cacheClassOptions: Record<string, IAppBinding>,
applyToCaches: IAppCacheKeys[]
) {
if (applyToCaches.length > 0) {
applyToCaches.forEach(cacheKey => {
this.cacheConfig[cacheKey] = { cacheClass: CacheClass, cacheClassOptions };
});
} else {
this.logger.warn('No cache keys provided to apply cache class to');
}
};
/**
* Pass a FHIR client instance to be used by the server
* @param fhirClient
*/
public registerFhirClient (fhirClient: IFhirClient) {
this.fhirClient = fhirClient;
setFhirClient(fhirClient);
}
/**
* @returns fhir client
*/
public getFhirClient () {
if (!this.fhirClient) {
throw new Error('FHIR client not registered');
}
return this.fhirClient;
}
/**
*
* @returns cache
*/
public getCache () {
return getCache();
}
/**
*
* @returns config
*/
public getConfig () {
return config.getServerConfig() as ConfigType;
}
/**
* Register additional bindings for `transform` function
* @param key
* @param binding
*/
public registerBinding (key: string, binding: IAppBinding) {
config.setBinding(key, binding);
}
/**
*
* @returns fhir package index
*/
public getFhirPackageIndex () {
return conformance.getFhirPackageIndex();
}
/**
*
* @returns fhir packages for the version set in config
*/
public getFhirPackages () {
const fhirVersionMinor = config.getFhirVersionMinor();
const packages = conformance.getFhirPackageIndex();
return packages[fhirVersionMinor];
}
/**
* Calls transform with any additional bindings passed using `registerBinding`
* @param input
* @param expression
* @returns
*/
public async transform (input: any, expression: string, bindings: Record<string, IAppBinding> = {}) {
return await transform(input, expression, { ...config.getBindings(), ...bindings });
}
}