UNPKG

@opengis/admin

Version:

This project Softpro Admin

270 lines (226 loc) 16 kB
import path from 'node:path'; import { createHash } from 'node:crypto'; import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { getTemplatePath, addHook, getToken, getTemplate, config, pgClients, initPG, getRedis, logger, getMenu, } from '@opengis/fastify-table/utils.js'; const { client } = pgClients; const rclient = getRedis(); const cwd = process.cwd(); const logDir = path.join(cwd, 'log/migration'); export default async function plugin(fastify) { const user1 = config?.auth?.disable || process.env.NODE_ENV !== 'admin' ? { uid: '1' } : null; await initPG(client); addHook('preForm', async ({ form, user }) => { if (!user?.uid) return null; const opt = await getToken({ mode: 'w', token: form, uid: user.uid, json: 1 }); return opt; }); fastify.addHook('onListen', async () => { const json = await getMenu({ user: { uid: 1 } }); // 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); } } }); fastify.addHook('onListen', async () => { const clsQuery = []; 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 || Object.hasOwn(loadTemplate?.[0] || {}, 'color'))) { 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 mkdirSync(logDir, { recursive: true }); writeFileSync(path.join(logDir, `${path.basename(cwd)}-${client.options?.database}-cls.sql`), clsQuery.filter((el) => el).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('admin/hook old cls deleted', rowCount); if (clsQuery.filter((el) => el).length) { console.log('admin/hook cls sql start', clsQuery?.length); await client.query(clsQuery.filter((el) => el).join(';')); await Promise.all(qHashes.filter(el => el).map(async (el) => rclient.hset('cls-insert-hashes', el, 1))); logger.file('migration/hash', { list: qHashes.filter(el => el) }); console.log('admin/hook cls sql finish', clsQuery?.length); } console.log('admin/hook cls promise finish', rowCount); } catch (err) { console.error('admin/hook cls sql error', err.toString()); console.trace(err); } }); addHook('afterTable', async ({ table, res = {}, payload: rows = [], user = {} }) => { const loadTable = await getTemplate('table', table); const { uid } = user1 || user; if (!uid || !table || !client?.pk?.[table] || !rows.length || !loadTable?.table) 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 (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) }); }); })); }); // extract table from form token for user columns - p.2 - read (refactor to global token) addHook('preTemplate', async ({ name, type, user = {} }) => { if (!name || !type) return; const { uid } = user1 || 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] }; }); addHook('afterTemplate', async ({ name, type, payload: data = {}, user = {} }) => { const { uid } = user1 || user; // extract table from form token for user columns - p.1 - assign (refactor to global token) if (!uid || !data || type !== 'form' || !name) 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 } }); })); }); addHook('afterUpdate', async ({ table, body = {}, payload: res = {}, user = {} }) => { const { uid } = user1 || user; if (!uid || !table || !Object.keys(body)?.length) 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) 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); }); addHook('afterInsert', async ({ table, body = {}, payload: res = {}, user = {} }) => { const { uid } = user1 || user; if (!uid || !table || !Object.keys(body)?.length) 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); }); }