@opengis/bi 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +37 -7
  2. package/dist/favicon.ico +0 -0
  3. package/dist/settings.js +48553 -0
  4. package/dist/settings.umd.cjs +170 -0
  5. package/dist/style.css +1 -0
  6. package/index.js +8 -2
  7. package/package.json +44 -5
  8. package/plugin.js +10 -0
  9. package/server/plugins/docs.js +47 -0
  10. package/server/plugins/vite.js +49 -0
  11. package/server/routes/dashboard/controllers/dashboard.delete.js +35 -0
  12. package/server/routes/dashboard/controllers/dashboard.js +60 -0
  13. package/server/routes/dashboard/controllers/dashboard.list.js +39 -0
  14. package/server/routes/dashboard/index.mjs +23 -0
  15. package/server/routes/data/controllers/data.js +100 -0
  16. package/server/routes/data/controllers/util/chartSQL.js +24 -0
  17. package/server/routes/data/controllers/util/normalizeData.js +21 -0
  18. package/server/routes/data/index.mjs +26 -0
  19. package/server/routes/edit/controllers/dashboard.add.js +19 -0
  20. package/server/routes/edit/controllers/dashboard.edit.js +33 -0
  21. package/server/routes/edit/controllers/widget.add.js +33 -0
  22. package/server/routes/edit/controllers/widget.del.js +63 -0
  23. package/server/routes/edit/controllers/widget.edit.js +79 -0
  24. package/server/routes/edit/index.mjs +27 -0
  25. package/server/routes/map/controllers/vtile.js +167 -0
  26. package/server/routes/map/index.mjs +32 -0
  27. package/server/templates/dashboard/erobota/bar_area.yml +17 -0
  28. package/server/templates/dashboard/erobota/bar_culture.yml +18 -0
  29. package/server/templates/dashboard/erobota/bar_grand.yml +18 -0
  30. package/server/templates/dashboard/erobota/count_grand.yml +8 -0
  31. package/server/templates/dashboard/erobota/index.yml +47 -0
  32. package/server/templates/dashboard/erobota/list_culture.yml +12 -0
  33. package/server/templates/dashboard/erobota/list_grant.yml +12 -0
  34. package/server/templates/dashboard/erobota/map.yml +4 -0
  35. package/server/templates/dashboard/erobota/pie_area.yml +17 -0
  36. package/server/templates/dashboard/erobota/pie_grant.yml +17 -0
  37. package/server/templates/dashboard/erobota/total_area.yml +9 -0
  38. package/server/templates/dashboard/erobota/total_grand.yml +9 -0
  39. package/server/templates/dashboard/sales/index.yml +40 -0
  40. package/server/templates/dashboard/sales/quarterly_revenue.yml +18 -0
  41. package/server/templates/dashboard/sales/quarterly_revenue_by_product_line.yml +19 -0
  42. package/server/templates/dashboard/sales/total_products_sold.yml +9 -0
  43. package/server/templates/dashboard/sales/total_products_sold_by_product_line.yml +12 -0
  44. package/server/templates/dashboard/sales/total_revenue.yml +8 -0
  45. package/server/templates/dashboard/sales/total_revenue_by_product_line.yml +17 -0
  46. package/server/templates/dashboard/sales/vehicle_sales_info.md +17 -0
  47. package/server/templates/dashboard/test3/index.yml +29 -0
  48. package/server/templates/dashboard/test3/quarterly_revenue.yml +19 -0
  49. package/server/templates/dashboard/test3/widget1.yml +8 -0
  50. package/server/templates/pt/vehicle_sales.md +17 -0
  51. package/server/templates/table/demo.cleaned_sales_data.table.json +104 -0
  52. package/server/templates/table/test.dataset.table.json +16 -0
  53. package/server/templates/widget/product_line.yml +20 -0
  54. package/server/templates/widget/test_vtile.yml +7 -0
  55. package/.eslintrc.cjs +0 -42
  56. package/.prettierrc.json +0 -9
  57. package/config.example +0 -21
  58. package/dashboard/controllers/dashboard.js +0 -22
  59. package/dashboard/controllers/data.js +0 -108
  60. package/dashboard/controllers/vtile.js +0 -3
  61. package/dashboard/index.mjs +0 -35
  62. package/helper.js +0 -34
  63. package/server.js +0 -14
  64. package/test/config.example +0 -18
  65. package/test/config.js +0 -19
  66. package/test/plugins/bi.test.js +0 -76
  67. package/test/templates/chart/bar.zone.yml +0 -23
  68. package/test/templates/chart/histogram.yml +0 -14
  69. package/test/templates/chart/line.yml +0 -16
  70. package/test/templates/chart/pivot.yml +0 -11
  71. package/test/templates/chart/table.yml +0 -15
  72. package/test/templates/chart/test.map.yml +0 -28
  73. package/test/templates/chart/user.yml +0 -7
  74. package/test/templates/dashboard/dashboard1.yml +0 -11
  75. /package/{dashboard → server/routes/dashboard}/controllers/utils/yaml.js +0 -0
  76. /package/{dashboard → server/routes/map}/controllers/geojson.js +0 -0
