UNPKG

@webda/core

Version:

Expose API with Lambda

741 lines 22.5 kB
import { WorkerOutput } from "@webda/workout"; import * as fs from "fs"; import * as path from "path"; import { CoreModel, WebdaError } from "./index.js"; import { getCommonJS } from "./utils/esm.js"; import { FileUtils } from "./utils/serializers.js"; const { __dirname } = getCommonJS(import.meta.url); export var SectionEnum; (function (SectionEnum) { SectionEnum["Moddas"] = "moddas"; SectionEnum["Deployers"] = "deployers"; SectionEnum["Beans"] = "beans"; })(SectionEnum || (SectionEnum = {})); /** * Map a Webda Application * * It allows to: * - Analyse imported modules * - Scan code for Modda and generate the webda.config.json * - Compile and Watch * - Migrate from old configuration * - List deployments * * * @category CoreFeatures */ export class Application { /** * * @param {string} fileOrFolder to load Webda Application from * @param {Logger} logger */ constructor(file, logger = undefined) { /** * Contains definitions of current application */ this.appModule = { moddas: {}, models: { list: {}, graph: {}, tree: {}, plurals: {}, shortIds: {}, reflections: {} }, deployers: {} }; /** * Contains already loaded modules */ this._loaded = []; /** * Deployers type registry */ this.deployers = {}; /** * Moddas registry */ this.moddas = {}; /** * Models type registry */ this.models = {}; /** * Models graph */ this.graph = {}; /** * Models specific plurals */ this.plurals = {}; /** * Detect if running as workspace */ this.workspacesPath = ""; /** * Direct parent of a model */ this.flatHierarchy = {}; this.logger = logger || new WorkerOutput(); this.initTime = Date.now(); if (!fs.existsSync(file)) { throw new WebdaError.CodeError("NO_WEBDA_FOLDER", `Not a webda application folder or webda.config.jsonc or webda.config.json file: unexisting ${file}`); } if (fs.lstatSync(file).isDirectory()) { file = path.join(file, "webda.config.jsonc"); if (!fs.existsSync(file)) { file = file.substring(0, file.length - 1); } } this.configurationFile = file; this.appPath = path.resolve(path.dirname(file)); } /** * Import all required modules */ async load() { this.loadConfiguration(this.configurationFile); await this.loadModule(this.baseConfiguration.cachedModules); // Flat the model tree const addParent = (parent, tree) => { for (let key in tree) { this.flatHierarchy[key] = parent; addParent(key, tree[key]); } }; addParent("Webda/CoreModel", this.baseConfiguration.cachedModules.models.tree); return this; } /** * Allow subclass to implement migration * * @param file * @returns */ loadConfiguration(file) { var _a, _b; // Check if file is a file or folder if (!fs.existsSync(file)) { throw new WebdaError.CodeError("NO_WEBDA_FOLDER", `Not a webda application folder or webda.config.jsonc or webda.config.json file: ${file}`); } try { this.baseConfiguration = FileUtils.load(file); (_a = this.baseConfiguration).parameters ?? (_a.parameters = {}); (_b = this.baseConfiguration.parameters).defaultStore ?? (_b.defaultStore = "Registry"); if (this.baseConfiguration.version !== 3) { this.log("ERROR", "Your configuration file should use version 3, see https://docs.webda.io/"); } } catch (err) { throw new WebdaError.CodeError("INVALID_WEBDA_CONFIG", `Cannot parse JSON of: ${file}`); } } /** * * @param proto Prototype to send */ getFullNameFromPrototype(proto) { for (let section in SectionEnum) { for (let i in this[SectionEnum[section]]) { if (this[SectionEnum[section]][i] && this[SectionEnum[section]][i].prototype === proto) { return i; } } } // Manage CoreModel too for (let i in this.models) { if (this.models[i].prototype === proto) { return i; } } } /** * Get a schema from a type * * Schema should be precomputed in the default app * @param type * @returns */ getSchema(type) { return this.baseConfiguration.cachedModules.schemas[type]; } /** * Get schemas * @returns */ getSchemas() { return this.baseConfiguration.cachedModules.schemas; } /** * Check if a schema exists * @param type * @returns schema name if it exists */ hasSchema(type) { return this.baseConfiguration.cachedModules.schemas[type] !== undefined; } /** * Get model graph */ getRelations(model) { const name = typeof model === "string" ? this.completeNamespace(model) : this.getModelName(model); // Get relations should not be case-sensitive until v4 const key = Object.keys(this.graph).find(k => k?.toLowerCase() === name?.toLowerCase()); return this.getGraph()[key] || {}; } /** * Get the all graph * @returns */ getGraph() { return this.graph; } /** * Check if application has cached modules * * When deployed the application contains cachedModules in the `webda.config.json` * It allows to avoid the search for `webda.module.json` inside node_modules and * take the schema from the cached modules also */ isCached() { return true; } /** * Retrieve specific webda conf from package.json * * In case of workspaces the object is combined */ getPackageWebda() { return (this.baseConfiguration.cachedModules.project?.webda || { namespace: "Webda" }); } /** * Retrieve content of package.json */ getPackageDescription() { return this.baseConfiguration.cachedModules?.project?.package || {}; } /** * Log information * * @param level to log for * @param args anything to display same as console.log */ log(level, ...args) { if (this.logger) { this.logger.log(level, ...args); } } /** * Get current logger */ getWorkerOutput() { return this.logger; } /** * Return the current app path * * @param subpath to append to */ getAppPath(subpath = undefined) { if (subpath && subpath !== "") { if (path.isAbsolute(subpath)) { return subpath; } return path.join(this.appPath, subpath); } return this.appPath; } /** * Add a new service * * @param name * @param service */ addService(name, service) { this.log("TRACE", "Registering service", name); this.moddas[name] = service; return this; } /** * Register a new schema in the application * @param name * @param schema */ registerSchema(name, schema) { if (this.hasSchema(name)) { throw new Error(`Schema ${name} already registered`); } this.baseConfiguration.cachedModules.schemas[name] = schema; } /** * Get plural of an Id * @param name * @returns */ getModelPlural(name) { let value = this.plurals[name] || name.split("/").pop(); return value.endsWith("s") ? value : value + "s"; } /** * * @param section * @param name * @param caseSensitive */ hasWebdaObject(section, name, caseSensitive = false) { let objectName = this.completeNamespace(name); this.log("TRACE", `Search for ${section} ${objectName}`); if (!this[section][objectName] && name.indexOf("/") === -1) { objectName = `Webda/${name}`; } if (!this[section][objectName]) { const caseInsensitive = Object.keys(this[section]).find(k => k.toLowerCase() === name.toLowerCase()); if (this[section][caseInsensitive] && !caseSensitive) { // We found a case insensitive match return true; } return false; } return true; } /** * * @param section * @param name * @returns */ getWebdaObject(section, name, caseSensitive = false) { let objectName = this.completeNamespace(name); this.log("TRACE", `Search for ${section} ${objectName}`); if (!this[section][objectName] && name.indexOf("/") === -1) { objectName = `Webda/${name}`; } if (!this[section][objectName]) { const caseInsensitive = Object.keys(this[section]).find(k => k.toLowerCase() === name.toLowerCase()); if (this[section][caseInsensitive] && !caseSensitive) { // We found a case insensitive match this.log("DEBUG", `Found ${caseInsensitive} instead of ${name}, will be removed in 4.0`); return this[section][caseInsensitive]; } objectName = name !== objectName ? ` or ${objectName}` : ""; throw Error(`Undefined ${section.substring(0, section.length - 1)} ${name}${objectName} (${Object.keys(this[section]).join(", ")})`); } return this[section][objectName]; } /** * Get a service based on name * * @param name */ getModda(name) { return this.getWebdaObject("moddas", name); } /** * Return all services of the application */ getModdas() { return this.moddas; } /** * Return all beans of the application */ getBeans() { return this.baseConfiguration.cachedModules.beans; } /** * Retrieve the model implementation * * @param name model to retrieve */ getModel(name) { return this.getWebdaObject("models", this.completeNamespace(name)); } /** * Get all models definitions */ getModels() { return this.models; } /** * Return models that do not have parents * @returns */ getRootModels() { return Object.keys(this.graph).filter(key => !this.graph[key].parent && this.models[key]?.Expose); } /** * Return models that do not have parents and are exposed * Or specifically set as root via the Expose.root parameter * @returns */ getRootExposedModels() { const results = new Set(this.getRootModels().filter(k => this.getModel(k).Expose)); for (let model in this.models) { if (this.models[model].Expose?.root) { results.add(model); } } return [...results]; } /** * Return the model name for a object * @param object */ getModelFromInstance(object) { return Object.keys(this.models).find(k => this.models[k] === object.constructor); } /** * Return the model name for a object * @param object */ getModelFromConstructor(model) { return Object.keys(this.models).find(k => this.models[k] === model); } /** * Get the model name from a model or a constructor * * @param model * @returns longId for a model */ getModelName(model) { // If __type is defined, use it if (model.__type) { return this.completeNamespace(model.__type); } if (model instanceof CoreModel) { return this.getModelFromInstance(model); } return this.getModelFromConstructor(model); } /** * Get the model hierarchy * @param model */ getModelHierarchy(model) { if (typeof model !== "string") { model = this.getModelName(model); } else { model = this.completeNamespace(model); } if (model === "Webda/CoreModel") { return { ancestors: [], children: this.baseConfiguration?.cachedModules?.models?.tree }; } let ancestors = []; let modelInfo = model; while ((this.flatHierarchy[modelInfo] || "Webda/CoreModel") !== "Webda/CoreModel") { modelInfo = this.flatHierarchy[modelInfo]; ancestors.unshift(modelInfo); } let tree = this.baseConfiguration?.cachedModules?.models?.tree || {}; ancestors.forEach(ancestor => { tree = tree[ancestor] || {}; }); ancestors.unshift(this.getShortId("Webda/CoreModel")); ancestors.reverse(); return { ancestors: ancestors.map(i => this.getShortId(i)), children: tree[model] || {} }; } /** * Get all model types with the hierarchy * @param model * @returns */ getModelTypes(model) { const hierarchy = this.getModelHierarchy(model); const coreModel = this.getShortId("Webda/CoreModel"); return [model.__type, ...hierarchy.ancestors] .map(i => (i.includes("/") ? this.getShortId(i) : i)) .filter(i => i !== coreModel); } /** * Return all deployers */ getDeployers() { return this.deployers; } /** * Add a new model * * @param name * @param model */ addModel(name, model, dynamic = true) { this.log("TRACE", "Registering model", name); this.models[name] = model; if (dynamic && model) { const superClass = Object.getPrototypeOf(model); Object.values(this.getModels()) .filter(m => m === superClass) .forEach(m => { this.flatHierarchy[name] = this.getModelName(m); this.getModelHierarchy(this.flatHierarchy[name]).children[name] = {}; }); } return this; } /** * Add a new deployer * * @param name * @param model */ addDeployer(name, model) { this.log("TRACE", "Registering deployer", name); this.deployers[name] = model; return this; } /** * Return webda current version * * @returns package version * @since 0.4.0 */ getWebdaVersion() { return JSON.parse(fs.readFileSync(__dirname + "/../package.json").toString()).version; } /** * Retrieve Git Repository information * * {@link GitInformation} for more details on how the information is gathered * @return the git information */ getGitInformation(_packageName, _version) { return this.baseConfiguration.cachedModules.project?.git; } /** * Allow variable inside of string * * @param templateString to copy * @param replacements additional replacements to run */ stringParameter(templateString, replacements = {}) { // Optimization if no parameter is found just skip the costy function if (templateString.indexOf("${") < 0) { return templateString; } let scan = templateString; let index; let i = 0; while ((index = scan.indexOf("${")) >= 0) { // Add escape sequence if (index > 0 && scan.substring(index - 1, 1) === "\\") { scan = scan.substring(scan.indexOf("}", index)); continue; } let next = scan.indexOf("}", index); let variable = scan.substring(index + 2, next); scan = scan.substring(next); if (variable.match(/[|&;<>\\{]/)) { throw new Error(`Variable cannot use every javascript features found ${variable}`); } if (i++ > 10) { throw new Error("Too many variables"); } } return new Function("return `" + (" " + templateString).replace(/([^\\])\$\{([^}{]+)}/g, "$1${this.$2}").substring(1) + "`;").call({ ...this.baseConfiguration.cachedModules.project, now: this.initTime, ...replacements }); } /** * Allow variable inside object strings * * Example * ```js * replaceVariables({ * myobj: "${test.replace}" * }, { * test: { * replace: 'plop' * } * }) * ``` * will return * ``` * { * myobj: 'plop' * } * ``` * * By default the replacements map contains * ``` * { * git: GitInformation, * package: 'package.json content', * deployment: string, * now: number, * ...replacements * } * ``` * * See: {@link GitInformation} * * @param object a duplicated object with replacement done * @param replacements additional replacements to run */ replaceVariables(object, replacements = {}) { if (typeof object === "string") { return this.stringParameter(object, replacements); } let app = this; return JSON.parse(JSON.stringify(object, function (key, value) { if (typeof this[key] === "string") { return app.stringParameter(value, replacements); } return value; })); } /** * Get current deployment name */ getCurrentDeployment() { return this.baseConfiguration.cachedModules.project.deployment.name; } /** * Return all application modules merged as one * * Used when deployed * @returns */ getModules() { return this.baseConfiguration.cachedModules; } /** * Get application configuration * @returns */ getConfiguration(_deployment = undefined) { return this.baseConfiguration; } /** * Return current Configuration of the Application * * Same as calling * * ```js * getConfiguration(this.currentDeployment); * ``` */ getCurrentConfiguration() { return this.getConfiguration(); } /** * Import a file * * If the `default` is set take this or use old format * * @param info */ async importFile(info, withExport = true) { if (info.startsWith(".")) { info = this.getAppPath(info); } try { this.log("TRACE", "Load file", info); let [importFilename, importName = "default"] = info.split(":"); if (!importFilename.endsWith(".js")) { importFilename += ".js"; } const importedFile = await import(importFilename); if (!withExport) { return; } const importObject = importedFile[importName]; if (!importObject) { this.log("WARN", `Module ${importFilename} does not have export named ${importName}`); } return importObject; } catch (err) { this.log("WARN", "Cannot resolve require", info, err.message); } } /** * Load local module */ async loadLocalModule() { let moduleFile = path.join(process.cwd(), "webda.module.json"); if (fs.existsSync(moduleFile)) { await this.loadModule(FileUtils.load(moduleFile), process.cwd()); } } /** * Load the module, * * @protected * @ignore Useless for documentation */ async loadModule(module, parent = this.appPath) { const info = { beans: {}, ...module }; const sectionLoader = async (section) => { var _a; for (let key in info[section]) { (_a = this[section])[key] ?? (_a[key] = await this.importFile(path.join(parent, info[section][key]))); } }; // Merging graph from different modules Object.keys(module.models.graph || {}).forEach(k => { this.graph[k] = module.models.graph[k]; }); // TODO Merging tree from different modules await Promise.all([ sectionLoader("moddas"), sectionLoader("deployers"), // Load models (async () => { // Copy plurals for (let key in info.models.plurals || {}) { this.plurals[key] = info.models.plurals[key]; } for (let key in info.models.list) { this.addModel(key, await this.importFile(path.join(parent, info.models.list[key])), false); } })(), ...Object.keys(info.beans).map(f => { this.baseConfiguration.cachedModules.beans[f] = info.beans[f]; return this.importFile(path.join(parent, info.beans[f]), false).catch(this.log.bind(this, "WARN")); }) ]); } /** * Return the full name including namespace * * In Webda the ServiceType include namespace `Webda/Store` or `Webda/Test` * This method will make sure the namespace is present, adding it if no '/' * is found in the name * * @param name */ completeNamespace(name = "") { // Do not add a namespace if already present if (name.includes("/")) { return name; } name = this.getShortId(name); if (name.includes("/")) { return name; } return `${this.getNamespace()}/${name}`; } /** * Return current namespace * @returns */ getNamespace() { return this.baseConfiguration?.cachedModules?.project?.webda?.namespace || "Webda"; } /** * Get short id for a name * @param name if name is shortId return longId else return shortId * @returns */ getShortId(name) { return this.baseConfiguration?.cachedModules?.models?.shortIds[name] || name; } } //# sourceMappingURL=application.js.map