/**
* State of Saltcorn
* Keeps cache for main objects
*/
const { contract, is } = require("contractis");
const {
is_plugin_wrap,
is_plugin,
is_header,
is_viewtemplate,
is_plugin_type,
is_plugin_layout,
} = require("../contracts");
const moment = require("moment");
const db = require(".");
const { migrate } = require("../migrate");
const File = require("../models/file");
const Trigger = require("../models/trigger");
const View = require("../models/view");
const { getAllTenants, createTenant } = require("../models/tenant");
const {
getAllConfigOrDefaults,
setConfig,
deleteConfig,
configTypes,
} = require("../models/config");
const emergency_layout = require("@saltcorn/markup/emergency_layout");
const { structuredClone } = require("../utils");
/**
* State class
*/
class State {
constructor() {
this.views = [];
this.triggers = [];
this.viewtemplates = {};
this.tables = [];
this.types = {};
this.files = {};
this.pages = [];
this.fields = [];
this.configs = {};
this.fileviews = {};
this.actions = {};
this.auth_methods = {};
this.plugins = {};
this.plugin_cfgs = {};
this.plugin_locations = {};
this.layouts = { emergency: { wrap: emergency_layout } };
this.headers = [];
this.function_context = { moment };
this.functions = { moment };
this.keyFieldviews = {};
this.external_tables = {};
contract.class(this);
}
/**
* Get Layout by user
* Based on role of user
* @param user
* @returns {unknown}
*/
getLayout(user) {
const role_id = user ? +user.role_id : 10;
const layout_by_role = this.getConfig("layout_by_role");
if (layout_by_role && layout_by_role[role_id]) {
const chosen = this.layouts[layout_by_role[role_id]];
if (chosen) return chosen;
}
const layoutvs = Object.values(this.layouts);
return layoutvs[layoutvs.length - 1];
}
/**
* Refresh State cache for all Saltcorn main objects
* @returns {Promise<void>}
*/
async refresh() {
await this.refresh_views();
await this.refresh_triggers();
await this.refresh_tables();
await this.refresh_files();
await this.refresh_pages();
this.configs = await getAllConfigOrDefaults();
}
/**
* Refresh views
* @returns {Promise<void>}
*/
async refresh_views() {
this.views = await View.find();
}
/**
* Refresh triggers
* @returns {Promise<void>}
*/
async refresh_triggers() {
this.triggers = await Trigger.findDB();
}
/**
* Refresh pages
* @returns {Promise<void>}
*/
async refresh_pages() {
const Page = require("../models/page");
this.pages = await Page.find();
}
/**
* Refresh files
* @returns {Promise<void>}
*/
// todo what will be if there are a lot of files? Yes, there are cache only ids of files.
async refresh_files() {
const allfiles = await File.find();
this.files = {};
for (const f of allfiles) {
this.files[f.id] = f;
}
}
/**
* Refresh tables & fields
* @returns {Promise<void>}
*/
async refresh_tables() {
const allTables = await db.select(
"_sc_tables",
{},
{ orderBy: "name", nocase: true }
);
const allFields = await db.select(
"_sc_fields",
{},
{ orderBy: "name", nocase: true }
);
for (const table of allTables) {
table.fields = allFields.filter((f) => f.table_id === table.id);
}
this.tables = allTables;
}
/**
* Get config parameter by key
* @param key - key of config paramter
* @param def - default value
* @returns {*}
*/
getConfig(key, def) {
const fixed = db.connectObj.fixed_configuration[key];
if (typeof fixed !== "undefined") return fixed;
if (db.connectObj.inherit_configuration.includes(key)) {
if (typeof singleton.configs[key] !== "undefined")
return singleton.configs[key].value;
else return def || configTypes[key].default;
}
if (this.configs[key] && typeof this.configs[key].value !== "undefined")
return this.configs[key].value;
if (def) return def;
else return configTypes[key] && configTypes[key].default;
}
/**
* Get copy of config parameter
* @param key - key of parameter
* @param def - default value
* @returns {any}
*/
getConfigCopy(key, def) {
return structuredClone(this.getConfig(key, def));
}
/**
*
* Set value of config parameter
* @param key - key of parameter
* @param value - value of parameter
* @returns {Promise<void>}
*/
async setConfig(key, value) {
if (
!this.configs[key] ||
typeof this.configs[key].value === "undefined" ||
this.configs[key].value !== value
) {
await setConfig(key, value);
this.configs[key] = { value };
}
}
/**
* Delete config parameter by key
* @param key - key of parameter
* @returns {Promise<void>}
*/
async deleteConfig(key) {
await deleteConfig(key);
delete this.configs[key];
}
/**
* Registre plugin
* @param name
* @param plugin
* @param cfg
* @param location
*/
registerPlugin(name, plugin, cfg, location) {
this.plugins[name] = plugin;
this.plugin_cfgs[name] = cfg;
this.plugin_locations[plugin.plugin_name || name] = location;
const withCfg = (key, def) =>
plugin.configuration_workflow
? plugin[key]
? plugin[key](cfg || {})
: def
: plugin[key] || def;
withCfg("types", []).forEach((t) => {
this.addType(t);
});
withCfg("viewtemplates", []).forEach((vt) => {
this.viewtemplates[vt.name] = vt;
});
Object.entries(withCfg("functions", {})).forEach(([k, v]) => {
this.functions[k] = v;
this.function_context[k] = typeof v === "function" ? v : v.run;
});
Object.entries(withCfg("fileviews", {})).forEach(([k, v]) => {
this.fileviews[k] = v;
});
Object.entries(withCfg("actions", {})).forEach(([k, v]) => {
this.actions[k] = v;
});
Object.entries(withCfg("authentication", {})).forEach(([k, v]) => {
this.auth_methods[k] = v;
});
Object.entries(withCfg("external_tables", {})).forEach(([k, v]) => {
if (!v.name) v.name = k;
this.external_tables[k] = v;
});
Object.entries(withCfg("fieldviews", {})).forEach(([k, v]) => {
if (v.type === "Key") {
this.keyFieldviews[k] = v;
return;
}
const type = this.types[v.type];
if (type) {
if (type.fieldviews) type.fieldviews[k] = v;
else type.fieldviews = { [k]: v };
}
});
const layout = withCfg("layout");
if (layout) {
this.layouts[name] = contract(is_plugin_layout, layout);
}
withCfg("headers", []).forEach((h) => {
if (!this.headers.includes(h)) this.headers.push(h);
});
}
/**
* Get type names
* @returns {string[]}
*/
get type_names() {
return Object.keys(this.types);
}
/**
* Add type
* @param t
*/
addType(t) {
this.types[t.name] = { ...t, fieldviews: { ...t.fieldviews } };
}
/**
* Remove plugin
* @param name
* @returns {Promise<void>}
*/
async remove_plugin(name) {
delete this.plugins[name];
await this.reload_plugins();
}
/**
* Reload plugins
* @returns {Promise<void>}
*/
async reload_plugins() {
this.viewtemplates = {};
this.types = {};
this.fields = [];
this.fileviews = {};
this.actions = {};
this.auth_methods = {};
this.layouts = { emergency: { wrap: emergency_layout } };
this.headers = [];
this.function_context = { moment };
this.functions = { moment };
this.keyFieldviews = {};
this.external_tables = {};
Object.entries(this.plugins).forEach(([k, v]) => {
this.registerPlugin(k, v, this.plugin_cfgs[k]);
});
await this.refresh();
}
}
/**
* State constract
* @type {{variables: {headers: ((function(*=): *)|*), types: ((function(*=): *)|*), viewtemplates: ((function(*=): *)|*)}, methods: {addType: ((function(*=): *)|*), registerPlugin: ((function(*=): *)|*), type_names: ((function(*=): *)|*), refresh: ((function(*=): *)|*)}}}
*/
State.contract = {
variables: {
headers: is.array(is_header),
viewtemplates: is.objVals(is_viewtemplate),
types: is.objVals(is_plugin_type),
},
methods: {
addType: is.fun(is_plugin_type, is.eq(undefined)),
registerPlugin: is.fun([is.str, is_plugin], is.eq(undefined)),
refresh: is.fun([], is.promise(is.eq(undefined))),
type_names: is.getter(is.array(is.str)),
},
};
// the state is singleton
const singleton = new State();
// return current State object
const getState = contract(
is.fun([], is.or(is.class("State"), is.eq(undefined))),
() => {
if (!db.is_it_multi_tenant()) return singleton;
const ten = db.getTenantSchema();
if (ten === db.connectObj.default_schema) return singleton;
else return tenants[ten];
}
);
// list of all tenants
var tenants = {};
// list of tenants with other domains
const otherdomaintenants = {};
/**
* Get other domain tenant
* @param hostname
*/
const get_other_domain_tenant = (hostname) => otherdomaintenants[hostname];
/**
* Get tenant
* @param ten
*/
const getTenant = (ten) => tenants[ten];
/**
* Remove protocol (http:// or https://) from domain url
* @param url
* @returns {*}
*/
const get_domain = (url) => {
const noproto = url.replace("https://", "").replace("http://", "");
return noproto.split("/")[0].split(":")[0];
};
/**
* Set tenant base url???
* From my point of view it just add tenant to list of otherdomaintenant
* @param tenant_subdomain
* @param value - new
*/
const set_tenant_base_url = (tenant_subdomain, value) => {
const root_domain = get_domain(singleton.configs.base_url.value);
if (value) {
const cfg_domain = get_domain(value);
if (!cfg_domain.includes("." + root_domain))
otherdomaintenants[cfg_domain] = tenant_subdomain;
}
};
/**
* Switch to multi_tenant
* @param plugin_loader
* @param disableMigrate - if true then dont migrate db
* @returns {Promise<void>}
*/
const init_multi_tenant = async (plugin_loader, disableMigrate) => {
const tenantList = await getAllTenants();
for (const domain of tenantList) {
try {
tenants[domain] = new State();
if (!disableMigrate)
await db.runWithTenant(domain, () => migrate(domain, true));
await db.runWithTenant(domain, plugin_loader);
set_tenant_base_url(domain, tenants[domain].configs.base_url.value);
} catch (err) {
console.error(
`init_multi_tenant error in domain ${domain}: `,
err.message
);
}
}
};
/**
* Create tenant
* @param t
* @param plugin_loader
* @param newurl
* @returns {Promise<void>}
*/
const create_tenant = async (t, plugin_loader, newurl) => {
await createTenant(t, newurl);
tenants[t] = new State();
await db.runWithTenant(t, plugin_loader);
};
/**
* Restart tenant
* @param plugin_loader
* @returns {Promise<void>}
*/
const restart_tenant = async (plugin_loader) => {
const ten = db.getTenantSchema();
tenants[ten] = new State();
await plugin_loader();
};
const process_init_time = new Date();
/**
* Get Process Init Time - moment when Saltcorn process was initiated
* @returns {Date}
*/
const get_process_init_time = () => process_init_time;
module.exports = {
getState,
getTenant,
init_multi_tenant,
create_tenant,
restart_tenant,
get_other_domain_tenant,
set_tenant_base_url,
get_process_init_time,
};