package/dist/style.css ADDED
@@ -0,0 +1 @@
1
+ .list-bar[data-v-07e206e1]{font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.vs-chart{width:100%;height:100%}.vs-chart-tooltip{border-radius:7px;max-height:100px;max-width:340px;min-width:140px;background-color:#fff;box-shadow:0 0 8px #0000002e}.vs-chart-tooltip__head{padding:5px;border-bottom:1px solid #eee;margin-bottom:5px}.vs-chart-tooltip__head-title{font-size:14px;font-weight:700;display:flex;align-items:center}.vs-chart-tooltip__head-value{margin-right:5px}.vs-chart-tooltip__head-series{font-size:14px;display:flex;align-items:center}.vs-chart-tooltip__color{width:12px;height:12px;display:block;border-radius:50%;margin-right:5px}.vs-chart-tooltip__body{padding:5px;font-size:14px;display:flex;align-items:start;flex-direction:column}.vs-chart-tooltip__body .vs-chart-tooltip__color{border-radius:3px}.vs-chart-tooltip__item{width:100%;display:flex;align-items:center;justify-content:space-between}.vs-chart-tooltip__text{margin-right:auto}.vs-chart-tooltip__body svg{margin-right:8px}
package/index.js CHANGED
@@ -1,7 +1,10 @@
1
1
  import fp from 'fastify-plugin';
2
2
  import config from './config.js';
3
3
 
4
- import biPlugin from './dashboard/index.mjs';
4
+ import biDashboard from './server/routes/dashboard/index.mjs';
5
+ import biData from './server/routes/data/index.mjs';
6
+ import biEdit from './server/routes/edit/index.mjs';
7
+ import biMap from './server/routes/map/index.mjs';
5
8
 
6
9
  async function plugin(fastify, opt) {
7
10
  config.folder = opt.folder;
@@ -23,6 +26,9 @@ async function plugin(fastify, opt) {
23
26
  });
24
27
  }
25
28
 
26
- biPlugin(fastify); // { prefix: config.prefix }
29
+ biDashboard(fastify); // { prefix: config.prefix }
30
+ biData(fastify); // { prefix: config.prefix }
31
+ biEdit(fastify); // { prefix: config.prefix }
32
+ biMap(fastify); // { prefix: config.prefix }
27
33
  }
28
34
  export default fp(plugin);
