UNPKG

@opengis/fastify-table

Version:

core-plugins

413 lines (412 loc) 18.8 kB
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"); } }