@webda/core
Version:
Expose API with Lambda
520 lines • 15.5 kB
JavaScript
import { deepmerge } from "deepmerge-ts";
import * as events from "events";
import { EventEmitterUtils } from "../index.js";
/**
* Represent a Inject annotation
*
* @see Inject
*/
class Injector {
/**
*
* @param property annotated
* @param parameterOrName to inject from
* @param defaultValue in case of a parameter
* @param optional if set to true, won't throw an error if not found
*/
constructor(property, parameterOrName, defaultValue, optional = false) {
this.property = property;
this.optional = optional;
if (!defaultValue) {
if (parameterOrName.startsWith("params:")) {
this.parameter = parameterOrName.substring(7);
}
else {
this.value = parameterOrName;
}
}
else {
this.value = defaultValue;
this.parameter = parameterOrName;
}
}
/**
* Resolve the current Inject annotation inside the service
*
* @param service to resolve
*/
resolve(service) {
let name = this.value;
if (this.parameter) {
name = service.getParameters()[this.parameter] || this.value;
}
service[this.property] = service.getService(name);
if (!service[this.property] && !this.optional) {
if (this.parameter) {
throw new Error(`Injector did not found bean '${name}'(parameter:${this.parameter}) for '${service.getName()}'`);
}
throw new Error(`Injector did not found bean '${name}' for '${service.getName()}'`);
}
}
/**
* Inject all annotated dependencies to the current service
*
* @param service to inject to
*/
static resolveAll(service) {
(Object.getPrototypeOf(service).Injectors || []).forEach(injector => injector.resolve(service));
}
}
/**
* Inject a Bean inside this attribute
*
* If defaultValue is undefined and parameter is not starting with `params:`, it will
* resolve by calling `this.getService(parameterOrName)`
*
* If defaultValue is defined or parameterOrName starts with `params:` then first argument is
* consider a parameter and it will resolve by calling `this.getService(this.getParameters()[parameterOrName] || defaultValue)`
*
* @param parameterOrName of the service to inject
*
* Might consider to split into two annotations
*/
export function Inject(parameterOrName, defaultValue, optional) {
return function (target, propertyName) {
target.Injectors = target.Injectors || [];
if (typeof defaultValue === "boolean") {
target.Injectors.push(new Injector(propertyName, parameterOrName || propertyName, undefined, defaultValue));
}
else {
target.Injectors.push(new Injector(propertyName, parameterOrName || propertyName, defaultValue, optional));
}
};
}
/**
* Register an Operation within the framework
*
* An operation is a callable method with an input and output
* The method will receive a Context from where it can execute
*
* @param id
* @param input
* @param output
*/
export function Operation(properties, route) {
return function (target, executor) {
var _a, _b, _c, _d, _e, _f;
(_a = target.constructor).operations ?? (_a.operations = {});
properties ?? (properties = {});
properties.id ?? (properties.id = executor);
const id = properties.id;
if (target.constructor.operations[id]) {
console.error("Operation already exists", id);
return;
}
(_b = target.constructor.operations)[id] ?? (_b[id] = {
method: executor,
...properties
});
// If url is specified define the openapi
if (route) {
route.method ?? (route.method = "GET");
route.openapi ?? (route.openapi = {});
(_c = route.openapi)[_d = route.method.toLowerCase()] ?? (_c[_d] = {});
let def = route.openapi[route.method.toLowerCase()];
def.operationId = id;
def.schemas ?? (def.schemas = {});
(_e = def.schemas).input ?? (_e.input = properties.id.toLowerCase() + ".input");
(_f = def.schemas).output ?? (_f.output = properties.id.toLowerCase() + ".output");
Route(route.url, route.method, route.openapi)(target, executor);
}
};
}
// @Route to declare route on Bean
export function Route(route, methods = ["GET"], openapi = {}) {
return function (target, executor) {
var _a, _b;
(_a = target.constructor).routes ?? (_a.routes = {});
(_b = target.constructor.routes)[route] ?? (_b[route] = []);
target.constructor.routes[route].push({
methods: Array.isArray(methods) ? methods : [methods],
executor,
openapi
});
};
}
/**
* A utility class that takes a array of string or string transformed into regex that includes
* a start line and end line
*/
export class RegExpValidator {
constructor(info) {
info = Array.isArray(info) ? info : [info];
this.validators = info.map(i => RegExpValidator.getRegExp(i));
}
static getRegExp(reg) {
if (!reg.startsWith("^")) {
reg = "^" + reg;
}
if (!reg.endsWith("$")) {
reg += "$";
}
return new RegExp(reg);
}
validate(value) {
return this.validators.some(p => p.test(value));
}
}
/**
* Standardized way to allow string/regex validation within configuration
*
* If url is prefixed with `regex:` it is considered a regex
*
* @example
* ```typescript
* class MyServiceParameters extends ServiceParameters {
* urls: string[];
* }
*
* class MyService extends Service {
* loadParameters(params:any) {
* const parameters = new MyServiceParameters(params);
* this.urlsValidator = new RegExpStringValidator(parameters.urls);
* return parameters;
* }
* }
* ```
*/
export class RegExpStringValidator extends RegExpValidator {
constructor(info) {
info = Array.isArray(info) ? info : [info];
super(info.filter(i => i.startsWith("regex:")).map(i => i.substring(6)));
this.stringValidators = info.filter(i => !i.startsWith("regex:"));
}
/**
* Add string validation
* @param value
* @returns
*/
validate(value) {
return this.stringValidators.find(p => p === value) !== undefined || super.validate(value);
}
}
/**
* Interface to specify the Service parameters
*/
export class ServiceParameters {
/**
* Copy all parameters into the object by default
*
* @param params from webda.config.json
*/
constructor(params) {
Object.assign(this, params);
}
}
/**
* Use this object for representing a service in the application
* A Service is a singleton in the application, that is init after all others services are created
*
* You can use a Service to create Listeners or implement shared behavior between others services
*
* @exports
* @abstract
* @class Service
*/
class Service extends events.EventEmitter {
/**
*
*
* @class Service
* @param {Webda} webda - The main instance of Webda
* @param {String} name - The name of the service
* @param {Object} params - The parameters block define in the configuration file
*/
constructor(webda, name, params = {}) {
super();
this._initException = undefined;
this.logger = webda ? webda.getLogger(this) : undefined;
this._webda = webda;
this._name = name;
this.parameters = this.loadParameters(params);
}
/**
* Load the parameters for a service
*/
loadParameters(params) {
return new ServiceParameters(params);
}
/**
* Used to compute or derivate input parameter to attribute
*/
computeParameters() {
// Can be overriden by subclasses if needed
}
/**
* Get the service parameters
*/
getParameters() {
return this.parameters;
}
/**
* Return WebdaCore
*/
getWebda() {
return this._webda;
}
/**
* Shutdown the current service if action need to be taken
*/
async stop() {
// Nothing to do
}
/**
* Return service representation
*/
toString() {
return this.parameters.type + "[" + this._name + "]";
}
/**
* Resolve parameters
* Call initRoutes and initBeanRoutes
*/
resolve() {
this.initMetrics();
// Inject dependencies
Injector.resolveAll(this);
// We wait for all services to be created before calling computeParameters
this.computeParameters();
this.initRoutes();
this.initOperations();
return this;
}
/**
* Init the metrics
*/
initMetrics() {
this.metrics = {};
}
/**
* Add service name label
* @param type
* @param configuration
* @returns
*/
getMetric(type, configuration) {
configuration.labelNames ?? (configuration.labelNames = []);
configuration.labelNames = [...configuration.labelNames, "service"];
configuration.name = `${this.getName().toLowerCase()}_${configuration.name}`;
return this.getWebda().getMetric(type, configuration);
}
/**
* Return the events that an external system can subscribe to
*
* @returns
*/
getClientEvents() {
return [];
}
/**
* Authorize a public event subscription
* @param event
* @param context
*/
authorizeClientEvent(_event, _context) {
return false;
}
/**
* Return the full path url based on parameters
*
* @param url relative url to service
* @param _methods in case we need filtering (like Store)
* @returns absolute url or undefined if need to skip the Route
*/
getUrl(url, _methods) {
// If url is absolute
if (url.startsWith("/")) {
return url;
}
if (!this.parameters.url) {
return undefined;
}
if (url.startsWith(".")) {
if (this.parameters.url.endsWith("/") && url.startsWith("./")) {
return this.parameters.url + url.substring(2);
}
return this.parameters.url + url.substring(1);
}
return url;
}
/**
* If undefined is returned it cancel the operation registration
* @param id
* @returns
*/
getOperationId(id) {
return id;
}
/**
* Add a route dynamicaly
*
* @param {String} url of the route can contains dynamic part like {uuid}
* @param {Array[]} methods
* @param {Function} executer Method to execute for this route
*/
addRoute(url, methods, executer, openapi = {}, override = false) {
let finalUrl = this.getUrl(url, methods);
if (!finalUrl) {
return;
}
this._webda.addRoute(finalUrl, {
// Create bounded function to keep the context
_method: executer.bind(this),
executor: this._name,
openapi: deepmerge(openapi, this.parameters.openapi || {}),
methods,
override
});
}
/**
* Return variables for replacement in openapi
* @returns
*/
getOpenApiReplacements() {
return {};
}
/**
* Init the routes
*/
initRoutes() {
// @ts-ignore
let routes = this.constructor.routes || {};
for (let j in routes) {
this.log("TRACE", "Adding route", j, "for bean", this.getName());
routes[j].forEach(route => {
this.addRoute(j, route.methods, this[route.executor], route.openapi);
});
}
}
/**
* Init the operations
*/
initOperations() {
// @ts-ignore
let operations = this.constructor.operations || {};
for (let j in operations) {
const id = this.getOperationId(j);
if (!id)
continue;
this.log("TRACE", "Adding operation", id, "for bean", this.getName());
this._webda.registerOperation(j.includes(".") ? j : `${this.getName()}.${j}`, {
...operations[j],
service: this.getName(),
input: `${this.getName()}.${operations[j].method}.input`,
output: `${this.getName()}.${operations[j].method}.output`,
id
});
}
}
/**
* Convert an object to JSON using the Webda json filter
*
* @class Service
* @param {Object} object - The object to export
* @return {String} The export of the strip object ( removed all attribute with _ )
*/
toPublicJSON(object) {
return this._webda.toPublicJSON(object);
}
/**
* Prevent service to be serialized
* @returns
*/
toJSON() {
return this._name;
}
/**
* Will be called after all the Services are created
*
* @param config for the host so you can add your own route here
* @abstract
*/
async init() {
// Can be overriden by subclasses if needed
return this;
}
/**
*
* @param config new parameters for the service
*/
async reinit(config) {
this.parameters = this.loadParameters(config);
this.computeParameters();
return this.init();
}
/**
* Emit the event with data and wait for Promise to finish if listener returned a Promise
*/
emitSync(event, data) {
return EventEmitterUtils.emitSync(this, event, data);
}
/**
* Override to allow capturing long listeners
* @override
*/
emit(event, data) {
return EventEmitterUtils.emit(this, event, data);
}
/**
* Type the listener part
* @param event
* @param listener
* @param queue
* @returns
*/
on(event, listener) {
super.on(event, listener);
return this;
}
/**
* Listen to an event as on(...) would do except that it will be asynchronous
* @param event
* @param callback
* @param queue Name of queue to use, can be undefined, queue name are used to define differents priorities
*/
onAsync(event, listener, queue = undefined) {
this._webda.getService("AsyncEvents").bindAsyncListener(this, event, listener, queue);
}
/**
* Return a webda service
* @param service name to retrieve
*/
getService(service) {
return this._webda.getService(service);
}
/**
* Get service name
*/
getName() {
return this._name;
}
/**
* Clean the service data, can only be used in test mode
*
* @abstract
*/
__clean() {
// @ts-ignore
if (typeof global.it !== "function") {
throw Error("Only for test purpose");
}
return this.___cleanData();
}
/**
* @private
*/
___cleanData() {
return Promise.resolve();
}
/**
*
* @param level to log
* @param args
*/
log(level, ...args) {
// Add the service name to avoid confusion
this.logger.log(level, `[${this._name}]`, ...args);
}
}
export { Service };
//# sourceMappingURL=service.js.map