package/package.json CHANGED
@@ -1,24 +1,63 @@
1
1
  {
2
2
  "name": "@opengis/bi",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "BI data visualization module",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
+ "files": [
8
+ "dist/*",
9
+ "server/*",
10
+ "plugin.js",
11
+ "config.js"
12
+ ],
7
13
  "scripts": {
14
+ "debug": "node --watch-path=server server",
15
+ "dev": "vite",
16
+ "build": "vite build",
17
+ "build-app": "cross-env APP=true vite build",
8
18
  "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
9
- "test": "node --test"
19
+ "test": "node --test",
20
+ "test21": "node --test ./test/plugins/*",
21
+ "start": "node --watch-path=server server.js",
22
+ "prod": "cross-env NODE_ENV=production npm run start",
23
+ "docs:local": "cross-env docs=local vitepress build docs",
24
+ "docs:dev": "vitepress dev docs",
25
+ "docs:build": "vitepress build docs",
26
+ "docs:preview": "vitepress preview docs",
27
+ "docs-dev:local": "cross-env docs=local vitepress build docs-dev",
28
+ "docs-dev:dev": "vitepress dev docs-dev",
29
+ "docs-dev:build": "vitepress build docs-dev",
30
+ "docs-dev:preview": "vitepress preview docs-dev"
10
31
  },
11
32
  "keywords": [],
12
33
  "author": "Softpro",
13
34
  "license": "ISC",
14
35
  "dependencies": {
36
+ "@mapbox/sphericalmercator": "^1.2.0",
37
+ "@opengis/fastify-table": "^1.1.7",
38
+ "@opengis/v3-core": "^0.1.94",
39
+ "axios": "^1.3.1",
40
+ "cross-env": "^7.0.3",
41
+ "d3-format": "^3.1.0",
42
+ "echarts": "^5.5.1",
15
43
  "fastify": "^4.26.1",
16
44
  "fastify-plugin": "^4.0.0",
17
- "@opengis/fastify-table": "^1.0.36",
18
- "js-yaml": "^4.1.0"
45
+ "js-yaml": "^4.1.0",
46
+ "marked": "^14.1.2",
47
+ "vite": "^5.1.5",
48
+ "vue": "^3.4.27",
49
+ "vue-router": "^4.4.3"
19
50
  },
20
51
  "devDependencies": {
21
52
  "eslint": "^8.49.0",
22
- "eslint-config-airbnb": "^19.0.4"
53
+ "eslint-config-airbnb": "^19.0.4",
54
+ "eslint-plugin-vue": "^9.17.0",
55
+ "prettier": "^3.0.3",
56
+ "sass": "^1.77.0",
57
+ "typescript": "~5.4.0",
58
+ "vitepress": "^1.1.4",
59
+ "vitepress-plugin-mermaid": "^2.0.16",
60
+ "vitepress-plugin-tabs": "^0.5.0",
61
+ "vitepress-sidebar": "^1.22.0"
23
62
  }
24
63
  }
