@webda/core
Version:
Expose API with Lambda
1,309 lines • 43.1 kB
JavaScript
import Ajv from "ajv";
import addFormats from "ajv-formats";
import { deepmerge } from "deepmerge-ts";
import * as events from "events";
import jsonpath from "jsonpath";
import pkg from "node-machine-id";
import { Counter, Gauge, Histogram, register } from "prom-client";
import { v4 as uuidv4 } from "uuid";
import { BinaryService, GlobalContext, Logger, OperationContext, RegExpValidator, Store, UnpackedApplication, WebContext, WebdaError, WebdaQL } from "./index.js";
import { CoreModel } from "./models/coremodel.js";
import { Router } from "./router.js";
import { JSONUtils } from "./utils/serializers.js";
const { machineIdSync } = pkg;
export class EventEmitterUtils {
static emit(eventEmitter, event, data) {
for (let listener of eventEmitter.listeners(event)) {
let start = Date.now();
listener(data);
this.elapse(start);
}
return true;
}
static async emitSync(eventEmitter, event, data) {
let promises = [];
for (let listener of eventEmitter.listeners(event)) {
let start = Date.now();
let result = listener(data);
if (result instanceof Promise) {
promises.push(result
.catch(err => {
Core.get().log("ERROR", "Listener error", err);
})
.then(() => {
this.elapse(start);
}));
}
else {
this.elapse(start);
}
}
return Promise.all(promises);
}
/**
* Display a message if the listener takes too long
* @param start
*/
static elapse(start) {
let elapsed = Date.now() - start;
if (elapsed > 100) {
Core.get().log("INFO", "Long listener", elapsed, "ms");
}
}
}
/**
* Copy from https://github.com/ajv-validator/ajv/blob/master/lib/runtime/validation_error.ts
* It is not exported by ajv
*/
export class ValidationError extends Error {
constructor(errors) {
super(`validation failed: ${errors.map(e => e.message).join("; ")}`);
this.errors = errors;
this.ajv = this.validation = true;
}
}
/**
* Operation
*/
export class OperationError extends Error {
constructor(operation, type) {
super(`Operation ${operation} ${type}`);
this.operation = operation;
this.type = type;
}
}
/**
* Filter request based on their origin
*
* @category CoreFeatures
*/
export class OriginFilter {
constructor(origins) {
this.regexs = new RegExpValidator(origins);
}
/**
*
* @param context
* @returns
*/
async checkRequest(context) {
let httpContext = context.getHttpContext();
return this.regexs.validate(httpContext.hostname) || this.regexs.validate(httpContext.origin);
}
}
/**
* Authorize requests based on the website
*/
export class WebsiteOriginFilter {
constructor(website) {
this.websites = [];
if (!Array.isArray(website)) {
if (typeof website === "object") {
this.websites.push(website.url);
}
else {
this.websites.push(website);
}
}
else {
this.websites = [...website];
}
}
async checkRequest(context) {
let httpContext = context.getHttpContext();
if (this.websites.indexOf(httpContext.origin) >= 0 ||
this.websites.indexOf(httpContext.host) >= 0 ||
this.websites.indexOf("*") >= 0) {
return true;
}
return false;
}
}
// @Bean to declare as a Singleton service
export function Bean(constructor) {
let name = constructor.name;
// @ts-ignore
process.webdaBeans ?? (process.webdaBeans = {});
// @ts-ignore
const beans = process.webdaBeans;
beans[name] ?? (beans[name] = constructor);
}
/**
* This is the main class of the framework, it handles the routing, the services initialization and resolution
*
* @class Core
* @category CoreFeatures
*/
export class Core extends events.EventEmitter {
/**
* @params {Object} config - The configuration Object, if undefined will load the configuration file
*/
constructor(application) {
var _a, _b, _c, _d, _e, _f, _g, _h;
/** @ignore */
super();
/**
* Webda Services
* @hidden
*/
this.services = {};
/**
* Router that will route http request in
*/
this.router = new Router(this);
/**
* If Core is already initiated
*/
this._initiated = false;
/**
* Services who failed to create or initialize
*/
this.failedServices = {};
/**
* Request Filter registry
*
* Added via [[Webda.registerRequestFilter]]
* See [[CorsFilter]]
*/
this._requestFilters = [];
/**
* CORS Filter registry
*
* Added via [[Webda.registerCORSRequestFilter]]
* See [[CorsFilter]]
*/
this._requestCORSFilters = [];
/**
* Contains all operations defined by services
*/
this.operations = {};
/**
* Cache for model to store resolution
*/
this._modelStoresCache = new Map();
/**
* Cache for model to store resolution
*/
this._modelBinariesCache = new Map();
/**
* True if the dual import warning has been sent
*/
this._dualImportWarn = false;
/**
* Registered context providers
*/
this._contextProviders = [
{
getContext: (info) => {
// If http is defined, return a WebContext
if (info.http) {
return new WebContext(this, info.http, info.stream);
}
return new OperationContext(this, info.stream);
}
}
];
/**
*
*/
this.interuptables = [];
// Store WebdaCore in process to avoid conflict with import
// @ts-ignore
Core.singleton = process.webda = this;
/**
* SIGINT handler
*/
process.on("SIGINT", async () => {
if (Core.get()?.interuptables.length > 0) {
console.log("Received SIGINT. Cancelling all interuptables.");
await Promise.all(Core.get().interuptables.map(i => i.cancel()));
}
process.exit(0);
});
this.workerOutput = application.getWorkerOutput();
this.logger = new Logger(this.workerOutput, "@webda/core/lib/core.js");
this.application = application || new UnpackedApplication(".");
this._initTime = new Date().getTime();
// Schema validations
this._ajv = new Ajv();
addFormats(this._ajv);
this._ajvSchemas = {};
// Load the configuration and migrate
this.configuration = this.application.getCurrentConfiguration();
// Init default values for configuration
(_a = this.configuration).parameters ?? (_a.parameters = {});
(_b = this.configuration.parameters).apiUrl ?? (_b.apiUrl = "http://localhost:18080");
(_c = this.configuration.parameters).defaultStore ?? (_c.defaultStore = "Registry");
(_d = this.configuration.parameters).metrics ?? (_d.metrics = {});
if (this.configuration.parameters.metrics) {
(_e = this.configuration.parameters.metrics).labels ?? (_e.labels = {});
(_f = this.configuration.parameters.metrics).config ?? (_f.config = {});
(_g = this.configuration.parameters.metrics).prefix ?? (_g.prefix = "");
}
(_h = this.configuration).services ?? (_h.services = {});
// Add CSRF origins filtering
if (this.configuration.parameters.csrfOrigins) {
this.registerCORSFilter(new OriginFilter(this.configuration.parameters.csrfOrigins));
}
// Add CSRF website filtering
if (this.configuration.parameters.website) {
this.registerCORSFilter(new WebsiteOriginFilter(this.configuration.parameters.website));
}
this.setGlobalContext(new GlobalContext(this));
}
/**
* Register a cancelable process
* @param interuptable
*/
static registerInteruptableProcess(interuptable) {
Core.get()?.interuptables.push(interuptable);
}
/**
* Unregister a cancelable process
* @param interuptable
*/
static unregisterInteruptableProcess(interuptable) {
let id = Core.get()?.interuptables.findIndex(i => i === interuptable);
id ?? (id = -1);
if (id >= 0) {
Core.get().interuptables.splice(id, 1);
}
}
/**
* Get the current script location
* @returns
*/
getScriptUrl() {
return import.meta.url;
}
/**
* Return information on the singleton import and version
* @param singleton
* @returns
*/
static getSingletonInfo(singleton) {
let res = `- `;
if (singleton.getScriptUrl) {
res += singleton.getScriptUrl();
}
res += " / " + singleton.getVersion();
return res;
}
/**
* Get the singleton of Webda Core
* @returns
*/
static get() {
// @ts-ignore
let singleton = process.webda;
if (Core.singleton !== singleton &&
!singleton._dualImportWarn &&
Core.singleton !== undefined &&
singleton !== undefined) {
singleton._dualImportWarn = true;
singleton.log("ERROR", `Several import version of WebdaCore has been identified.\n\tIt can impact your models.\n\t- ${this.getSingletonInfo(singleton)}\n\t- ${this.getSingletonInfo(Core.singleton)}`);
}
// Store WebdaCore in process to avoid conflict with import
return singleton;
}
/**
* Enforce a specific store for a model
*
* Useful in some specific case when you want to update store dynamically
*
* @param model
* @param store
*/
setModelStore(model, store) {
this._modelStoresCache.set(model, store);
}
/**
* Get the store assigned to this model
* @param model
* @returns
*/
getModelStore(modelOrConstructor) {
const model = ((modelOrConstructor instanceof CoreModel ? modelOrConstructor.__class : modelOrConstructor));
if (this._modelStoresCache.has(model)) {
return this._modelStoresCache.get(model);
}
const setCache = store => {
this._modelStoresCache.set(model, store);
};
const stores = this.getStores();
let actualScore;
let actualStore = this.getService(this.parameter("defaultStore") || "Registry");
for (let store in stores) {
let score = stores[store].handleModel(model);
// As 0 mean exact match we stop there
if (score === 0) {
setCache(stores[store]);
return stores[store];
}
else if (score > 0 && (actualScore === undefined || actualScore > score)) {
actualScore = score;
actualStore = stores[store];
}
}
setCache(actualStore);
return actualStore;
}
/**
* Get the service that manage a model
* @param modelOrConstructor
* @param attribute
* @returns
*/
getBinaryStore(modelOrConstructor, attribute) {
const binaries = this.getServicesOfType(BinaryService);
const model = this.application.getModelName(modelOrConstructor);
let actualScore = -1;
let actualService;
const setCache = store => {
this._modelBinariesCache.set(model, store);
};
for (let binary in binaries) {
let score = binaries[binary].handleBinary(model, attribute);
// As 0 mean exact match we stop there
if (score === 2) {
setCache(binaries[binary]);
return binaries[binary];
}
else if (score >= 0 && (actualService === undefined || actualScore > score)) {
actualScore = score;
actualService = binaries[binary];
}
}
if (!actualService) {
throw new Error("No binary store found for " + model + " " + attribute);
}
setCache(actualService);
return actualService;
}
/**
* Return Core instance id
*
* It is a random generated string
*/
getInstanceId() {
this.instanceId ?? (this.instanceId = this.getUuid());
return this.instanceId;
}
/**
* Get absolute url with subpath
* @param subpath
*/
getApiUrl(subpath = "") {
if (subpath.length > 0 && !subpath.startsWith("/")) {
subpath = "/" + subpath;
}
return this.configuration.parameters.apiUrl + subpath;
}
/**
* Return application path with subpath
*
* Helper that redirect to this.application.getAppPath
*
* @param subpath
* @returns
*/
getAppPath(subpath = "") {
return this.application.getAppPath(subpath);
}
/**
* Retrieve all detected modules definition
*/
getModules() {
return this.application.getModules();
}
/**
* Return application definition
*/
getApplication() {
return this.application;
}
/**
* Get WorkerOutput
*/
getWorkerOutput() {
return this.workerOutput;
}
/**
* Init one service
* @param service
*/
async initService(service) {
try {
this.log("TRACE", "Initializing service", service);
this.services[service]._initTime = Date.now();
await this.services[service].init();
}
catch (err) {
this.services[service]._initException = err;
this.failedServices[service] = { _initException: err };
this.log("ERROR", "Init service " + service + " failed: " + err.message);
this.log("TRACE", err.stack);
}
}
/**
* Get an object from the application based on its full uuid
* @param fullUuid
* @param partials
*/
async getModelObject(fullUuid, partials) {
return CoreModel.fromFullUuid(fullUuid, this, partials);
}
/**
* Init Webda
*
* It will resolve Services init method and autolink
*/
async init() {
if (this._init) {
return this._init;
}
if (this.configuration.parameters.configurationService) {
try {
this.log("INFO", "Create and init ConfigurationService", this.configuration.parameters.configurationService);
// Create the configuration service
this.createService(this.configuration.services, this.configuration.parameters.configurationService);
let cfg = await this.getService(this.configuration.parameters.configurationService).initConfiguration();
if (cfg) {
cfg.parameters ?? (cfg.parameters = {});
cfg.services ?? (cfg.services = {});
this.configuration.parameters = deepmerge(this.configuration.parameters, cfg.parameters);
// Ensure beans are known too
Object.keys(this.getBeans()).forEach(bean => {
var _a;
(_a = this.configuration.services)[bean] ?? (_a[bean] = {
type: `Beans/${bean}`
});
});
// Merge services - for security reason we cannot add new services from configuration
for (let i in this.configuration.services) {
this.configuration.services[i] = {
...deepmerge(this.configuration.services[i], cfg.services[i] || {}),
type: this.configuration.services[i].type
};
}
}
await this.getService(this.configuration.parameters.configurationService).init();
}
catch (err) {
this.log("ERROR", "Cannot use ConfigurationService", this.configuration.parameters.configurationService, err);
this.services = {};
}
}
// Init the other services
this.initStatics();
// Reset the model cache
this._modelStoresCache.clear();
this._modelBinariesCache.clear();
this.log("TRACE", "Create Webda init promise");
this._init = (async () => {
await this.initService("Registry");
await this.initService("CryptoService");
// Init services
let service;
let inits = [];
for (service in this.services) {
if (this.services[service].init !== undefined &&
!this.services[service]._createException &&
!this.services[service]._initTime) {
inits.push(this.initService(service));
}
}
await Promise.all(inits);
await this.emitSync("Webda.Init.Services", this.services);
})();
return this._init;
}
/**
* Pause for time ms
*
* @param time ms
*/
static async sleep(time) {
return new Promise(resolve => {
setTimeout(resolve, time);
});
}
/**
* Check if an operation can be executed with the current context
* Not checking the input use `checkOperation` instead to check everything
* @param context
* @param operationId
* @throws OperationError if operation is unknown
* @returns true if operation can be executed
*/
checkOperationPermission(context, operationId) {
var _a;
if (!this.operations[operationId]) {
throw new OperationError(operationId, "Unknown");
}
if (this.operations[operationId].permission) {
(_a = this.operations[operationId]).permissionQuery ?? (_a.permissionQuery = new WebdaQL.QueryValidator(this.operations[operationId].permission));
return this.operations[operationId].permissionQuery.eval(context.getSession());
}
return true;
}
/**
* Check if an operation can be executed with the current context
* @param context
* @param operationId
*/
async checkOperation(context, operationId) {
if (!this.checkOperationPermission(context, operationId)) {
throw new OperationError(operationId, "PermissionDenied");
}
let input = await context.getInput();
this.log("TRACE", `Operation ${operationId} input is '${JSONUtils.safeStringify(input, undefined, 2)}' (schema: ${this.operations[operationId].input})`);
try {
if (this.operations[operationId].input &&
(input === undefined || this.validateSchema(this.operations[operationId].input, input) !== true)) {
throw new OperationError(operationId, "InvalidInput");
}
}
catch (err) {
if (err instanceof ValidationError) {
throw new OperationError(operationId, "InvalidInput");
}
throw err;
}
}
/**
* Call an operation within the framework
*/
async callOperation(context, operationId) {
context.setExtension("operation", operationId);
await this.checkOperation(context, operationId);
return this.getService(this.operations[operationId].service)[this.operations[operationId].method](context);
}
/**
* Get available operations
* @returns
*/
listOperations() {
const list = {};
Object.keys(this.operations).forEach(o => {
list[o] = {
...this.operations[o]
};
delete list[o].service;
delete list[o].method;
});
return list;
}
/**
* Register a new operation within the app
* @param operationId
* @param definition
*/
registerOperation(operationId, definition) {
if (operationId.match(/[^a-zA-Z0-9.]/)) {
throw new Error("OperationId can only contain [a-zA-Z0-9.]");
}
this.operations[operationId] = { ...definition, id: operationId };
["input", "output"]
.filter(key => this.operations[operationId][key])
.forEach(key => {
if (!this.getApplication().hasSchema(this.operations[operationId][key])) {
delete this.operations[operationId][key];
}
});
}
/**
* Register a request filtering
*
* Will apply to all requests regardless of the devMode
* @param filter
*/
registerRequestFilter(filter) {
this._requestFilters.push(filter);
}
/**
* Register a CORS request filtering
*
* Does not apply in devMode
* @param filter
*/
registerCORSFilter(filter) {
this._requestCORSFilters.push(filter);
}
/**
* Register a new context provider
* @param provider
*/
registerContextProvider(provider) {
this._contextProviders.unshift(provider);
}
/**
* Validate the object with schema
*
* @param schema path to use
* @param object to validate
*/
validateSchema(webdaObject, object, ignoreRequired) {
let name = typeof webdaObject === "string" ? webdaObject : this.application.getModelFromInstance(webdaObject);
let cacheName = name;
if (name?.endsWith("?")) {
name = name.substring(0, name.length - 1);
ignoreRequired = true;
}
if (ignoreRequired) {
cacheName += "_noRequired";
}
if (!this._ajvSchemas[cacheName]) {
let schema = this.application.getSchema(name);
if (!schema) {
return null;
}
if (ignoreRequired) {
schema = JSONUtils.duplicate(schema);
schema.required = [];
}
this.log("TRACE", "Add schema for", name);
this._ajv.addSchema(schema, cacheName);
this._ajvSchemas[cacheName] = true;
}
if (this._ajv.validate(cacheName, object)) {
return true;
}
throw new ValidationError(this._ajv.errors);
}
/**
* Return webda current version
*
* @returns package version
* @since 0.4.0
*/
getVersion() {
return this.getApplication().getWebdaVersion();
}
/**
* To define the locales just add a locales: ['en-GB', 'fr-FR'] in your host global configuration
*
* @return The configured locales or "en-GB" if none are defined
*/
getLocales() {
if (!this.configuration || !this.configuration.parameters.locales) {
return ["en-GB"];
}
return this.configuration.parameters.locales;
}
/**
* Get a Logger for a class
* @param clazz
*/
getLogger(clazz) {
let className = clazz;
if (typeof clazz !== "string") {
let definitions = this.application.getModdas();
for (let i in definitions) {
if (definitions[i] === clazz.constructor) {
className = i.replace(/\//g, ".");
break;
}
}
}
return new Logger(this.workerOutput, className);
}
/**
* Add a route dynamicaly
*
* @param {String} url of the route can contains dynamic part like {uuid}
* @param {Object} info the type of executor
*/
addRoute(url, info) {
this.router.addRoute(url, info);
}
/**
* Remove a route dynamicly
*
* @param {String} url to remove
*/
removeRoute(url) {
this.router.removeRoute(url);
}
/**
* Return current Router object
*/
getRouter() {
return this.router;
}
/**
* Check for a service name and return the wanted singleton or undefined if none found
*
* @param {String} name The service name to retrieve
*/
getService(name = "") {
return this.services[name];
}
/**
* Return a map of defined services
* @returns {{}}
*/
getServices() {
return this.services;
}
/**
* Return a map of services that extends type
* @param type The type of implementation
* @returns {{}}
*/
getServicesOfType(type = undefined) {
let result = {};
for (let i in this.services) {
let service = this.services[i];
if (!type || service instanceof type) {
result[i] = service;
}
}
return result;
}
getConfiguration() {
return this.configuration;
}
/**
* Return a map of defined stores
* @returns {{}}
*/
getStores() {
return this.getServicesOfType(Store);
}
/**
* Return a map of defined models
* @returns {{}}
*/
getModels() {
return this.application.getModels();
}
/**
* Check for a model name and return the wanted class or throw exception if none found
*
* @param {String} name The model name to retrieve
*/
getModel(name) {
return this.application.getModel(name);
}
/**
* Add to context information and executor based on the http context
*/
updateContextWithRoute(ctx) {
let http = ctx.getHttpContext();
// Check mapping
let route = this.router.getRouteFromUrl(ctx, http.getMethod(), http.getRelativeUri());
if (route === undefined) {
return false;
}
ctx.setRoute({ ...this.configuration, ...route });
ctx.setExecutor(this.getService(route.executor));
this.emit("Webda.UpdateContextRoute", { context: ctx });
return true;
}
/**
* Flush the headers to the response, no more header modification is possible after that
*
* This method should set the `context.setFlushedHeaders()` and use of `context.hasFlushedHeaders()`
*
* @abstract
*/
flushHeaders(_context) {
// Should be overriden by implementation
}
/**
* Flush the entire response to the client
*/
flush(_context) {
// Should be overriden by implementation
}
/**
* Return if Webda is in debug mode
*/
isDebug() {
return false;
}
/**
* Return the global parameters of a domain
*/
getGlobalParams() {
return this.configuration.parameters || {};
}
/**
* Get the system context
* @returns
*/
getGlobalContext() {
return this.globalContext;
}
/**
* Set the system context
* @param context
*/
setGlobalContext(context) {
this.globalContext = context;
}
/**
* Reinit one service
* @param service
*/
async reinitService(service) {
try {
this.log("TRACE", "Re-Initializing service", service);
let serviceBean = this.services[service];
await serviceBean.reinit(this.getServiceParams(serviceBean.getName()));
}
catch (err) {
this.log("ERROR", "Re-Init service " + service + " failed", err);
this.log("TRACE", err.stack);
}
}
/**
* Reinit all services with updated parameters
* @param updates
* @returns
*/
async reinit(updates) {
let configuration = JSON.parse(JSON.stringify(this.configuration.services));
for (let service in updates) {
jsonpath.value(configuration, service, updates[service]);
}
if (JSON.stringify(Object.keys(configuration)) !== JSON.stringify(Object.keys(this.configuration.services))) {
this.log("ERROR", "Configuration update cannot modify services");
throw new WebdaError.CodeError("REINIT_SERVICE_INJECTION", "Configuration is not designed to add dynamically services");
}
this.configuration.services = configuration;
let inits = [];
for (let service in this.services) {
inits.push(this.reinitService(service));
}
await Promise.all(inits);
}
/**
* Get a full resolved service parameter
*
* @param service
* @param configuration
* @returns
*/
getServiceParams(service, configuration = { parameters: {}, services: {} }) {
var _a;
configuration.parameters ?? (configuration.parameters = {});
configuration.services ?? (configuration.services = {});
(_a = configuration.services)[service] ?? (_a[service] = {});
const params = deepmerge(this.configuration.parameters || {}, configuration.parameters || {}, this.configuration.services[service] || {}, configuration.services[service] || {});
delete params.require;
return params;
}
createService(services, service) {
let type = services[service]?.type;
if (type === undefined) {
type = service;
}
let serviceConstructor = undefined;
try {
serviceConstructor = this.application.getModda(type);
}
catch (ex) {
this.log("ERROR", `Create service ${service}(${type}) failed ${ex.message}`);
this.log("TRACE", ex.stack);
return;
}
try {
this.log("TRACE", "Constructing service", service);
this.services[service] = new serviceConstructor(this, service, this.getServiceParams(service));
}
catch (err) {
this.log("ERROR", "Cannot create service", service, err);
// @ts-ignore
this.failedServices[service] = { _createException: err };
}
}
getBeans() {
// @ts-ignore
return process.webdaBeans || {};
}
/**
* @hidden
*
*/
createServices(excludes = []) {
const services = this.configuration.services;
const beans = this.getBeans();
this.log("DEBUG", "BEANS", beans);
for (let i in beans) {
let name = beans[i].name;
if (!services[name]) {
services[name] = {};
}
// Force type to Bean
services[name].type = `Beans/${name}`;
// Register the type
this.application.addService(`Beans/${name}`, beans[i]);
}
// Construct services
for (let service in services) {
if (excludes.indexOf(service) >= 0) {
continue;
}
this.createService(services, service);
}
// Call resolve on all services
Object.keys(this.services)
.filter(s => !excludes.includes(s))
.forEach(s => {
try {
this.services[s].resolve();
}
catch (err) {
this.log("ERROR", `Service(${s})`, err);
}
});
this.emit("Webda.Create.Services", this.services);
}
/**
* A registry is a predefined store
* @returns
*/
getRegistry() {
return this.registry;
}
/**
* Return the crypto service
* @returns
*/
getCrypto() {
return this.cryptoService;
}
/**
* Stop all services
*/
async stop() {
const services = this.getServices();
await Promise.all(Object.keys(services).map(async (s) => {
try {
await services[s].stop();
}
catch (err) {
this.log("ERROR", `Cannot stop service ${s}`, err);
}
}));
}
jsonFilter(key, value) {
if (key[0] === "_")
return undefined;
return value;
}
static getMachineId() {
try {
return process.env["WEBDA_MACHINE_ID"] || machineIdSync();
/* c8 ignore next 4 */
}
catch (err) {
// Useful in k8s pod
return process.env["HOSTNAME"];
}
}
/**
* Init services and Beans along with Routes
*/
initStatics() {
var _a, _b, _c;
// Init the registry
const autoRegistry = this.configuration.services["Registry"] === undefined;
(_a = this.configuration.services)["Registry"] ?? (_a["Registry"] = {
type: "Webda/MemoryStore",
persistence: {
path: ".registry",
key: Core.getMachineId()
}
});
this.createService(this.configuration.services, "Registry");
this.registry = this.getService("Registry").resolve();
// Init the key service
(_b = this.configuration.services)["CryptoService"] ?? (_b["CryptoService"] = {
type: "Webda/CryptoService",
autoRotate: autoRegistry ? 30 : undefined,
autoCreate: true
});
this.createService(this.configuration.services, "CryptoService");
this.cryptoService = this.getService("CryptoService").resolve();
// Session Manager
(_c = this.configuration.services)["SessionManager"] ?? (_c["SessionManager"] = {
type: "Webda/CookieSessionManager"
});
if (this.configuration.services !== undefined) {
let excludes = ["Registry", "CryptoService"];
if (this.configuration.parameters.configurationService) {
excludes.push(this.configuration.parameters.configurationService);
}
// Do not recreate the configuration services
this.createServices(excludes);
}
this.router.remapRoutes();
this._initiated = true;
this.emit("Webda.Init", this.configuration);
}
/**
* Get a context based on the info
* @param info
* @returns
*/
async newContext(info, noInit = false) {
let context;
this._contextProviders.find(provider => (context = provider.getContext(info)) !== undefined);
if (!noInit) {
await context.init();
}
await this.emitSync("Webda.NewContext", { context, info });
return context;
}
/**
* Create a new context for a request
*
* @class Service
* @param httpContext THe HTTP request context
* @param stream - The request output stream if any
* @return A new context object to pass along
*/
async newWebContext(httpContext, stream = undefined, noInit = false) {
return await this.newContext({ http: httpContext, stream: stream });
}
/**
* 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 JSON.stringify(object, this.jsonFilter);
}
/**
* Return a UUID
*
* @param format to return different type of format
* Plan to implement base64 and maybe base85
*/
getUuid(format = "uuid") {
if (format === "uuid") {
return uuidv4().toString();
}
let buffer = Buffer.alloc(16);
uuidv4(undefined, buffer);
if (format === "base64") {
// Remove useless = we won't transfer back to original value or could just add ==
// https://datatracker.ietf.org/doc/html/rfc4648#page-7
return buffer.toString(format).replace(/=/g, "").replace(/\//g, "_").replace(/\+/g, "-");
}
return buffer.toString(format);
}
/**
* @override
*/
emit(eventType, event, ...data) {
return super.emit(eventType, event, ...data);
}
/**
* Emit the event with data and wait for Promise to finish if listener returned a Promise
*/
emitSync(eventType, event, ...data) {
let result;
let promises = [];
let listeners = this.listeners(eventType);
for (let listener of listeners) {
result = listener(event, ...data);
if (result instanceof Promise) {
promises.push(result);
}
}
return Promise.all(promises);
}
/**
* Type the listener part
* @param event
* @param listener
* @param queue
* @returns
*/
on(event, listener) {
super.on(event, listener);
return this;
}
/**
* Logs
* @param level
* @param args
*/
log(level, ...args) {
this.logger.log(level, ...args);
}
/**
* Retrieve a global parameter
*/
parameter(name) {
return this.getGlobalParams()[name];
}
/**
* Verify if a request can be done
*
* @param context Context of the request
*/
async checkRequest(ctx) {
// Do not need to filter on OPTIONS as CORS is for that
if (ctx.getHttpContext().getMethod() === "OPTIONS" || this._requestFilters.length === 0) {
return true;
}
return (await Promise.all(this._requestFilters.map(filter => filter.checkRequest(ctx, "AUTH")))).some(v => v);
}
/**
* Verify if an origin is allowed to do request on the API
*
* @param context Context of the request
*/
async checkCORSRequest(ctx) {
return (await Promise.all(this._requestCORSFilters.map(filter => filter.checkRequest(ctx, "CORS")))).some(v => v);
}
/**
* Export OpenAPI
* @param skipHidden
* @returns
*/
exportOpenAPI(skipHidden = true) {
var _a, _b, _c;
let packageInfo = this.application.getPackageDescription();
let contact;
if (typeof packageInfo.author === "string") {
contact = {
name: packageInfo.author
};
}
else if (packageInfo.author) {
contact = packageInfo.author;
}
let license;
if (typeof packageInfo.license === "string") {
license = {
name: packageInfo.license
};
}
else if (packageInfo.license) {
license = packageInfo.license;
}
let openapi = deepmerge({
openapi: "3.0.3",
info: {
description: packageInfo.description,
version: packageInfo.version || "0.0.0",
title: packageInfo.title || "Webda-based application",
termsOfService: packageInfo.termsOfService,
contact,
license
},
components: {
schemas: {
Object: {
type: "object"
}
}
},
paths: {},
tags: []
}, this.application.getConfiguration().openapi || {});
let models = this.application.getModels();
const schemas = this.application.getSchemas();
// Copy all input/output from actions
for (let i in schemas) {
if (!(i.endsWith(".input") || i.endsWith(".output"))) {
continue;
}
// @ts-ignore
(_a = openapi.components.schemas)[i] ?? (_a[i] = schemas[i]);
// Not sure how to test following
/* c8 ignore next 5 */
for (let j in schemas[i].definitions) {
// @ts-ignore
(_b = openapi.components.schemas)[j] ?? (_b[j] = schemas[i].definitions[j]);
}
}
for (let i in models) {
let model = models[i];
let desc = {
type: "object"
};
let modelName = model.name || i.split("/").pop();
let schema = this.application.getSchema(i);
if (schema) {
for (let j in schema.definitions) {
// @ts-ignore
(_c = openapi.components.schemas)[j] ?? (_c[j] = schema.definitions[j]);
}
delete schema.definitions;
desc = schema;
}
// Remove empty required as openapi does not like that
// Our compiler is not generating this anymore but it is additional protection
/* c8 ignore next 3 */
if (desc.required && desc.required.length === 0) {
delete desc.required;
}
// Remove $schema
delete desc.$schema;
// Rename all #/definitions/ by #/components/schemas/
openapi.components.schemas[modelName] = JSON.parse(JSON.stringify(desc).replace(/#\/definitions\//g, "#/components/schemas/"));
}
this.router.completeOpenAPI(openapi, skipHidden);
openapi.tags.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
let paths = {};
Object.keys(openapi.paths)
.sort()
.forEach(i => (paths[i] = openapi.paths[i]));
openapi.paths = paths;
return openapi;
}
/**
* Get a metric object
*
* Use the Service.getMetric method if possible
*
* This is map from prometheus 3 types of metrics
* Our hope is that we can adapt them to export to other
* metrics system if needed
*
* @param type
* @param configuration
* @returns
*/
getMetric(type, configuration) {
const metrics = this.getGlobalParams().metrics;
if (metrics === false) {
// Return a mock
return {
inc: () => { },
reset: () => { },
labels: () => undefined,
remove: () => { },
observe: () => { },
startTimer: () => {
return () => 0;
},
zero: () => { },
dec: () => { },
setToCurrentTime: () => { },
set: () => { }
};
}
const name = `${metrics.prefix}webda_${configuration.name}`;
const labelNames = [...(configuration.labelNames || []), ...Object.keys(metrics.labels)];
// Will probably need to override with a staticLabels property
return (register.getSingleMetric(name) ||
new type({
...configuration,
...metrics.config[configuration.name],
name,
labelNames
}));
}
}
export { Counter, Gauge, Histogram };
//# sourceMappingURL=core.js.map