UNPKG

@opengis/bi

Version:

BI data visualization module

228 lines (189 loc) 7.14 kB
import yaml from 'js-yaml'; import { config, autoIndex, pgClients, getSelect, getSelectVal, getFilterSQL, getMeta, logger, } from '@opengis/fastify-table/utils.js'; import chartSQL from './util/chartSQL.js'; import normalizeData from './util/normalizeData.js'; import { getWidget } from '../../../../utils.js'; const maxLimit = 100; export default async function dataAPI(req, reply) { const time = Date.now(); const { query = {}, user = {}, unittest } = req; query.metric = Array.isArray(query.metric) ? query.metric.pop() : query.metric; const { dashboard, widget, filter, search, samples } = query; const widgetData = await getWidget({ pg: req.pg, dashboard, widget }); if (widgetData.status) return widgetData; const { type, text, data = {}, controls, style, options } = widgetData; const pg = widgetData.pg || req.pg || pgClients.client; const { fields: cols } = await pg.query( `select * from ${data.table} t ${widgetData.tableSQL || data.tableSQL || ''} limit 0` ); const columnTypes = cols.map((el) => ({ name: el.name, type: pg.pgType?.[el.dataTypeID], })); // data param const { x, cls, groupbyCls, metric, table, where, tableSQL, groupby, xName, yName, xType, yType, error } = normalizeData(widgetData, query, columnTypes); if (error) { return reply.status(400).send(error); } // auto Index if (pg.pk?.[data.table]) { autoIndex({ table: data.table, pg, columns: [data?.time] .concat([xName]) .concat([groupby]) .filter((el) => el), }).catch((err) => console.log(err)); } const { pk, columns = [], view } = await getMeta({ pg, table: data.table }); if (!view && !pk) { return { message: `table not found: ${data.table} (${pg.options?.database})`, status: 404 }; } // const columnList = columns.map(col => col.name); const groupbyColumnNotExists = groupby?.split?.(',')?.filter?.(el => !columnTypes.map(el => el.name).includes(el.trim())); if (groupby && groupbyColumnNotExists?.length) { return { message: `groupby column not found: ${groupbyColumnNotExists} (${data.table}/${pg.options?.database})`, status: 404 }; } // get group const groupData = groupby ? await pg .query( `select ${groupby} as name ,count(*) from ${tableSQL || table} group by ${groupby} order by count(*) desc limit 20` ) .then((el) => el.rows) : null; if (query.sql === '2') return { x, metric, table, tableSQL, data, groupData }; const order = data.order || (type === 'listbar' && cols.find(el => el.name === 'metric') ? 'metric desc' : null); const fData = filter || search ? await getFilterSQL({ pg, table, filter, search, filterList: widgetData.filters, }) : {}; const optimizedSQL = widgetData?.sql ? `${widgetData.sql} ${fData?.q && false ? fData?.q : ''} limit ${Math.min(query.limit || widgetData.limit || maxLimit, maxLimit)}` : (fData?.optimizedSQL || `select * from ${tableSQL || table}`); if (type?.includes('bar') && !metric?.length) { return { message: 'empty widget params: metrics', status: 400 }; } const sql = widgetData.sql ? optimizedSQL : (chartSQL[type] || chartSQL.chart)({ where: config.local && user?.user_type === 'superadmin' ? 'true' : where, // test metric, yType, // metric type columns: widgetData.columns, table: `(${optimizedSQL})q`, x, groupData, groupby, order, samples, limit: Math.min(query.limit || maxLimit, maxLimit), xType, fx: widgetData.fx, }); if (query.sql) return sql; if (!sql || sql?.includes('undefined')) { return { message: { error: 'invalid sql', type, sql, where, metric, table: `(${optimizedSQL})q`, x, groupData, groupby, }, status: 500, }; } if (config.trace) console.log(sql, user?.uid); const { rows = [], fields = [], errorSql } = await pg.query(sql.replace('{{uid}}', user?.uid)).catch(err => { logger.file('bi/data', { error: err.toString(), sql }); return { errorSql: err.toString() }; }); // test with limit if (groupbyCls) { const { arr = [] } = await getSelect(groupbyCls, pg) || {}; if (arr.length) { const ids = arr.map(el => el.id); const text = arr.reduce((acc, curr) => ({ ...acc, [curr.id]: curr.text }), {}); rows.forEach(row => { ids.reduce((acc, curr) => { Object.assign(row, { [text[curr]]: row[curr] }); delete row[curr]; return acc; }, {}); }); } } if (cls) { const values = rows .map((row) => row[x]) ?.filter((el, idx, arr) => el && arr.indexOf(el) === idx); const vals = await getSelectVal({ pg, name: cls, values }); rows .filter((row) => row[x]) .forEach((row) => { Object.assign(row, { [x]: vals?.[row[x]]?.text || vals?.[row[x]] || row[x] }); }); } const metaTitles = columns.reduce((acc, curr) => Object.assign(acc, { [curr.name]: curr.title || curr.ua }), {}); const titles = Array.isArray(widgetData?.columns) ? widgetData.columns.reduce((acc, curr) => Object.assign(acc, { [curr.name]: curr.title || curr.ua }), {}) : Object.keys(widgetData?.columns || {}).reduce((acc, curr) => Object.assign(acc, { [curr]: widgetData?.columns?.[curr] }), {}); const rows1 = type === 'table' ? rows.map(row => Object.keys(row || {}).reduce((acc, curr) => Object.assign(acc, { [titles?.[curr] || metaTitles?.[curr] || curr]: row?.[curr] }), {})) : rows; const yml = widgetData.yml || yaml.dump(extractYml(widgetData)); const dimensions = fields.map((el) => el.name); const res = { time: Date.now() - time, error: errorSql || (!widgetData.sql ? widgetData.error : undefined), dimensions, filter: xName, dimensionsType: [xType, yType].filter((el) => el)?.length ? [xType, yType].filter((el) => el) : fields.map((el) => pg.pgType?.[el.dataTypeID]), type, text: text || widgetData?.title || data.text, // data: query.format === 'data' ? dimensions.map(el => rows.map(r => r[el])) : undefined, source: query.format === 'array' ? dimensions.map((el) => rows1.map((r) => r[el])) : rows1, style, options, controls, yml, data: widgetData.data, id: query.widget, columns: columnTypes.map(el => Object.assign(el, { title: titles[el.name] || metaTitles?.[el.name] || el.name })), params: config?.local || unittest ? { x, cls, metric, table, tableSQL, where, groupby, sql, } : undefined, }; return res; } function extractYml(sourceData) { const { title, description, type, data, style, controls } = sourceData; return { title, description, type, data, style, controls }; }