@opengis/admin
Version:
This project Softpro Admin
270 lines (226 loc) • 15.9 kB
JavaScript
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) {
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,data)
select value->>'id',value->>'text','${name}',value->>'icon',value->>'data'
from json_array_elements('${JSON.stringify(loadTemplate).replace(/'/g, "''")}'::json)`);
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);
});
}