package/plugin.js ADDED
@@ -0,0 +1,10 @@
1
+ import config from './config.js';
2
+ config.prefix = config.prefix || '/api'
3
+ export default async function (fastify, opts) {
4
+
5
+ // API
6
+ fastify.register(import('./server/routes/dashboard/index.mjs'), config);
7
+ fastify.register(import('./server/routes/data/index.mjs'), config);
8
+ fastify.register(import('./server/routes/edit/index.mjs'), config);
9
+
10
+ }
@@ -0,0 +1,47 @@
1
+ 'use strict'
2
+
3
+ import path, { dirname } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import fs from 'fs';
6
+
7
+ const dir = dirname(fileURLToPath(import.meta.url));
8
+ const root = `${dir}/../../`;
9
+
10
+ async function plugin(fastify, opts) {
11
+ fastify.get('/docs-dev*', async (req, reply) => {
12
+ if (!fs.existsSync(path.join(root, 'docs-dev/.vitepress/dist/'))) {
13
+ return reply.status(404).send('docs not exists');
14
+ }
15
+
16
+ const { params } = req;
17
+ const url = params['*'];
18
+
19
+ const filePath = url && url[url.length - 1] !== '/' ? path.join(root, 'docs-dev/.vitepress/dist/', url) : path.join(root, 'docs-dev/.vitepress/dist/', url, 'index.html');
20
+
21
+ if (!fs.existsSync(filePath)) {
22
+ return reply.status(404).send('File not found');
23
+ }
24
+
25
+ const ext = path.extname(filePath);
26
+ const mime = {
27
+ '.js': 'text/javascript',
28
+ '.css': 'text/css',
29
+ '.woff2': 'application/font-woff',
30
+ '.png': 'image/png',
31
+ '.svg': 'image/svg+xml',
32
+ '.jpg': 'image/jpg',
33
+ '.html': 'text/html',
34
+ '.json': 'application/json',
35
+ '.pdf': 'application/pdf'
36
+ }[ext];
37
+
38
+ const stream = fs.createReadStream(filePath);
39
+ stream.on('error', (err) => {
40
+ reply.status(500).send('Error reading file');
41
+ });
42
+
43
+ return mime ? reply.type(mime).send(stream) : reply.send(stream);
44
+ });
45
+ }
46
+
47
+ export default plugin;
@@ -0,0 +1,49 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ const isProduction = process.env.NODE_ENV === 'production';
5
+
6
+ async function plugin(fastify) {
7
+ // vite server
8
+ if (!isProduction) {
9
+ const vite = await import('vite');
10
+ const viteDevMiddleware = (
11
+ await vite.createServer({
12
+ server: {
13
+ middlewareMode: true,
14
+ },
15
+ })
16
+ ).middlewares;
17
+
18
+ // this is middleware for vite's dev servert
19
+ fastify.addHook('onRequest', async (req, reply) => {
20
+ const { user } = req.session?.passport || {};
21
+
22
+
23
+ const next = () => new Promise((resolve) => {
24
+ viteDevMiddleware(req.raw, reply.raw, () => resolve());
25
+ });
26
+ await next();
27
+ });
28
+ fastify.get('*', async () => { });
29
+ return;
30
+ }
31
+
32
+ // From Build
33
+ fastify.get('*', async (req, reply) => {
34
+
35
+ const stream = fs.createReadStream('dist/index.html');
36
+ return reply.type('text/html').send(stream);
37
+ });
38
+ fastify.get('/assets/:file', async (req, reply) => {
39
+ const stream = fs.createReadStream(`dist/assets/${req.params.file}`);
40
+ const ext = path.extname(req.params.file);
41
+ const mime = {
42
+ '.js': 'text/javascript', '.css': 'text/css', '.woff2': 'application/font-woff', '.png': 'image/png',
43
+ }[ext];
44
+ //reply.cacheControl('max-age', '1d');
45
+ return reply.headers({ 'Cache-Control': 'public, max-age=3600' }).type(mime).send(stream);
46
+ });
47
+ }
48
+
49
+ export default plugin;
@@ -0,0 +1,35 @@
1
+ import { existsSync, readdirSync } from 'fs';
2
+ import path from 'path';
3
+ import pgClients from '@opengis/fastify-table/pg/pgClients.js';
4
+
5
+ const cwd = process.cwd();
6
+ const dashboardDir = path.join(cwd, 'server/templates/dashboard');
7
+
8
+ export default async function data({ pg = pgClients.client, params = {} }) {
9
+ const { id } = params;
10
+
11
+ if (!id) {
12
+ return { message: 'not enough params: id', status: 400 };
13
+ }
14
+
15
+ const dirContent = existsSync(dashboardDir) ? readdirSync(dashboardDir) : [];
16
+
17
+ if (dirContent.includes(id)) {
18
+ return { message: 'access restricted: ' + id, status: 403 };
19
+ }
20
+ try {
21
+ const { rowCount } = await pg.query(
22
+ `select * from bi.dashboard where $1 in (dashboard_id,name)`,
23
+ [id]
24
+ );
25
+
26
+ if (rowCount === 0) {
27
+ return { message: 'not found ' + id, status: 404 };
28
+ }
29
+ await pg.query(`delete from bi.widget where $1 in (dashboard_id)`, [id]);
30
+ await pg.query(`delete from bi.dashboard where $1 in (dashboard_id,name)`, [id]);
31
+ return { message: 'successfully deleted', status: 200 };
32
+ } catch (err) {
33
+ return { error: err.toString(), status: 500 };
34
+ }
35
+ }
@@ -0,0 +1,60 @@
1
+ import path from 'path';
2
+ import { existsSync, readFileSync, readdirSync } from 'fs';
3
+
4
+ import pgClients from '@opengis/fastify-table/pg/pgClients.js';
5
+ import getTemplatePath from '@opengis/fastify-table/table/controllers/utils/getTemplatePath.js';
6
+ import { getTemplate } from '@opengis/fastify-table/utils.js';
7
+
8
+
9
+ export default async function data({
10
+ pg = pgClients.client, params = {},
11
+ }) {
12
+ const time = Date.now();
13
+ const { id } = params;
14
+
15
+ if (!id) {
16
+ return { message: 'not enough params: dashboard required', status: 400 };
17
+ }
18
+ const dashboards = await getTemplatePath('dashboard');
19
+
20
+ const fileDashboard = dashboards.find(el => el[0] === id)
21
+ if (!fileDashboard) {
22
+ const sql = `select title, description, table_name, panels, grid, widgets, filters, style
23
+ from bi.dashboard where $1 in (dashboard_id, name)`;
24
+
25
+ const data = await pg.query(sql, [id]).then((res) => res.rows?.[0] || {});
26
+ data.type = 'bd';
27
+ const { table_name: table } = data;
28
+
29
+ if (!table) {
30
+ return { message: 'not enough params: table required', status: 400 };
31
+ }
32
+
33
+ const { fields = [] } = table && pg.pk?.[table] ? await pg.query(`select * from ${table} limit 1`) : {};
34
+
35
+ const columns = table ? fields.map(({ name, dataTypeID }) => ({ name, type: pg.pgType?.[dataTypeID] })) : [];
36
+ return { ...data, error: table && !pg.pk?.[table] ? `table pkey not found: ${table}` : undefined, table_name: table, time: Date.now() - time, columns };
37
+ }
38
+
39
+
40
+
41
+ const fileData = await getTemplate('dashboard', id);
42
+ const index = fileData.find(el => el[0] === 'index.yml')[1]
43
+
44
+ if (!index) {
45
+ return { message: 'not found ' + id, status: 404 };
46
+ }
47
+
48
+
49
+ const data = index;
50
+ data.type = 'file';
51
+ const { table } = data?.data || { table: data?.table_name };
52
+ // console.log(fileData)
53
+ const widgets = fileData.filter(el => el[0] !== 'index.yml').map(el => el[1]).map(el => el.data ? ({ name: el.name, type: el.type, text: el.text, style: el.style, data: el.data }) : { text: el });
54
+
55
+ const { fields = [] } = table ? await pg.query(`select * from ${table} limit 1`) : {};
56
+
57
+ const columns = fields.map(({ name, dataTypeID }) => ({ name, type: pg.pgType?.[dataTypeID] }));
58
+
59
+ return { ...data, table_name: table, time: Date.now() - time, columns, widgets };
60
+ }
@@ -0,0 +1,39 @@
1
+ import { readFileSync, readdirSync } from 'fs';
2
+ import path from 'path';
3
+ import pgClients from '@opengis/fastify-table/pg/pgClients.js';
4
+ import { getTemplate } from '@opengis/fastify-table/utils.js';
5
+ import getTemplatePath from '@opengis/fastify-table/table/controllers/utils/getTemplatePath.js';
6
+
7
+ import yaml from './utils/yaml.js';
8
+ //import getTemplate from '@opengis/fastify-table/table/controllers/utils/getTemplate.js';
9
+
10
+
11
+ const cwd = process.cwd();
12
+
13
+
14
+ const q = `select dashboard_id as name, 'db' as type, title, description, table_name from bi.dashboard`;
15
+
16
+ export default async function data({ pg = pgClients.client }) {
17
+ const time = Date.now();
18
+ const data = await getTemplatePath('dashboard');
19
+ const dir = await Promise.all(
20
+ data.map(async ([filename]) => {
21
+
22
+ const data = await getTemplate('dashboard', filename);
23
+ const index = data.find(el => el[0] === 'index.yml')[1]
24
+ const { table_name, description, title } = index || {};
25
+
26
+ return { name: filename, type: 'file', title, description, table_name };
27
+ })
28
+ );
29
+
30
+ const { rows = [] } = await pg.query(q);
31
+
32
+ const list = dir.concat(rows);
33
+
34
+ const res = {
35
+ time: Date.now() - time,
36
+ rows: list,
37
+ };
38
+ return res;
39
+ }
@@ -0,0 +1,23 @@
1
+ import config from '../../../config.js';
2
+
3
+ import dashboard from './controllers/dashboard.js';
4
+ import dashboardList from './controllers/dashboard.list.js';
5
+ import dashboardDelete from './controllers/dashboard.delete.js';
6
+ const biSchema = {
7
+ querystring: {
8
+ widget: { type: 'string', pattern: '^([\\d\\w]+)$' },
9
+ dashboard: { type: 'string', pattern: '^([\\d\\w]+)$' },
10
+ list: { type: 'string', pattern: '^([\\d])$' },
11
+ sql: { type: 'string', pattern: '^([\\d])$' }
12
+ },
13
+ params: {
14
+ id: { type: 'string', pattern: '^([\\d\\w]+)$' },
15
+ },
16
+ };
17
+
18
+
19
+ export default async function route(fastify) {
20
+ fastify.get(`/api/bi-dashboard/:id`, { schema: biSchema }, dashboard);
21
+ fastify.get(`/api/bi-dashboard`, dashboardList);
22
+ fastify.delete(`/api/bi-dashboard/:id`, dashboardDelete);
23
+ }
@@ -0,0 +1,100 @@
1
+ import path from 'path';
2
+ // import getTemplate from '@opengis/fastify-table/table/controllers/utils/getTemplate.js';
3
+ import autoIndex from '@opengis/fastify-table/pg/funcs/autoIndex.js';
4
+ import pgClients from '@opengis/fastify-table/pg/pgClients.js';
5
+ import chartSQL from './util/chartSQL.js';
6
+ import normalizeData from './util/normalizeData.js';
7
+
8
+ const cwd = process.cwd();
9
+ const dashboardDir = path.join(cwd, 'server/templates/dashboard');
10
+ const widgetDir = path.join(cwd, 'server/templates/widget');
11
+
12
+ import { existsSync, readdirSync, readFileSync } from 'fs';
13
+
14
+ import yaml from '../../dashboard/controllers/utils/yaml.js';
15
+
16
+ export default async function data({
17
+ pg = pgClients.client, funcs = {}, query = {},
18
+ }) {
19
+ const time = Date.now();
20
+ const { config = {} } = funcs;
21
+ const { dashboard, widget } = query;
22
+
23
+ if (!widget && !dashboard) {
24
+ return { message: 'not enough params: widget or dashboard required', status: 400 };
25
+ }
26
+
27
+ const fileList = dashboard && existsSync(path.join(dashboardDir, dashboard)) ? readdirSync(path.join(dashboardDir, dashboard)) : []
28
+ const filePath = dashboard ? fileList.filter(el => el.includes(widget)).map(el => path.join(dashboardDir, dashboard, el))[0] : path.join(widgetDir, widget + '.yml')
29
+
30
+ const { id, tableName } = !existsSync(filePath) ? await pg.query(`select dashboard_id as id, table_name as "tableName" from bi.dashboard where $1 in (dashboard_id, name)`, [dashboard]).then((res1) => res1.rows?.[0] || {}) : {};
31
+
32
+ if (!id && !existsSync(filePath)) {
33
+ return { message: { root: config?.local ? dashboardDir : undefined, error: `not found`, widget, dashboard }, status: 404 };
34
+ }
35
+
36
+ const fileData = existsSync(filePath) ? readFileSync(filePath, 'utf-8') : '';
37
+
38
+ const jsonData = filePath?.includes('.yml') ? yaml.loadSafe(fileData) : {};
39
+
40
+ const q = `select *, coalesce(data::jsonb, '{}'::jsonb) || jsonb_build_object('table', table_name) as data from bi.widget where dashboard_id=$1 and name=$2`;
41
+ const { type, text, data, options, controls, style } = existsSync(filePath)
42
+ ? jsonData
43
+ : await pg.query(q, [id || dashboard, widget])
44
+ .then((res1) => res1.rows?.[0] || {});
45
+
46
+ if (type === 'text' || filePath?.includes('.md')) {
47
+
48
+ return fileData;
49
+ }
50
+
51
+ if (!data?.table && tableName) {
52
+ Object.assign(data, { table: tableName });
53
+ }
54
+
55
+ if (!data?.table) {
56
+ return { error: /* json.error || */ `invalid ${widget ? 'widget' : 'dashboard'}: 1`, status: 500 };
57
+ }
58
+
59
+ // data param
60
+ const { x, metric, table, where, groupby, xName } = normalizeData(data, query);
61
+
62
+ // auto Index
63
+ autoIndex({ table: data.table, columns: [data?.time].concat([xName]).concat([groupby]).filter(el => el) })
64
+
65
+ // get group
66
+ const groupData = groupby ? await pg.query(`select ${groupby} as name ,count(*) from ${table} group by ${groupby} order by count(*) desc limit 20`).then(el => el.rows) : null;
67
+
68
+ if (query.sql === '2') return { x, metric, table, data, groupData };
69
+
70
+ const sql = (chartSQL[type] || chartSQL['chart'])({ where, metric, table, x, groupData, groupby });
71
+
72
+
73
+ if (query.sql) return sql;
74
+
75
+ if (!sql || sql?.includes('undefined')) {
76
+ return { message: { error: 'invalid sql', type, sql, where, metric, table, x, groupData, groupby }, status: 500 };
77
+ }
78
+
79
+ const { rows, fields } = await pg.query(sql); // test with limit
80
+
81
+ const dimensions = fields.map(el => el.name)
82
+
83
+ const res = {
84
+ time: Date.now() - time,
85
+ dimensions,
86
+
87
+ dimensionsType: fields.map(el => pg.pgType[el.dataTypeID]),
88
+ type,
89
+ text: text ? text : data.text,
90
+ //data: query.format === 'data' ? dimensions.map(el => rows.map(r => r[el])) : undefined,
91
+ source: query.format === 'array' ? dimensions.map(el => rows.map(r => r[el])) : rows,
92
+ style,
93
+ options,
94
+ controls,
95
+ };
96
+ return res;
97
+
98
+
99
+
100
+ }
@@ -0,0 +1,24 @@
1
+ function number({ metric, where, table }) {
2
+ const sql = `select ${metric} from ${table} where ${where}`
3
+ return sql;
4
+ }
5
+ function table({ columns, table, where }) {
6
+ return `select ${columns.map(el => el.name || el)}::text from ${table} where ${where} limit 20 `
7
+ }
8
+
9
+ function chart({ metric, where, table, x, groupby, groupData }) {
10
+
11
+ const metricData = groupData?.map(el => `${metric} filter (where ${groupby}='${el.name}') as "${el.name}"`).join(',') || metric
12
+ const sql = `select ${x}, ${metricData}
13
+ from ${table}
14
+ where ${where}
15
+ group by ${x}
16
+ order by ${x}`;
17
+ return sql;
18
+ }
19
+
20
+ function text() {
21
+ return undefined;
22
+ }
23
+
24
+ export default { number, chart, }
@@ -0,0 +1,21 @@
1
+ function normalizeData(data, query = {}) {
2
+
3
+
4
+ const granularity = query.granularity || data.granularity;
5
+
6
+
7
+ const xName = Array.isArray(data.x) ? data.x[0] : data.x;
8
+
9
+ const x = (granularity ? `date_trunc('${granularity}',${xName})::date` : null) || xName;
10
+
11
+ const metrics = Array.isArray(data.metrics) ? data.metrics : [data.metrics];
12
+ const metric = metrics.length ? metrics?.filter(el => el).map(el => `${el.operator || 'sum'}(${el.name || el})`) : 'count(*)';
13
+
14
+ const table = data.table;
15
+ const where = data.query || 'true';
16
+ const groupby = query.groupby || data.groupby;
17
+ //const orderby = query.orderby || data.orderby || 'count(*)';
18
+
19
+ return { x, metric, table, where, groupby, xName }
20
+ }
21
+ export default normalizeData;
@@ -0,0 +1,26 @@
1
+ import config from '../../../config.js';
2
+
3
+ import data from './controllers/data.js';
4
+
5
+
6
+ const biSchema = {
7
+ querystring: {
8
+ widget: { type: 'string', pattern: '^([\\d\\w]+)$' },
9
+ dashboard: { type: 'string', pattern: '^([\\d\\w]+)$' },
10
+ sql: { type: 'string', pattern: '^([\\d])$' }
11
+ },
12
+ params: {
13
+ id: { type: 'string', pattern: '^([\\d\\w]+)$' },
14
+ },
15
+ };
16
+
17
+
18
+ export default async function route(fastify, opts) {
19
+ const prefix = opts?.prefix || config.prefix || '/api';
20
+ fastify.route({
21
+ method: 'GET',
22
+ url: '/api/bi-data',
23
+ schema: biSchema,
24
+ handler: data,
25
+ });
26
+ }
@@ -0,0 +1,19 @@
1
+ export default async function widgetAdd({ pg, funcs, params = {}, body }) {
2
+ try {
3
+ const time = Date.now();
4
+ console.log(body);
5
+ const res = await funcs.dataInsert({
6
+ table: 'bi.dashboard',
7
+ data: body
8
+ });
9
+
10
+ return {
11
+ time: Date.now() - time,
12
+ message: `Added new dashboard, ID: '${res.rows[0].title}'`,
13
+ status: 200,
14
+ rows: res.rows
15
+ };
16
+ } catch (err) {
17
+ return { error: err.toString(), status: 500 };
18
+ }
19
+ }
@@ -0,0 +1,33 @@
1
+ export default async function dashboardEdit({
2
+ pg, funcs, params = {}, body = {},
3
+ }, reply) {
4
+ try {
5
+ if (!params.name) {
6
+ return {
7
+ message: "not enough params: dashboard name required",
8
+ status: 400,
9
+ }
10
+ }
11
+
12
+ const { name: dashboardName } = params;
13
+ const row = await pg.query(`select dashboard_id from bi.dashboard where $1 in (dashboard_id, name)`, [dashboardName]).then((res1) => res1.rows?.[0] || {});
14
+ const { dashboard_id: dashboardId } = row;
15
+
16
+ const res = await funcs.dataUpdate({
17
+ table: 'bi.dashboard',
18
+ id: dashboardId,
19
+ data: body,
20
+ });
21
+ if (!Object.keys(res)?.length) {
22
+ return { message: 'not found data', status: 404 };
23
+ }
24
+
25
+ return {
26
+ message: `updated ${dashboardName}`,
27
+ status: 200,
28
+ rows: res,
29
+ };
30
+ } catch (err) {
31
+ return reply.status(500).send(err.toString());
32
+ }
33
+ }
@@ -0,0 +1,33 @@
1
+ export default async function widgetAdd({
2
+ pg, funcs, params = {}, body = {},
3
+ }) {
4
+ const { name: dashboardName } = params;
5
+ if (!dashboardName) {
6
+ return { message: 'not enough params: id', status: 400 };
7
+ }
8
+
9
+ try {
10
+ const row = await pg.query(`select dashboard_id, widgets, panels from bi.dashboard where $1 in (dashboard_id,name)`, [dashboardName]).then((res) => res.rows?.[0] || {});
11
+ const { dashboard_id: dashboardId } = row;
12
+
13
+ const res = await funcs.dataUpdate({
14
+ table: 'bi.dashboard',
15
+ id: dashboardId,
16
+ data: {
17
+ widgets: [body].concat(row.widgets || []),
18
+ panels: [{ widget: body.name, col: body.col }].concat(row.panels || [])
19
+ }
20
+ });
21
+ const res2 = await funcs.dataInsert({
22
+ table: 'bi.widget',
23
+ data: { ...body, data: body, dashboard_id: dashboardId },
24
+ });
25
+ return {
26
+ message: `Added widget to ${dashboardName}`,
27
+ status: 200,
28
+ rows: res,
29
+ };
30
+ } catch (err) {
31
+ return { error: err.toString(), status: 500 };
32
+ }
33
+ }