@opengis/bi 1.0.1

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.
package/.eslintrc.cjs ADDED
@@ -0,0 +1,42 @@
1
+ /* eslint-env node */
2
+ //require('@rushstack/eslint-patch/modern-module-resolution');
3
+
4
+ module.exports = {
5
+ root: true,
6
+ extends: [
7
+ 'plugin:vue/vue3-essential',
8
+ 'eslint:recommended',
9
+ '@vue/eslint-config-typescript',
10
+ '@vue/eslint-config-prettier/skip-formatting',
11
+ 'airbnb-base',
12
+ ],
13
+ parserOptions: {
14
+ ecmaVersion: 2024,
15
+ sourceType: 'module',
16
+ },
17
+ rules: {
18
+ 'brace-style': [2, 'stroustrup', { allowSingleLine: true }],
19
+ 'vue/max-attributes-per-line': 0,
20
+ 'vue/valid-v-for': 0,
21
+
22
+ // allow async-await
23
+ 'generator-star-spacing': 'off',
24
+
25
+ // allow paren-less arrow functions
26
+ 'arrow-parens': 0,
27
+ 'one-var': 0,
28
+ 'max-len': 0,
29
+ 'import/first': 0,
30
+ 'import/named': 2,
31
+ 'import/namespace': 2,
32
+ 'import/default': 2,
33
+ 'import/export': 2,
34
+ 'import/extensions': 0,
35
+ 'no-console': ['warn', { allow: ['warn', 'error'] }],
36
+ 'import/no-unresolved': 0,
37
+ 'import/no-extraneous-dependencies': 0,
38
+ 'linebreak-style': ['error', 'unix'],
39
+ // allow debugger during development
40
+ 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
41
+ },
42
+ };
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/prettierrc",
3
+ "semi": true,
4
+ "tabWidth": 2,
5
+ "singleQuote": true,
6
+ "printWidth": 100,
7
+ "trailingComma": "none",
8
+ "endOfLine": "lf"
9
+ }
package/README.md ADDED
@@ -0,0 +1,22 @@
1
+ # bi
2
+
3
+ [![NPM version](https://img.shields.io/npm/v/@opengis/fastify-table)](https://www.npmjs.com/package/@opengis/fastify-table)
4
+ [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/)
5
+
6
+ It standardizes the entire form bi visualization process:
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ npm i @opengis/bi
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ```js
17
+ fastify.register(import('@opengis/bi'), config);
18
+ ```
19
+
20
+ ## Documenation
21
+
22
+ For a detailed understanding fastify-table, its features, and how to use them, refer to our [Documentation](https://apidocs.softpro.ua/bi/api/).
package/config.example ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "folder": "M:/work/geo/storage",
3
+ "templateDir": "test/templates",
4
+ "pg": {
5
+ "host": "192.168.3.160",
6
+ "port": 5432,
7
+ "database": "geo_green_poltava",
8
+ "user": "postgres",
9
+ "password": "postgres"
10
+ },
11
+ "redis": {
12
+ "host": "192.168.3.160",
13
+ "port": 6379,
14
+ "family": 4
15
+ },
16
+ "prefix": "/api",
17
+ "signServerAddress": "192.168.3.160:4001",
18
+ "mapServerAddress": "192.168.3.160:4002",
19
+ "convertServerAddress": "192.168.3.160:4003",
20
+ "gdalServerAddress": "192.168.3.160:4005"
21
+ }
package/config.js ADDED
@@ -0,0 +1,12 @@
1
+ import fs from 'fs';
2
+
3
+ import { readFile } from 'fs/promises';
4
+
5
+ const fileName = ['/data/local/config.json', 'config.json'].find(el => (fs.existsSync(el) ? el : null));
6
+ const config = fileName ? await readFile(fileName).then(el => JSON.parse(el)) : {};
7
+
8
+ Object.assign(config, {
9
+ allTemplates: config?.allTemplates || {},
10
+ });
11
+
12
+ export default config;
@@ -0,0 +1,22 @@
1
+
2
+
3
+ //import { existsSync, readFileSync } from 'fs';
4
+
5
+ import yaml from './utils/yaml.js';
6
+ import getTemplate from '@opengis/fastify-table/table/controllers/utils/getTemplate.js';
7
+
8
+ export default async function data({ params, query = {} }) {
9
+
10
+ const { id, } = params;
11
+
12
+
13
+ if (!id) {
14
+ return { message: 'not enough params: dashboard required', status: 400 };
15
+ }
16
+
17
+ const yml = await getTemplate('dashboard', id);
18
+ if (!yml) return { message: 'not found ' + id, status: 404 };
19
+ const data = yaml.loadSafe(yml);
20
+ return data
21
+
22
+ }
@@ -0,0 +1,108 @@
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
+
6
+ //import { existsSync, readFileSync } from 'fs';
7
+
8
+ import yaml from './utils/yaml.js';
9
+
10
+ export default async function data({ pg = pgClients.client, funcs, query = {} }) {
11
+ const time = Date.now();
12
+ const {
13
+ dashboard, chart, filter,
14
+ } = query;
15
+
16
+ if (!chart && !dashboard) {
17
+ return { message: 'not enough params: chart or dashboard required', status: 400 };
18
+ }
19
+
20
+ const yml = await getTemplate('chart', chart);
21
+ if (!yml) return { message: 'not found ' + chart, status: 404 };
22
+ const { data, options, controls } = yaml.loadSafe(yml);
23
+
24
+ if (!data?.source) {
25
+ return { error: json.error || `invalid ${chart ? 'chart' : 'dashboard'}`, status: 500 };
26
+ }
27
+
28
+
29
+ const granularity = query.granularity || data.granularity || 'month';
30
+
31
+ const dimension = (data?.time ? `date_trunc('${granularity}',${data?.time})::date` : null) || data.dimension?.[0]?.name || data.dimension;
32
+ const metric = data.metrics?.map(el => `${el.operator || 'sum'}(${el.name}) as "${el.title || el.name}"`) || 'count(*)';
33
+ const groupby = query.groupby || data.groupby?.[0] || data.groupby;
34
+
35
+ // filter rows
36
+ const fData = filter ? await funcs.getFilterSQL({
37
+ filter, table: json.data.source, json: 1,
38
+ }) : {};
39
+
40
+
41
+ if (data.columns) {
42
+ const sql = `select ${data.columns.map(el => el.name || el)}::text from ${data.source} where ${data.query || '1=1'} limit 20 `;
43
+ const { rows } = await pg.query(sql); // test with limit
44
+ return { count: rows.length, rows, options }
45
+ }
46
+ // auto Index
47
+ autoIndex({ table: data.source, columns: [data?.time].concat([dimension]).concat([groupby]).filter(el => el) })
48
+
49
+
50
+
51
+
52
+
53
+ const groupSQL = `select ${groupby}::text as x,count(*) from ${data.source} where ${groupby} is not null group by ${groupby} order by count(*) desc limit 10 `;
54
+ const { rows: groups } = groupby ? await pg.query(groupSQL) : {}; // test with limit
55
+ const groupMetric = data.metrics?.map(el => `${el.operator || 'sum'}(${el.name || el})`)?.[0] || 'count(*)';
56
+
57
+ // scratch, rewrite 2015, 2016 as separate columns
58
+ // const { rows: granularityDimensions } = await pg.query('select json_array_elements_text($1)', [granularities]);
59
+
60
+ const sql = `select ${dimension}::text as ${(data.time ? granularity : null) || dimension || 'x'} , ${metric}
61
+ ${groups?.length ? ',' : ''} ${groups?.map(el => `${groupMetric} filter(where ${groupby} = '${el.x}') as "${el.x}"`).join(',') || ''}
62
+ from ${data.source}
63
+ where ${data.query || '1=1'}
64
+ group by ${dimension}`;
65
+
66
+ if (query.sql) return sql;
67
+
68
+ const { rows, fields } = await pg.query(sql); // test with limit
69
+ const dimensions = fields.map(el => el.name)
70
+ // return result
71
+ const res = {
72
+ time: Date.now() - time,
73
+ dimensions,
74
+ source: rows,
75
+
76
+ options,
77
+ controls,
78
+ };
79
+ /*
80
+ source: [
81
+ {
82
+ product: 'Matcha Latte',
83
+ 2015: 43.3,
84
+ 2016: 85.8,
85
+ 2017: 93.7,
86
+ },
87
+ {
88
+ product: 'Milk Tea',
89
+ 2015: 83.1,
90
+ 2016: 73.4,
91
+ 2017: 55.1,
92
+ },
93
+ {
94
+ product: 'Cheese Cocoa',
95
+ 2015: 86.4,
96
+ 2016: 65.2,
97
+ 2017: 82.5,
98
+ },
99
+ {
100
+ product: 'Walnut Brownie',
101
+ 2015: 72.4,
102
+ 2016: 53.9,
103
+ 2017: 39.1,
104
+ },
105
+ ],
106
+ */
107
+ return res;
108
+ }
@@ -0,0 +1,3 @@
1
+ export default async function geojson(req) {
2
+ return { test: '2' };
3
+ }
@@ -0,0 +1,12 @@
1
+ import yaml from 'js-yaml';
2
+
3
+ yaml.loadSafe = (yml) => {
4
+ try {
5
+ return yaml.load(yml);
6
+ }
7
+ catch (err) {
8
+ return { error: err.toString() };
9
+ }
10
+ };
11
+
12
+ export default yaml;
@@ -0,0 +1,3 @@
1
+ export default async function vtile(req) {
2
+ return { test: '3' };
3
+ }
@@ -0,0 +1,35 @@
1
+ const schemaInfo = {};
2
+
3
+ import config from '../config.js';
4
+
5
+ import data from './controllers/data.js';
6
+ import dashboard from './controllers/dashboard.js';
7
+ import geojson from './controllers/geojson.js';
8
+ import vtile from './controllers/vtile.js';
9
+
10
+ export default async function route(fastify, opts) {
11
+ const prefix = opts?.prefix || config.prefix || '/api';
12
+ fastify.route({
13
+ method: 'GET',
14
+ url: `${prefix}/bi-data`,
15
+ schema: schemaInfo,
16
+ handler: data,
17
+ });
18
+ fastify.route({
19
+ method: 'GET',
20
+ url: `${prefix}/bi-dashboard/:id`,
21
+ handler: dashboard,
22
+ });
23
+ fastify.route({
24
+ method: 'GET',
25
+ url: `${prefix}/bi-geojson`,
26
+ schema: schemaInfo,
27
+ handler: geojson,
28
+ });
29
+ fastify.route({
30
+ method: 'GET',
31
+ url: `${prefix}/bi-vtile`,
32
+ schema: schemaInfo,
33
+ handler: vtile,
34
+ });
35
+ }
package/helper.js ADDED
@@ -0,0 +1,34 @@
1
+ // This file contains code that we reuse
2
+ // between our tests.
3
+ import Fastify from 'fastify';
4
+ import rclient from '@opengis/fastify-table/redis/client.js';
5
+ import pgClients from '@opengis/fastify-table/pg/pgClients.js';
6
+
7
+ import config from './test/config.js';
8
+ import appService from './index.js';
9
+
10
+ // automatically build and tear down our instance
11
+ async function build(t) {
12
+ await pgClients.client.init()
13
+ // you can set all the options supported by the fastify CLI command
14
+ // const argv = [AppPath]
15
+ process.env.NODE_ENV = 'production';
16
+ const app = Fastify({ logger: false });
17
+ app.register(appService, config);
18
+
19
+ // unit test
20
+ // app.register(import('@opengis/fastify-table'), config);
21
+ // app.register(import('../../../npm/fastify-table/index.js'), config);
22
+
23
+ // close the app after we are done
24
+ t.after(() => {
25
+ // console.log('close app');
26
+ pgClients.client?.end();
27
+ rclient?.quit();
28
+ app.close();
29
+ });
30
+
31
+ return app;
32
+ }
33
+
34
+ export default build;
package/index.js ADDED
@@ -0,0 +1,28 @@
1
+ import fp from 'fastify-plugin';
2
+ import config from './config.js';
3
+
4
+ import biPlugin from './dashboard/index.mjs';
5
+
6
+ async function plugin(fastify, opt) {
7
+ config.folder = opt.folder;
8
+ config.root = opt.root;
9
+
10
+ config.funcs = fastify;
11
+ config.pg = opt.pg;
12
+ config.redis = opt.redis;
13
+ config.s3 = opt.s3;
14
+
15
+ // independent npm start / unit test
16
+ if (!fastify.config) {
17
+ fastify.decorate('config', config);
18
+ }
19
+
20
+ if (!fastify.funcs) {
21
+ fastify.addHook('onRequest', async (req) => {
22
+ req.funcs = fastify;
23
+ });
24
+ }
25
+
26
+ biPlugin(fastify); // { prefix: config.prefix }
27
+ }
28
+ export default fp(plugin);
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@opengis/bi",
3
+ "version": "1.0.1",
4
+ "description": "BI data visualization module",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
9
+ "test": "node --test"
10
+ },
11
+ "keywords": [],
12
+ "author": "Softpro",
13
+ "license": "ISC",
14
+ "dependencies": {
15
+ "fastify": "^4.26.1",
16
+ "fastify-plugin": "^4.0.0",
17
+ "@opengis/fastify-table": "^1.0.36",
18
+ "js-yaml": "^4.1.0"
19
+ },
20
+ "devDependencies": {
21
+ "eslint": "^8.49.0",
22
+ "eslint-config-airbnb": "^19.0.4"
23
+ }
24
+ }
package/server.js ADDED
@@ -0,0 +1,14 @@
1
+ // This file contains code that we reuse
2
+ // between our tests.
3
+ import Fastify from 'fastify';
4
+ import config from './test/config.js';
5
+ import appService from './index.js';
6
+
7
+ const app = Fastify({ logger: false });
8
+ app.register(appService, config);
9
+ app.listen({ host: '0.0.0.0', port: process.env.PORT || 3000 }, (err) => {
10
+ if (err) {
11
+ app.log.error(err);
12
+ process.exit(1);
13
+ }
14
+ });
@@ -0,0 +1,18 @@
1
+ import config from '../config.js';
2
+
3
+ Object.assign(config, {
4
+ templateDir: 'test/templates',
5
+ pg: {
6
+ host: '192.168.3.160',
7
+ port: 5434,
8
+ database: 'mbk_rivne_dma',
9
+ user: 'postgres',
10
+ password: 'postgres',
11
+ },
12
+ redis: {
13
+ host: '192.168.3.160',
14
+ port: 6379,
15
+ family: 4,
16
+ },
17
+ });
18
+ export default config;
package/test/config.js ADDED
@@ -0,0 +1,19 @@
1
+ import config from '../config.js';
2
+
3
+ Object.assign(config, {
4
+ templateDir: 'test/templates',
5
+ pg: config.pg || {
6
+ host: '192.168.3.160',
7
+ port: 5432,
8
+ database: 'geo_green_poltava',
9
+ user: 'postgres',
10
+ password: 'postgres',
11
+ },
12
+ redis: config.redis || {
13
+ host: '192.168.3.160',
14
+ port: 6379,
15
+ family: 4,
16
+ },
17
+ });
18
+
19
+ export default config;
@@ -0,0 +1,76 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert';
3
+
4
+ import build from '../../helper.js';
5
+
6
+ import config from '../config.js';
7
+
8
+ test('api crud', async (t) => {
9
+ const app = await build(t);
10
+ const prefix = config.prefix || '/api';
11
+
12
+ // 2 metric + dimension
13
+ await t.test('GET /bi-data', async () => {
14
+ const res = await app.inject({
15
+ method: 'GET',
16
+ url: `${prefix}/bi-data`,
17
+ query: { chart: 'bar.zone' }
18
+ });
19
+
20
+ const rep = JSON.parse(res?.body);
21
+ assert.ok(rep.source);
22
+ });
23
+
24
+ // count + time
25
+ await t.test('GET /bi-data', async () => {
26
+ const res = await app.inject({
27
+ method: 'GET',
28
+ url: `${prefix}/bi-data`,
29
+ query: { chart: 'user' }
30
+ });
31
+ const rep = JSON.parse(res?.body);
32
+ assert.ok(rep.source);
33
+ });
34
+
35
+ // dimension + groupby
36
+ await t.test('GET /bi-data', async () => {
37
+ const res = await app.inject({
38
+ method: 'GET',
39
+ url: `${prefix}/bi-data`,
40
+ query: { chart: 'user' }
41
+ });
42
+ const rep = JSON.parse(res?.body);
43
+ assert.ok(rep.source);
44
+ });
45
+
46
+ await t.test('GET /bi-data', async () => {
47
+ const res = await app.inject({
48
+ method: 'GET',
49
+ url: `${prefix}/bi-data`,
50
+ query: { chart: 'bar.zone' }
51
+ });
52
+
53
+ const rep = JSON.parse(res?.body);
54
+ assert.ok(rep.source);
55
+ });
56
+
57
+ await t.test('GET /bi-vtile', async () => {
58
+ const res = await app.inject({
59
+ method: 'GET',
60
+ url: `${prefix}/bi-vtile`,
61
+ });
62
+
63
+ const rep = JSON.parse(res?.body);
64
+ assert.ok(rep);
65
+ });
66
+
67
+ await t.test('GET /bi-geojson', async () => {
68
+ const res = await app.inject({
69
+ method: 'DELETE',
70
+ url: `${prefix}/bi-geojson`,
71
+ });
72
+
73
+ const rep = JSON.parse(res?.body);
74
+ assert.ok(rep);
75
+ });
76
+ });
@@ -0,0 +1,23 @@
1
+ data:
2
+ source: itree.rest_zones # Назва таблиці
3
+ query: 1=1 # Запит
4
+
5
+ dimension: category
6
+ groupby1: [verif]
7
+ metrics: # Групування
8
+ - name: area
9
+ operator: sum
10
+ title: Площа
11
+ - name: area
12
+ operator: count
13
+ title: Кількість
14
+
15
+ controls: [invertAxis, details, switcher]
16
+ filters:
17
+ - name: cdate
18
+ type: date
19
+ default: year
20
+ title: Дата внесення у БД
21
+ - name: katotg
22
+ type: text
23
+ title: КАТОТТГ
@@ -0,0 +1,14 @@
1
+ data:
2
+ source: itree.rest_zones # Назва таблиці
3
+ query: uid is null # Запит
4
+ metrics: # Групування
5
+ - name: ownership
6
+ title: Форма власності
7
+ time: cdate
8
+ granularity: month
9
+
10
+ options:
11
+ formats:
12
+ - name: metric
13
+ type: histogram, background
14
+ color: blue [red,blue]
@@ -0,0 +1,16 @@
1
+ data:
2
+ source: itree.rest_zones # Назва таблиці
3
+ query: uid is null # Запит
4
+
5
+ metrics: # Групування
6
+ - name: monetary_valuation
7
+ title: Нормативна грошова оцінка, грн
8
+ dimension:
9
+ - name: balancer
10
+ title: Балансоутримувач об'єкта
11
+
12
+ options:
13
+ formats:
14
+ - name: metric
15
+ type: histogram, background
16
+ color: blue [red,blue]
@@ -0,0 +1,11 @@
1
+ data:
2
+ source: itree.rest_zones # Назва таблиці
3
+ query: 1=1 # Запит
4
+
5
+ time: cdate
6
+ granularity: month
7
+ groupby: [category]
8
+ metrics: # Групування
9
+ - name: area
10
+ operator: count
11
+ title: Площа
@@ -0,0 +1,15 @@
1
+ data:
2
+ source: itree.rest_zones # Назва таблиці
3
+ query: uid is null # Запит
4
+
5
+ columns:
6
+ - name
7
+ - ownership
8
+ - purpose
9
+
10
+ type: table
11
+ options:
12
+ formats:
13
+ - name: metric
14
+ type: table, background
15
+ color: blue [red,blue]
@@ -0,0 +1,28 @@
1
+ data:
2
+ source: data_address.addr_city # Назва таблиці
3
+ query: verified # Запит
4
+
5
+ columns:
6
+ - city_name
7
+ - city_koatuu
8
+
9
+ geom: geom
10
+
11
+ country: ua
12
+
13
+ cluster: # закачати через cdn
14
+ - region
15
+ - community
16
+
17
+ controls: []
18
+
19
+ options:
20
+ style:
21
+ color: red
22
+ size: 5
23
+ colorAttr: status
24
+
25
+ filters:
26
+ - name: cdate
27
+ type: date
28
+ title: Creation year
@@ -0,0 +1,7 @@
1
+ data:
2
+ source: crm_acc.crm_contact # Назва таблиці
3
+ query: uid is null # Запит
4
+
5
+ title: Дата
6
+ time: cdate
7
+ granularity: month
@@ -0,0 +1,11 @@
1
+ title: Audience
2
+ description: Understand your audience
3
+ panels:
4
+ - chart: line
5
+ type: line
6
+ title: Daily Active Users
7
+ col: 6
8
+ - chart: bar.zone
9
+ type: bar
10
+ - chart: table
11
+ type: table