@webda/core
Version:
Expose API with Lambda
741 lines • 22.5 kB
JavaScript
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