2020-06-27 17:11:31 +02:00
|
|
|
import config from "config";
|
2021-05-03 19:29:22 +02:00
|
|
|
import express, {NextFunction, Request, Response, Router} from 'express';
|
2020-07-15 15:13:40 +02:00
|
|
|
import * as fs from "fs";
|
2021-05-03 19:29:22 +02:00
|
|
|
import nunjucks from "nunjucks";
|
2020-07-15 15:22:04 +02:00
|
|
|
import * as path from "path";
|
2021-05-03 19:29:22 +02:00
|
|
|
|
|
|
|
import ApplicationComponent from "./ApplicationComponent.js";
|
|
|
|
import CacheProvider from "./CacheProvider.js";
|
2021-06-01 14:34:04 +02:00
|
|
|
import {route, setPublicUrl} from "./common/Routing.js";
|
2021-05-03 19:29:22 +02:00
|
|
|
import FrontendToolsComponent from "./components/FrontendToolsComponent.js";
|
|
|
|
import LogRequestsComponent from "./components/LogRequestsComponent.js";
|
|
|
|
import RedisComponent from "./components/RedisComponent.js";
|
|
|
|
import Controller from "./Controller.js";
|
|
|
|
import Migration, {MigrationType} from "./db/Migration.js";
|
|
|
|
import MysqlConnectionManager from "./db/MysqlConnectionManager.js";
|
|
|
|
import {ValidationBag, ValidationError} from "./db/Validator.js";
|
2021-05-13 15:58:41 +02:00
|
|
|
import Extendable, {MissingComponentError} from "./Extendable.js";
|
2021-05-03 19:29:22 +02:00
|
|
|
import {BadRequestError, HttpError, NotFoundHttpError, ServerError, ServiceUnavailableHttpError} from "./HttpError.js";
|
|
|
|
import {logger, loggingContextMiddleware} from "./Logger.js";
|
|
|
|
import SecurityError from "./SecurityError.js";
|
2021-05-12 14:32:01 +02:00
|
|
|
import {doesFileExist, Type} from "./Utils.js";
|
2021-05-03 19:29:22 +02:00
|
|
|
import WebSocketListener from "./WebSocketListener.js";
|
|
|
|
import TemplateError = nunjucks.lib.TemplateError;
|
2020-04-22 15:52:17 +02:00
|
|
|
|
2020-09-25 23:42:15 +02:00
|
|
|
export default abstract class Application implements Extendable<ApplicationComponent | WebSocketListener<Application>> {
|
2020-04-22 15:52:17 +02:00
|
|
|
private readonly version: string;
|
2021-03-24 16:08:50 +01:00
|
|
|
private coreVersion: string = 'unknown';
|
2020-07-08 09:52:08 +02:00
|
|
|
private readonly ignoreCommandLine: boolean;
|
2020-04-22 15:52:17 +02:00
|
|
|
private readonly controllers: Controller[] = [];
|
2020-09-25 22:03:22 +02:00
|
|
|
private readonly webSocketListeners: { [p: string]: WebSocketListener<Application> } = {};
|
|
|
|
private readonly components: ApplicationComponent[] = [];
|
2020-07-19 11:57:47 +02:00
|
|
|
private cacheProvider?: CacheProvider;
|
2020-04-22 15:52:17 +02:00
|
|
|
|
|
|
|
private ready: boolean = false;
|
2021-03-26 10:32:02 +01:00
|
|
|
private started: boolean = false;
|
|
|
|
private busy: boolean = false;
|
2020-04-22 15:52:17 +02:00
|
|
|
|
2020-07-08 09:52:08 +02:00
|
|
|
protected constructor(version: string, ignoreCommandLine: boolean = false) {
|
2020-04-22 15:52:17 +02:00
|
|
|
this.version = version;
|
2020-07-08 09:52:08 +02:00
|
|
|
this.ignoreCommandLine = ignoreCommandLine;
|
2021-06-01 14:34:04 +02:00
|
|
|
|
|
|
|
setPublicUrl(config.get<string>('app.public_url'));
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
|
2020-09-25 23:42:15 +02:00
|
|
|
protected abstract getMigrations(): MigrationType<Migration>[];
|
2020-04-23 15:43:15 +02:00
|
|
|
|
2020-12-04 14:42:09 +01:00
|
|
|
protected abstract init(): Promise<void>;
|
2020-04-22 15:52:17 +02:00
|
|
|
|
2020-09-25 23:42:15 +02:00
|
|
|
protected use(thing: Controller | WebSocketListener<this> | ApplicationComponent): void {
|
2020-04-22 15:52:17 +02:00
|
|
|
if (thing instanceof Controller) {
|
2020-09-25 22:03:22 +02:00
|
|
|
thing.setApp(this);
|
2020-04-22 15:52:17 +02:00
|
|
|
this.controllers.push(thing);
|
|
|
|
} else if (thing instanceof WebSocketListener) {
|
|
|
|
const path = thing.path();
|
|
|
|
this.webSocketListeners[path] = thing;
|
2020-07-19 11:57:47 +02:00
|
|
|
thing.init(this);
|
2021-01-22 15:54:26 +01:00
|
|
|
logger.info(`Added websocket listener on ${path}`);
|
2020-04-22 15:52:17 +02:00
|
|
|
} else {
|
2020-04-25 09:34:02 +02:00
|
|
|
thing.setApp(this);
|
2020-04-22 15:52:17 +02:00
|
|
|
this.components.push(thing);
|
2020-07-19 11:57:47 +02:00
|
|
|
|
|
|
|
if (thing instanceof RedisComponent) {
|
|
|
|
this.cacheProvider = thing;
|
|
|
|
}
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public async start(): Promise<void> {
|
2021-03-26 10:32:02 +01:00
|
|
|
if (this.started) throw new Error('Application already started');
|
|
|
|
if (this.busy) throw new Error('Application busy');
|
|
|
|
this.busy = true;
|
|
|
|
|
2021-03-24 16:08:50 +01:00
|
|
|
// Load core version
|
2021-05-12 14:32:01 +02:00
|
|
|
const file = await this.isInNodeModules() ?
|
2021-05-03 19:29:22 +02:00
|
|
|
'node_modules/swaf/package.json' :
|
|
|
|
'package.json';
|
2021-03-24 16:08:50 +01:00
|
|
|
|
|
|
|
try {
|
|
|
|
this.coreVersion = JSON.parse(fs.readFileSync(file).toString()).version;
|
|
|
|
} catch (e) {
|
|
|
|
logger.warn('Couldn\'t determine coreVersion.', e);
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.info(`${config.get('app.name')} v${this.version} | swaf v${this.coreVersion}`);
|
|
|
|
|
|
|
|
// Catch interrupt signals
|
2021-03-26 10:32:02 +01:00
|
|
|
const exitHandler = () => {
|
2020-04-22 15:52:17 +02:00
|
|
|
this.stop().catch(console.error);
|
2021-03-26 10:32:02 +01:00
|
|
|
};
|
|
|
|
process.once('exit', exitHandler);
|
|
|
|
process.once('SIGINT', exitHandler);
|
|
|
|
process.once('SIGUSR1', exitHandler);
|
|
|
|
process.once('SIGUSR2', exitHandler);
|
2021-04-21 13:52:52 +02:00
|
|
|
process.once('SIGTERM', exitHandler);
|
2021-03-26 10:32:02 +01:00
|
|
|
process.once('uncaughtException', exitHandler);
|
2020-04-22 15:52:17 +02:00
|
|
|
|
2020-04-23 15:43:15 +02:00
|
|
|
// Register migrations
|
|
|
|
MysqlConnectionManager.registerMigrations(this.getMigrations());
|
|
|
|
|
2021-05-13 16:03:59 +02:00
|
|
|
// Register and initialize all components and alike
|
2021-03-26 10:32:02 +01:00
|
|
|
await this.init();
|
2021-05-13 16:03:59 +02:00
|
|
|
for (const component of this.components) {
|
|
|
|
await component.init?.();
|
|
|
|
}
|
2021-03-26 10:32:02 +01:00
|
|
|
|
2020-06-05 14:32:39 +02:00
|
|
|
// Process command line
|
2021-05-04 17:17:34 +02:00
|
|
|
if (!this.ignoreCommandLine) {
|
|
|
|
let result: boolean;
|
|
|
|
try {
|
|
|
|
result = await this.processCommandLine();
|
|
|
|
} catch (err) {
|
|
|
|
logger.error(err);
|
|
|
|
process.exit(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (result) {
|
|
|
|
this.started = true;
|
|
|
|
this.busy = false;
|
|
|
|
return;
|
|
|
|
}
|
2020-06-05 14:32:39 +02:00
|
|
|
}
|
|
|
|
|
2020-07-15 15:06:13 +02:00
|
|
|
// Security
|
|
|
|
if (process.env.NODE_ENV === 'production') {
|
|
|
|
await this.checkSecuritySettings();
|
|
|
|
}
|
|
|
|
|
2020-04-22 15:52:17 +02:00
|
|
|
// Init express
|
|
|
|
const app = express();
|
2021-01-22 15:54:26 +01:00
|
|
|
|
|
|
|
// Logging context
|
|
|
|
app.use(loggingContextMiddleware);
|
|
|
|
|
|
|
|
// Routers
|
2020-07-11 11:46:16 +02:00
|
|
|
const initRouter = express.Router();
|
|
|
|
const handleRouter = express.Router();
|
|
|
|
app.use(initRouter);
|
|
|
|
app.use(handleRouter);
|
2020-04-22 15:52:17 +02:00
|
|
|
|
2020-07-11 11:46:16 +02:00
|
|
|
// Error handlers
|
2020-09-25 23:42:15 +02:00
|
|
|
app.use((err: unknown, req: Request, res: Response, next: NextFunction) => {
|
2020-07-11 11:46:16 +02:00
|
|
|
if (res.headersSent) return next(err);
|
2020-04-22 15:52:17 +02:00
|
|
|
|
2020-11-14 16:24:00 +01:00
|
|
|
// Transform single validation errors into a validation bag for convenience
|
|
|
|
if (err instanceof ValidationError) {
|
|
|
|
const bag = new ValidationBag();
|
|
|
|
bag.addMessage(err);
|
|
|
|
err = bag;
|
|
|
|
}
|
|
|
|
|
2020-06-14 21:48:50 +02:00
|
|
|
if (err instanceof ValidationBag) {
|
2020-11-14 16:24:00 +01:00
|
|
|
const bag = err;
|
2020-06-15 12:58:15 +02:00
|
|
|
res.format({
|
|
|
|
json: () => {
|
2020-11-15 14:12:45 +01:00
|
|
|
res.status(400);
|
2020-06-15 12:58:15 +02:00
|
|
|
res.json({
|
|
|
|
status: 'error',
|
2020-11-15 14:12:45 +01:00
|
|
|
code: 400,
|
2020-06-15 12:58:15 +02:00
|
|
|
message: 'Invalid form data',
|
2020-11-14 16:24:00 +01:00
|
|
|
messages: bag.getMessages(),
|
2020-06-15 12:58:15 +02:00
|
|
|
});
|
|
|
|
},
|
|
|
|
text: () => {
|
2020-11-15 14:12:45 +01:00
|
|
|
res.status(400);
|
2020-11-14 16:24:00 +01:00
|
|
|
res.send('Error: ' + bag.getMessages());
|
2020-06-15 12:58:15 +02:00
|
|
|
},
|
|
|
|
html: () => {
|
2020-11-14 16:24:00 +01:00
|
|
|
req.flash('validation', bag.getMessages());
|
2021-06-01 14:34:04 +02:00
|
|
|
res.redirect(req.getPreviousUrl() || route('home'));
|
2020-06-15 12:58:15 +02:00
|
|
|
},
|
|
|
|
});
|
2020-06-14 21:48:50 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-11-02 17:48:52 +01:00
|
|
|
const errorId = LogRequestsComponent.logRequest(req, res, err, '500 Internal Error',
|
2020-09-25 23:42:15 +02:00
|
|
|
err instanceof BadRequestError || err instanceof ServiceUnavailableHttpError);
|
2020-04-22 15:52:17 +02:00
|
|
|
|
|
|
|
let httpError: HttpError;
|
|
|
|
|
|
|
|
if (err instanceof HttpError) {
|
|
|
|
httpError = err;
|
|
|
|
} else if (err instanceof TemplateError && err.cause instanceof HttpError) {
|
|
|
|
httpError = err.cause;
|
|
|
|
} else {
|
2020-09-25 23:42:15 +02:00
|
|
|
httpError = new ServerError('Internal server error.', err instanceof Error ? err : undefined);
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
res.status(httpError.errorCode);
|
|
|
|
res.format({
|
|
|
|
html: () => {
|
2021-05-13 15:55:29 +02:00
|
|
|
const locals = {
|
2020-04-22 15:52:17 +02:00
|
|
|
error_code: httpError.errorCode,
|
|
|
|
error_message: httpError.message,
|
|
|
|
error_instructions: httpError.instructions,
|
2020-09-25 23:42:15 +02:00
|
|
|
error_id: errorId,
|
2021-05-13 15:55:29 +02:00
|
|
|
};
|
|
|
|
res.render('errors/' + httpError.errorCode, locals, (err: Error | undefined, html) => {
|
|
|
|
if (err) {
|
|
|
|
res.render('errors/Error', locals);
|
|
|
|
} else {
|
|
|
|
res.send(html);
|
|
|
|
}
|
2020-04-22 15:52:17 +02:00
|
|
|
});
|
|
|
|
},
|
|
|
|
json: () => {
|
|
|
|
res.json({
|
|
|
|
status: 'error',
|
|
|
|
code: httpError.errorCode,
|
|
|
|
message: httpError.message,
|
|
|
|
instructions: httpError.instructions,
|
2020-09-25 23:42:15 +02:00
|
|
|
error_id: errorId,
|
2020-04-22 15:52:17 +02:00
|
|
|
});
|
|
|
|
},
|
|
|
|
default: () => {
|
2020-09-25 23:42:15 +02:00
|
|
|
res.type('txt').send(`${httpError.errorCode} - ${httpError.message}\n\n${httpError.instructions}\n\nError ID: ${errorId}`);
|
|
|
|
},
|
2020-04-22 15:52:17 +02:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2020-07-11 11:46:16 +02:00
|
|
|
// Components routes
|
2020-04-22 15:52:17 +02:00
|
|
|
for (const component of this.components) {
|
2021-05-13 16:03:59 +02:00
|
|
|
if (component.initRoutes) {
|
2020-09-25 22:03:22 +02:00
|
|
|
component.setCurrentRouter(initRouter);
|
2021-05-13 16:03:59 +02:00
|
|
|
await component.initRoutes(initRouter);
|
2020-09-25 22:03:22 +02:00
|
|
|
}
|
|
|
|
|
2021-05-13 16:03:59 +02:00
|
|
|
if (component.handleRoutes) {
|
2020-09-25 22:03:22 +02:00
|
|
|
component.setCurrentRouter(handleRouter);
|
2021-05-13 16:03:59 +02:00
|
|
|
await component.handleRoutes(handleRouter);
|
2020-09-25 22:03:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
component.setCurrentRouter(null);
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
|
2021-05-13 16:26:27 +02:00
|
|
|
// Start components
|
|
|
|
for (const component of this.components) {
|
|
|
|
await component.start?.(app);
|
|
|
|
}
|
|
|
|
|
2020-04-22 15:52:17 +02:00
|
|
|
// Routes
|
2020-07-11 11:46:16 +02:00
|
|
|
this.routes(initRouter, handleRouter);
|
2020-04-22 15:52:17 +02:00
|
|
|
|
|
|
|
this.ready = true;
|
2021-03-26 10:32:02 +01:00
|
|
|
this.started = true;
|
|
|
|
this.busy = false;
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
|
2020-06-05 14:32:39 +02:00
|
|
|
protected async processCommandLine(): Promise<boolean> {
|
|
|
|
const args = process.argv;
|
2021-03-26 10:32:02 +01:00
|
|
|
// Flags
|
|
|
|
const flags = {
|
|
|
|
verbose: false,
|
|
|
|
fullHttpRequests: false,
|
2021-03-26 10:56:32 +01:00
|
|
|
watch: false,
|
2021-03-26 10:32:02 +01:00
|
|
|
};
|
|
|
|
let mainCommand: string | null = null;
|
|
|
|
const mainCommandArgs: string[] = [];
|
2020-06-05 14:32:39 +02:00
|
|
|
for (let i = 2; i < args.length; i++) {
|
|
|
|
switch (args[i]) {
|
|
|
|
case '--verbose':
|
2021-03-26 10:32:02 +01:00
|
|
|
flags.verbose = true;
|
2020-06-05 14:32:39 +02:00
|
|
|
break;
|
2020-06-14 11:43:00 +02:00
|
|
|
case '--full-http-requests':
|
2021-03-26 10:32:02 +01:00
|
|
|
flags.fullHttpRequests = true;
|
2020-06-14 11:43:00 +02:00
|
|
|
break;
|
2021-03-26 10:56:32 +01:00
|
|
|
case '--watch':
|
|
|
|
flags.watch = true;
|
|
|
|
break;
|
2020-06-05 14:32:39 +02:00
|
|
|
case 'migration':
|
2021-03-26 10:56:32 +01:00
|
|
|
case 'pre-compile-views':
|
2021-03-26 10:32:02 +01:00
|
|
|
if (mainCommand === null) mainCommand = args[i];
|
|
|
|
else throw new Error(`Only one main command can be used at once (${mainCommand},${args[i]})`);
|
|
|
|
break;
|
2020-06-05 14:32:39 +02:00
|
|
|
default:
|
2021-03-26 10:32:02 +01:00
|
|
|
if (mainCommand) mainCommandArgs.push(args[i]);
|
|
|
|
else logger.fatal('Unrecognized argument', args[i]);
|
2020-06-05 14:32:39 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
2021-03-26 10:32:02 +01:00
|
|
|
|
|
|
|
if (flags.verbose) logger.setSettings({minLevel: "trace"});
|
|
|
|
if (flags.fullHttpRequests) LogRequestsComponent.logFullHttpRequests();
|
|
|
|
|
|
|
|
if (mainCommand) {
|
|
|
|
switch (mainCommand) {
|
|
|
|
case 'migration':
|
|
|
|
await MysqlConnectionManager.migrationCommand(mainCommandArgs);
|
|
|
|
await this.stop();
|
|
|
|
break;
|
2021-04-28 14:53:46 +02:00
|
|
|
case 'pre-compile-views': {
|
|
|
|
// Prepare migrations
|
|
|
|
for (const migration of this.getMigrations()) {
|
|
|
|
new migration().registerModels?.();
|
|
|
|
}
|
|
|
|
// Prepare routes
|
|
|
|
for (const controller of this.controllers) {
|
|
|
|
controller.setupRoutes();
|
|
|
|
}
|
|
|
|
|
|
|
|
const frontendToolsComponent = this.as(FrontendToolsComponent);
|
|
|
|
await frontendToolsComponent.preCompileViews(flags.watch);
|
2021-03-26 10:56:32 +01:00
|
|
|
break;
|
2021-04-28 14:53:46 +02:00
|
|
|
}
|
2021-03-26 10:32:02 +01:00
|
|
|
default:
|
|
|
|
logger.fatal('Unimplemented main command', mainCommand);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-06-05 14:32:39 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-07-15 15:06:13 +02:00
|
|
|
private async checkSecuritySettings(): Promise<void> {
|
2020-07-15 15:13:40 +02:00
|
|
|
// Check config file permissions
|
2020-07-15 15:22:04 +02:00
|
|
|
const configDir = 'config';
|
|
|
|
for (const file of fs.readdirSync(configDir)) {
|
|
|
|
const fullPath = path.resolve(configDir, file);
|
|
|
|
const stats = fs.lstatSync(fullPath);
|
2020-07-15 15:13:40 +02:00
|
|
|
if (stats.uid !== process.getuid())
|
2020-07-15 15:39:08 +02:00
|
|
|
throw new SecurityError(`${fullPath} is not owned by this process (${process.getuid()}).`);
|
|
|
|
|
|
|
|
const mode = (stats.mode & parseInt('777', 8)).toString(8);
|
|
|
|
if (mode !== '400')
|
|
|
|
throw new SecurityError(`${fullPath} is ${mode}; should be 400.`);
|
2020-07-15 15:13:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Check security fields
|
2020-07-15 15:06:13 +02:00
|
|
|
for (const component of this.components) {
|
2020-09-25 22:03:22 +02:00
|
|
|
await component.checkSecuritySettings?.();
|
2020-07-15 15:06:13 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-23 08:46:21 +02:00
|
|
|
public async stop(): Promise<void> {
|
2021-03-26 10:32:02 +01:00
|
|
|
if (this.started && !this.busy) {
|
|
|
|
this.busy = true;
|
|
|
|
logger.info('Stopping application...');
|
2020-04-22 15:52:17 +02:00
|
|
|
|
2021-03-26 10:32:02 +01:00
|
|
|
for (const component of this.components) {
|
|
|
|
await component.stop?.();
|
|
|
|
}
|
2020-04-22 15:52:17 +02:00
|
|
|
|
2021-03-26 10:32:02 +01:00
|
|
|
logger.info(`${this.constructor.name} stopped properly.`);
|
|
|
|
this.started = false;
|
|
|
|
this.busy = false;
|
|
|
|
}
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
|
2020-07-11 11:46:16 +02:00
|
|
|
private routes(initRouter: Router, handleRouter: Router) {
|
2020-04-22 15:52:17 +02:00
|
|
|
for (const controller of this.controllers) {
|
2020-09-25 22:03:22 +02:00
|
|
|
if (controller.hasGlobalMiddlewares()) {
|
2020-07-11 11:46:16 +02:00
|
|
|
controller.setupGlobalHandlers(handleRouter);
|
2020-04-22 15:52:17 +02:00
|
|
|
|
2021-01-22 15:54:26 +01:00
|
|
|
logger.info(`Registered global middlewares for controller ${controller.constructor.name}`);
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const controller of this.controllers) {
|
2020-07-11 11:08:57 +02:00
|
|
|
const {mainRouter, fileUploadFormRouter} = controller.setupRoutes();
|
2020-07-11 11:46:16 +02:00
|
|
|
initRouter.use(controller.getRoutesPrefix(), fileUploadFormRouter);
|
|
|
|
handleRouter.use(controller.getRoutesPrefix(), mainRouter);
|
2020-04-22 15:52:17 +02:00
|
|
|
|
2021-01-22 15:54:26 +01:00
|
|
|
logger.info(`> Registered routes for controller ${controller.constructor.name} at ${controller.getRoutesPrefix()}`);
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
|
2020-07-11 11:46:16 +02:00
|
|
|
handleRouter.use((req: Request) => {
|
2020-04-22 15:52:17 +02:00
|
|
|
throw new NotFoundHttpError('page', req.originalUrl);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-09-25 22:03:22 +02:00
|
|
|
public getWebSocketListeners(): { [p: string]: WebSocketListener<Application> } {
|
2020-07-19 11:57:47 +02:00
|
|
|
return this.webSocketListeners;
|
|
|
|
}
|
|
|
|
|
2020-09-25 23:42:15 +02:00
|
|
|
public getCache(): CacheProvider | null {
|
|
|
|
return this.cacheProvider || null;
|
2020-07-19 11:57:47 +02:00
|
|
|
}
|
2020-09-23 08:46:21 +02:00
|
|
|
|
2021-05-13 16:26:27 +02:00
|
|
|
public getComponents(): ApplicationComponent[] {
|
|
|
|
return [...this.components];
|
|
|
|
}
|
|
|
|
|
2020-09-25 23:42:15 +02:00
|
|
|
public as<C extends ApplicationComponent | WebSocketListener<Application>>(type: Type<C>): C {
|
|
|
|
const module = this.components.find(component => component.constructor === type) ||
|
|
|
|
Object.values(this.webSocketListeners).find(listener => listener.constructor === type);
|
|
|
|
if (!module) throw new Error(`This app doesn't have a ${type.name} component.`);
|
|
|
|
return module as C;
|
2020-09-25 22:03:22 +02:00
|
|
|
}
|
|
|
|
|
2020-09-25 23:42:15 +02:00
|
|
|
public asOptional<C extends ApplicationComponent | WebSocketListener<Application>>(type: Type<C>): C | null {
|
|
|
|
const module = this.components.find(component => component.constructor === type) ||
|
|
|
|
Object.values(this.webSocketListeners).find(listener => listener.constructor === type);
|
|
|
|
return module ? module as C : null;
|
2020-09-23 08:46:21 +02:00
|
|
|
}
|
2021-03-24 16:08:50 +01:00
|
|
|
|
2021-05-13 15:58:41 +02:00
|
|
|
public has<C extends ApplicationComponent | WebSocketListener<Application>>(type: Type<C>): boolean {
|
|
|
|
return !!this.asOptional(type);
|
|
|
|
}
|
|
|
|
|
|
|
|
public require<C extends ApplicationComponent | WebSocketListener<Application>>(type: Type<C>): void {
|
|
|
|
if (!this.has(type)) {
|
|
|
|
throw new MissingComponentError(type);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-12 14:32:01 +02:00
|
|
|
public async isInNodeModules(): Promise<boolean> {
|
2021-05-13 14:13:45 +02:00
|
|
|
return await doesFileExist('node_modules/swaf');
|
2021-03-24 16:08:50 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public isReady(): boolean {
|
|
|
|
return this.ready;
|
|
|
|
}
|
|
|
|
|
|
|
|
public getVersion(): string {
|
|
|
|
return this.version;
|
|
|
|
}
|
|
|
|
|
|
|
|
public getCoreVersion(): string {
|
|
|
|
return this.coreVersion;
|
|
|
|
}
|
2020-09-25 22:03:22 +02:00
|
|
|
}
|