UNPKG

@webda/core

Version:

Expose API with Lambda

1,309 lines 43.1 kB
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