mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-18 15:42:54 +00:00
refactor: split api into api, api-cli & api-plugin
This commit is contained in:
2
packages/api-plugin/src/index.ts
Normal file
2
packages/api-plugin/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './plugin.js';
|
||||
export * from './plugin-client.js';
|
||||
68
packages/api-plugin/src/plugin-client.ts
Normal file
68
packages/api-plugin/src/plugin-client.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (C) 2019-2022 Open StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {SCPluginRegisterRequest, SCPluginRegisterRoute} from '@openstapps/core';
|
||||
import {ConnectorClient} from '@openstapps/api';
|
||||
import {Plugin} from './plugin.js';
|
||||
|
||||
/**
|
||||
* The PluginClient for registering and unregistering HTTP Plugins
|
||||
*
|
||||
* It contains a lot of the boilerplate for creating plugins, and thus simplifies the creation of such.
|
||||
*/
|
||||
export class PluginClient extends ConnectorClient {
|
||||
/**
|
||||
* Register a plugin in the backend
|
||||
*
|
||||
* **This method automatically calls [[Plugin.start]]**
|
||||
* You need to call this method before you can do anything with the plugin. If you want to register the plugin again,
|
||||
* you might first want to inform yourself how the backend behaves in such cases TODO: add docs for this
|
||||
* @param plugin The instance of the plugin you want to register
|
||||
*/
|
||||
async registerPlugin(plugin: Plugin) {
|
||||
const request: SCPluginRegisterRequest = {
|
||||
action: 'add',
|
||||
plugin: {
|
||||
address: plugin.fullUrl,
|
||||
name: plugin.name,
|
||||
requestSchema: plugin.requestSchema,
|
||||
responseSchema: plugin.responseSchema,
|
||||
route: plugin.route,
|
||||
},
|
||||
};
|
||||
await this.invokeRoute(new SCPluginRegisterRoute(), undefined, request);
|
||||
|
||||
// start the plugin we just registered
|
||||
plugin.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a plugin from the backend
|
||||
*
|
||||
* **This method automatically calls [[Plugin.stop]]**
|
||||
* If you want to unregister your plugin for some reason, you can do so by calling this method.
|
||||
* Use with caution.*
|
||||
* @param plugin The instance of the plugin you want to register
|
||||
*/
|
||||
async unregisterPlugin(plugin: Plugin) {
|
||||
const request: SCPluginRegisterRequest = {
|
||||
action: 'remove',
|
||||
route: plugin.route,
|
||||
};
|
||||
// stop the plugin we want to unregister
|
||||
plugin.stop();
|
||||
|
||||
await this.invokeRoute(new SCPluginRegisterRoute(), undefined, request);
|
||||
}
|
||||
}
|
||||
250
packages/api-plugin/src/plugin.ts
Normal file
250
packages/api-plugin/src/plugin.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
* Copyright (C) 2019-2022 Open StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {Converter} from '@openstapps/core-tools';
|
||||
import {Logger} from '@openstapps/logger';
|
||||
import bodyParser from 'body-parser';
|
||||
import express from 'express';
|
||||
import * as http from 'http';
|
||||
import * as http2 from 'http2';
|
||||
import {JSONSchema7} from 'json-schema';
|
||||
import morgan from 'morgan';
|
||||
import ErrnoException = NodeJS.ErrnoException;
|
||||
|
||||
/**
|
||||
* The Plugin for creating HTTP backend plugins
|
||||
*
|
||||
* It contains a lot of the boilerplate for creating plugins, and thus simplifies the creation of such.
|
||||
* To create your own plugin, you need to extend this class and implement the [[Plugin.onRouteInvoke]] method
|
||||
*/
|
||||
export abstract class Plugin {
|
||||
/**
|
||||
* The express instance
|
||||
*/
|
||||
private readonly app = express();
|
||||
|
||||
/**
|
||||
* The HTTP server
|
||||
*/
|
||||
private readonly server: http.Server;
|
||||
|
||||
/**
|
||||
* Whether the server is active or not
|
||||
*
|
||||
* When active is false, it will return 404 on all routes.
|
||||
*/
|
||||
protected active = false;
|
||||
|
||||
/**
|
||||
* The full URL of the plugin
|
||||
*
|
||||
* The full URL of the plugin consists out of URL:PORT
|
||||
*/
|
||||
public get fullUrl() {
|
||||
return `${this.url}:${this.port}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The port on which the plugin will listen on
|
||||
*/
|
||||
public port: string | number | false;
|
||||
|
||||
/**
|
||||
* The schema of the request interfaces defined by the user
|
||||
*/
|
||||
public readonly requestSchema: JSONSchema7 = {};
|
||||
|
||||
/**
|
||||
* The schema of the response interfaces defined by the user
|
||||
*/
|
||||
public readonly responseSchema: JSONSchema7 = {};
|
||||
|
||||
/**
|
||||
* Normalize a port into a number, string, or false.
|
||||
* @param value the port you want to normalize
|
||||
*/
|
||||
protected static normalizePort(value: string) {
|
||||
const portNumber = Number.parseInt(value, 10);
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (Number.isNaN(portNumber)) {
|
||||
// named pipe
|
||||
/* istanbul ignore next */
|
||||
return value;
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (portNumber >= 0) {
|
||||
// port number
|
||||
return portNumber;
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance of the PluginClient
|
||||
*
|
||||
* Don't forget to call [[PluginClient.registerPlugin]]!
|
||||
* Refer to the examples for how to use the schemas. TODO: examples
|
||||
* @param port The port of the plugin
|
||||
* @param name The name of the plugin
|
||||
* @param url The url of the plugin without the port or anything else, for example `http://localhost`
|
||||
* @param route The desired route that will be registered in the backend
|
||||
* @param backendUrl The url of the backend
|
||||
* @param converter If you want to use an already existing converter, you can pass it here
|
||||
* @param requestName the name of the request schema
|
||||
* @param responseName the name of the response schema
|
||||
* @param version the version. You should retrieve it from the package.json
|
||||
*/
|
||||
constructor(
|
||||
port: number,
|
||||
public name: string,
|
||||
public url: string,
|
||||
public route: string,
|
||||
protected backendUrl: string,
|
||||
converter: Converter,
|
||||
requestName: string,
|
||||
responseName: string,
|
||||
version: string,
|
||||
) {
|
||||
this.app.use(bodyParser.json());
|
||||
this.port = Plugin.normalizePort(
|
||||
/* istanbul ignore next */
|
||||
process.env.PORT === undefined ? port.toString() : process.env.PORT,
|
||||
);
|
||||
this.app.set('port', this.port);
|
||||
|
||||
// setup express
|
||||
this.server = http.createServer(this.app);
|
||||
this.server.listen(this.port);
|
||||
/* istanbul ignore next */
|
||||
this.server.on('error', error => {
|
||||
/* istanbul ignore next */
|
||||
this.onError(error);
|
||||
});
|
||||
this.server.on('listening', () => {
|
||||
this.onListening();
|
||||
});
|
||||
|
||||
this.requestSchema = converter.getSchema(requestName, version);
|
||||
this.responseSchema = converter.getSchema(responseName, version);
|
||||
|
||||
this.app.use(morgan('dev'));
|
||||
|
||||
this.app.set('env', process.env.NODE_ENV);
|
||||
|
||||
this.app.all('*', async (request: express.Request, response: express.Response) => {
|
||||
if (this.active) {
|
||||
await this.onRouteInvoke(request, response);
|
||||
} else {
|
||||
response.status(http2.constants.HTTP_STATUS_NOT_FOUND);
|
||||
response.send();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "error" event.
|
||||
* @param error The error that occurred
|
||||
*/
|
||||
|
||||
/* istanbul ignore next */
|
||||
private onError(error: ErrnoException) {
|
||||
if (error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const bind = typeof this.port === 'string' ? `Pipe ${this.port}` : `Port ${this.port}`;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES': {
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
Logger.error(`${bind} requires elevated privileges`);
|
||||
process.exit(1);
|
||||
break;
|
||||
}
|
||||
case 'EADDRINUSE': {
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
Logger.error(`${bind} is already in use`);
|
||||
process.exit(1);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "listening" event.
|
||||
*/
|
||||
private onListening() {
|
||||
const addr = this.server.address();
|
||||
/* istanbul ignore next */
|
||||
const bind = typeof addr === 'string' ? `pipe ${addr}` : addr === null ? 'null' : `port ${addr.port}`;
|
||||
Logger.ok(`Listening on ${bind}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* When the route gets invoked
|
||||
*
|
||||
* Override this method for your own plugin
|
||||
* @param request An express Request from the backend
|
||||
* @param response An express Response to the backend for you to send back data
|
||||
*/
|
||||
protected abstract onRouteInvoke(request: express.Request, response: express.Response): Promise<void>;
|
||||
|
||||
/**
|
||||
* Closes the server
|
||||
*
|
||||
* This will stop the plugin from listening to any requests at all, and is currently an irreversible process.
|
||||
* This means, that the instantiated plugin is basically useless afterwards.
|
||||
*/
|
||||
public async close() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.server.close(error => {
|
||||
/* istanbul ignore next */
|
||||
if (error !== undefined) {
|
||||
/* istanbul ignore next */
|
||||
reject(error);
|
||||
}
|
||||
resolve(undefined);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the plugin
|
||||
*
|
||||
* **THIS METHOD GETS CALLED AUTOMATICALLY WITH [[PluginClient.registerPlugin]]**
|
||||
* If the plugin is not started, it will return 404 on any route
|
||||
*/
|
||||
public start() {
|
||||
this.active = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the plugin
|
||||
*
|
||||
* **THIS METHOD GETS CALLED AUTOMATICALLY WITH [[PluginClient.unregisterPlugin]]**
|
||||
* If the plugin is not started, it will return 404 on any route
|
||||
*/
|
||||
public stop() {
|
||||
// You can't unregister routes from express. So this is a workaround.
|
||||
this.active = false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user