@opengis/fastify-table
Version:
core-plugins
413 lines (412 loc) • 18.8 kB
JavaScript
import path from "node:path";
import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, } from "node:fs";
import { createHash } from "node:crypto";
import { fileURLToPath } from "node:url";
import config from "./config.js";
import getTemplatePath from "./server/plugins/table/funcs/getTemplatePath.js";
import getToken from "./server/plugins/crud/funcs/getToken.js";
import getTemplate from "./server/plugins/table/funcs/getTemplate.js";
import getRedis from "./server/plugins/redis/funcs/getRedis.js";
import logger from "./server/plugins/logger/getLogger.js";
import getMenu from "./server/routes/menu/controllers/getMenu.js";
import getSelectVal from "./server/plugins/table/funcs/metaFormat/getSelectVal.js";
import locales from "./server/routes/table/controllers/utils/locales.js";
import pgClients from "./server/plugins/pg/pgClients.js";
const { client } = pgClients;
const rclient = getRedis();
// core templates && cls
const filename = fileURLToPath(import.meta.url);
const cwd = path.dirname(filename);
export async function onListen1() {
const json = await getMenu({ user: { uid: "1" }, pg: client }, null);
// insert interface list to db (user access management)
if (client?.pk?.["admin.routes"] && json?.menus?.length) {
const menuList = json?.menus?.filter?.((el) => el?.table ||
el?.component ||
el?.menu?.length /*&& el?.ua || el?.en || el?.name*/) || [];
// skip dupes
//admin_route_menu_id_fkey
menuList.forEach((el) => Object.assign(el, { ua: el?.ua || el?.en || el?.name }));
const uniqueList = menuList.filter((el, idx, arr) => el?.ua &&
arr.map((item) => item?.ua).indexOf(el?.ua) === idx);
const q = `insert into admin.menu(name, ord) values${uniqueList
.map((el, i) => `('${el?.ua.replace(/'/g, "''")}', ${i}) `)
.join(",")}
on conflict (name) do update set ord=excluded.ord, enabled=true returning name, menu_id`;
const { rows = [] } = uniqueList.length ? await client.query(q) : {};
const menus = rows.reduce((acc, curr) => Object.assign(acc, {
[curr.menu_id]: uniqueList.find((item) => item?.ua === curr.name),
}), {});
const values = Object.entries(menus).reduce((acc, curr) => {
if (curr[1]?.table || curr[1]?.component) {
acc.push({ ...curr[1], menuId: curr[0] });
}
curr[1]?.menu?.forEach?.((el) => acc.push({ ...el, menuId: curr[0] }));
return acc;
}, []);
await Promise.all(values
.filter((el) => el?.table)
.map(async (el) => {
const loadTable = await getTemplate("table", el.table);
Object.assign(el, {
table1: loadTable?.table || el.table,
actions: loadTable?.actions,
access: loadTable?.access,
});
}));
// console.log(values)
const q1 = `insert into admin.routes(route_id, alias, title, menu_id, table_name, actions, access, query)
values ${values
.filter((el) => el?.path)
.map((el) => `('${el.path}', ${el.table ? `'${el.table}'` : null},
${el.title || el.ua
? `'${(el.title || el.ua).replace(/'/g, "''")}'`
: null},
${el.menuId ? `'${el.menuId}'` : null}, ${el.table1 ? `'${el.table1}'` : null},
${el.actions?.length ? `'{ ${el.actions} }'::text[]` : null}, ${el.access ? `'${el.access}'` : null},
${el.query ? `'${el.query.replace(/'/g, "''")}'` : "'1=1'"})`)
.join(",")}
on conflict (route_id) do update set menu_id=excluded.menu_id, alias=excluded.alias, title=excluded.title, enabled=true, query=excluded.query,
table_name=excluded.table_name, actions=excluded.actions, access=excluded.access returning route_id, table_name`;
try {
console.log("admin/hook routes sql start");
const { rowCount: menuCount } = await client.query(`delete from admin.menu
where not array[menu_id] <@ $1::text[] and menu_id not in (select menu_id from admin.routes)`, [rows.map((el) => el.menu_id)]);
console.log("delete deprecated menus ok", menuCount);
const { rowCount: interfaceCount } = await client.query(`delete from admin.routes
where not array[route_id] <@ $1::text[] and route_id not in (select route_id from admin.role_access)`, [values.filter((el) => el?.path)]);
console.log("delete deprecated interfaces ok", interfaceCount);
const { rowCount } = values?.length ? await client.query(q1) : {};
console.log("insert interfaces ok", rowCount);
}
catch (err) {
console.log("admin/hook routes sql error", values, q1, err);
}
}
}
export async function onListen2() {
const clsQuery = [];
if (!client?.pk && client?.init) {
await client.init();
}
if (!client?.pk?.["admin.cls"])
return;
const selectList = getTemplatePath("select");
const clsList = getTemplatePath("cls")?.filter((el) => !(selectList?.map((el) => el?.[0]) || []).includes(el[0]));
const cls = (selectList || []).concat(clsList || [])?.map((el) => ({
name: el[0],
module: path.basename(path.dirname(path.dirname(el[1]))),
type: { json: "cls", sql: "select" }[el[2]],
}));
if (!cls?.length)
return;
try {
const hashes = await rclient
.hgetall("cls-insert-hashes")
.then((obj) => Object.keys(obj));
const dbdata = await client
.query(`select json_object_agg(name, hash) from admin.cls where parent is null`)
.then((el) => el.rows?.[0]?.json_object_agg || {});
const names = Object.keys(dbdata);
console.log("admin/hook cls promise start");
const qHashes = await Promise.all(cls
.filter((el, idx, arr) => arr.map((item) => item.name).indexOf(el.name) === idx)
.map(async (el) => {
const { name, module, type } = el;
const loadTemplate = await getTemplate(type, name);
el.hash = createHash("md5")
.update(type === "cls"
? JSON.stringify(loadTemplate)
: loadTemplate?.sql || loadTemplate || "")
.digest("hex");
el.dbhash = dbdata[name];
// check for changes by redis hash / dropped from db / changed at git project
el.update =
!hashes.includes(el.hash) ||
!names.includes(name) ||
el.hash !== el.dbhash;
if (type === "select" &&
(loadTemplate?.sql || loadTemplate) &&
el.update) {
clsQuery.push(`insert into admin.cls(name,type,data,module,hash) values('${name}','sql','${(loadTemplate?.sql || loadTemplate)?.replace(/'/g, "''")}', '${module?.replace(/'/g, "''")}','${el.hash}')`);
if (config.trace)
console.log(name, type, "insert fresh select");
return el.hash;
}
else if (type === "cls" && loadTemplate?.length && el.update) {
clsQuery.push(`insert into admin.cls(name,type, module,hash) values('${name}','json', '${module?.replace(/'/g, "''")}','${el.hash}');
insert into admin.cls(code,name,parent,icon,color,data)
select value->>'id',value->>'text','${name}',value->>'icon',value->>'color',value->>'data'
from json_array_elements('${JSON.stringify(loadTemplate).replace(/'/g, "''")}'::json) on conflict (code,parent) do update set color=excluded.color;`);
if (config.trace)
console.log(name, type, "insert fresh cls");
return el.hash;
}
else if (hashes.includes(el.hash)) {
if (config.trace)
console.log(name, type, names.includes(name)
? "skip equal hash"
: "insert missing cls");
return el.hash;
}
else {
if (config.trace)
console.log(name, type, "empty");
return el.hash;
}
}));
// debug
const logDir = path.join(cwd, "log/migration");
mkdirSync(logDir, { recursive: true });
writeFileSync(path.join(logDir, `${path.basename(cwd)}-${client.options?.database}-cls.sql`), clsQuery.filter(Boolean).join(";"));
writeFileSync(path.join(logDir, `${path.basename(cwd)}-${client.options?.database}-cls.json`), JSON.stringify(cls));
const { rowCount = 0 } = await client.query("delete from admin.cls where $1::text[] && array[name,parent]", [cls.filter((el) => el.update).map((el) => el.name)]);
console.log("fastify-table/hook old cls deleted", rowCount);
if (clsQuery.filter((el) => el).length) {
console.log("fastify-table cls sql start", clsQuery?.length);
await client.query(clsQuery.filter((el) => el).join(";"));
await Promise.all(qHashes
.filter(Boolean)
.map(async (el) => rclient.hset("cls-insert-hashes", el, 1)));
logger.file("migration/hash", { list: qHashes.filter(Boolean) });
console.log("fastify-table/hook cls sql finish", clsQuery?.length);
}
console.log("fastify-table/hook cls promise finish", rowCount);
}
catch (err) {
console.error("fastify-table/hook cls sql error", err.toString());
console.trace(err);
}
}
export async function preTemplate({ name, type, user = {}, }) {
if (!name || !type)
return;
const { uid } = config?.auth?.disable || process.env.NODE_ENV !== "admin"
? { uid: "1" }
: user;
const tokenData = (await getToken({
uid,
token: name,
mode: "w",
json: 1,
})) || // edit?
(await getToken({
uid,
token: name,
mode: "a",
json: 1,
})) ||
{}; // add?
return { name: tokenData?.[type] };
}
export async function preForm({ form, user }) {
if (!user?.uid)
return null;
const opt = await getToken({
mode: "w",
token: form,
uid: user.uid,
json: 1,
});
return opt;
}
export async function afterTable({ table, res = {}, payload: rows = [], user = {}, }) {
const loadTable = await getTemplate("table", table);
const { uid } = config?.auth?.disable || process.env.NODE_ENV !== "admin"
? { uid: "1" }
: user;
if (!uid ||
!table ||
!client?.pk?.[table] ||
!rows.length ||
!loadTable?.table ||
!client?.pk?.["crm.extra_data"] ||
!client?.pk?.["admin.custom_column"])
return;
// admin.custom_column - user column data
const { rows: properties = [] } = await client.query(`select column_id, name, title, format, data from admin.custom_column
where _table and entity=$1 and uid=$2`, [table, uid]);
const extraColumnList = properties.map((row) => ({
id: row.column_id,
name: row.name,
title: row.title,
format: row.format,
data: row.data,
}));
if (!extraColumnList?.length)
return;
if (Array.isArray(res?.columns) && res?.columns?.length) {
extraColumnList.forEach((col) => res.columns.push(col));
}
const { rows: extraData = [] } = await client.query(`select object_id, json_object_agg( property_id, coalesce(value_date::text,value_text) ) as extra from crm.extra_data
where property_entity=$1 and property_id=any($2) and object_id=any($3) group by object_id`, [
table,
extraColumnList?.map((el) => el.id),
rows.map((el) => el.id),
]);
if (!extraData?.length) {
// Object.assign(rows?.[0] || {}, { ...extraColumnList.reduce((acc, curr) => Object.assign(acc, { [curr.name]: null }), {}) });
return;
}
rows
.filter((row) => extraData.map((el) => el?.object_id).includes(row.id))
.forEach((row) => {
const { extra = {} } = extraData.find((el) => el.object_id === row.id);
Object.assign(row, {
...Object.fromEntries(Object.entries(extra).map((el) => [
extraColumnList.find((col) => col.id === el[0])
.name,
el[1],
])),
});
});
// admin.custom_column - metaFormat
await Promise.all(extraColumnList
.filter((el) => el?.data)
.map(async (attr) => {
const values = [
...new Set(rows?.map((el) => el[attr.name]).flat()),
].filter((el) => el);
if (!values.length)
return;
const cls = await getSelectVal({ name: attr.data, values });
if (!cls)
return;
rows.forEach((el) => {
const val = el[attr.name]?.map?.((c) => cls[c] || c) ||
cls[el[attr.name]] ||
el[attr.name];
if (!val)
return;
Object.assign(el, {
[val?.color ? `${attr.name}_data` : `${attr.name}_text`]: val.color
? val
: val.text || val,
});
});
}));
}
export async function afterTemplate({ name, type, payload: data = {}, user = {}, }) {
const { uid } = config?.auth?.disable || process.env.NODE_ENV !== "admin"
? { uid: "1" }
: user;
// extract table from form token for user columns - p.1 - assign (refactor to global token)
if (!uid ||
!data ||
type !== "form" ||
!name ||
!client?.pk?.["admin.custom_column"])
return null;
const { form, id, table } = (await getToken({
uid,
token: name,
mode: "w",
json: 1,
})) || // edit?
(await getToken({
uid,
token: name,
mode: "a",
json: 1,
})) ||
{}; // add?
const { rows: properties = [] } = await client.query(`select name, title, format, data from admin.custom_column
where entity=$1 and uid=$2`, [table || name, uid]);
await Promise.all(properties.map(async (el) => {
const clsData = el.data
? await getTemplate(["cls", "select"], el.data)
: undefined;
const type = clsData
? "Select"
: { date: "DatePicker" }[el.format || ""] || "Text";
Object.assign(data?.schema || data || {}, {
[el.name]: {
type,
ua: el.title,
data: el.data,
options: type === "Select" && Array.isArray(clsData) && clsData?.length
? clsData
: undefined,
extra: 1,
},
});
}));
}
export async function afterUpdate({ table, body = {}, payload: res = {}, user = {}, }) {
const { uid } = config?.auth?.disable || process.env.NODE_ENV !== "admin"
? { uid: "1" }
: user;
if (!uid ||
!table ||
!Object.keys(body)?.length ||
!client?.pk?.["crm.extra_data"] ||
!client?.pk?.["admin.custom_column"])
return null;
const loadTable = await getTemplate("table", table);
if (!client?.pk?.[loadTable?.table || table])
return null;
const pk = client?.pk?.[loadTable?.table || table];
const id = res[pk];
const { rows: properties = [] } = await client.query(`select column_id, name, title, format, data from admin.custom_column
where entity=$1 and uid=$2`, [table, uid]);
if (!id || !properties?.length || !client.pk?.["crm.extra_data"])
return null;
const q = `delete from crm.extra_data where property_entity='${table}' and object_id='${id}';${properties
.filter((el) => Object.keys(body).includes(el.name))
.map((el) => `insert into crm.extra_data(property_id,property_key,property_entity,object_id,${el.format?.toLowerCase() === "date" ? "value_date" : "value_text"})
select '${el.column_id}', '${el.name}', '${table}', '${id}', ${el.format?.toLowerCase() === "date"
? `'${body[el.name]}'::timestamp without time zone`
: `'${body[el.name]}'::text`}`)
.join(";\n") || ""}`;
return client.query(q);
}
export async function afterInsert({ table, body, payload: res = {}, user = {}, }) {
const { uid } = config?.auth?.disable || process.env.NODE_ENV !== "admin"
? { uid: "1" }
: user;
if (!uid ||
!table ||
!Object.keys(body)?.length ||
!client?.pk?.["crm.extra_data"] ||
!client?.pk?.["admin.custom_column"])
return null;
const loadTable = await getTemplate("table", table);
if (!client?.pk?.[loadTable?.table || table])
return null;
const pk = client?.pk?.[loadTable?.table || table];
const id = res.rows?.[0]?.[pk];
const { rows: properties = [] } = await client.query(`select column_id, name, title, format, data from admin.custom_column
where entity=$1 and uid=$2`, [table, uid]);
if (!id || !properties?.length)
return null;
const q = properties
.filter((el) => Object.keys(body).includes(el.name))
.map((el) => `insert into crm.extra_data(property_id,property_key,property_entity,object_id,${el.format?.toLowerCase() === "date" ? "value_date" : "value_text"})
select '${el.column_id}', '${el.name}', '${table}', '${id}', ${el.format?.toLowerCase() === "date"
? `'${body[el.name]}'::timestamp without time zone`
: `'${body[el.name]}'::text`}`)
.join(";\n");
return client.query(q);
}
export async function onReady() {
if (existsSync("locales")) {
const subdirs = readdirSync("locales", { withFileTypes: true }).filter((el) => el.isDirectory());
const res = subdirs.reduce((acc, curr) => {
const content = readdirSync(`locales/${curr.name}`, {
withFileTypes: true,
});
const obj = content
.filter((el) => el.isFile())
.reduce((acc1, file) => ({
...acc1,
...JSON.parse(readFileSync(`locales/${curr.name}/${file.name}`, "utf-8").replace(/[\u200B-\u200D\uFEFF]/g, "")),
}), {});
Object.keys(obj).reduce((acc1, curr1) => Object.assign(acc, {
[curr1]: { ...acc[curr1], [curr.name]: obj?.[curr1] },
}), {});
return acc;
}, {});
Object.assign(locales, res);
console.log("locales loaded");
}
}