@opengis/fastify-table 1.0.68 → 1.0.70

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/Changelog.md CHANGED
@@ -1,6 +1,14 @@
1
1
  # fastify-table
2
2
 
3
- ## 1.0.68- 26.07.2024
3
+ ## 1.0.70 - 29.07.2024
4
+
5
+ - add cron funcs and API
6
+
7
+ ## 1.0.69 - 26.07.2024
8
+
9
+ - add column classifier mode for data API
10
+
11
+ ## 1.0.68 - 26.07.2024
4
12
 
5
13
  - code optimization
6
14
 
@@ -0,0 +1,22 @@
1
+ import cronList from './utils/cronList.js';
2
+
3
+ export default async function cronApi(req) {
4
+ const {
5
+ params = {}, user = {}, hostname,
6
+ } = req;
7
+
8
+ if ((!user.uid || !user.user_type?.includes('admin')) && !hostname?.includes('localhost')) {
9
+ return { message: 'access restricted', status: 403 };
10
+ }
11
+
12
+ if (params.name === 'list') {
13
+ return { data: Object.keys(cronList || {}) };
14
+ }
15
+
16
+ if (!cronList[params.name]) {
17
+ return { message: `cron not found: ${params.name}`, status: 404 };
18
+ }
19
+
20
+ const result = await cronList[params.name](req);
21
+ return result;
22
+ }
@@ -0,0 +1 @@
1
+ export default {};
@@ -0,0 +1,131 @@
1
+ import { createHash } from 'crypto';
2
+
3
+ import cronList from '../controllers/utils/cronList.js';
4
+ import getRedis from '../../redis/funcs/getRedis.js';
5
+ import getPG from '../../pg/funcs/getPG.js';
6
+
7
+ const md5 = (string) => createHash('md5').update(string).digest('hex');
8
+
9
+ async function verifyUnique(name, config, rclient) {
10
+ const cronId = config.port || 3000 + md5(name);
11
+ // one per node check
12
+ const key = `cron:unique:${cronId}`;
13
+ const unique = await rclient.setnx(key, 1);
14
+ const ttl = await rclient.ttl(key);
15
+ if (!unique && ttl !== -1) {
16
+ return false;
17
+ }
18
+ await rclient.expire(key, 20);
19
+ return true;
20
+ }
21
+
22
+ const intervalStringMs = {
23
+ everyMin: 1000 * 60,
24
+ tenMin: 1000 * 60 * 10,
25
+ everyHour: 1000 * 60 * 60,
26
+ isHalfday: 1000 * 60 * 60 * 12,
27
+ dailyHour: 1000 * 60 * 60 * 24,
28
+ };
29
+
30
+ const interval2ms = {
31
+ string: (interval) => {
32
+ const date = new Date();
33
+ const intervarSplit = interval.match(/^(\*{2}|(\*)?(\d{1,2})):(\*(\d)|(\d{2}))/);
34
+ if (!intervarSplit) {
35
+ throw new Error(`interval ${interval} not suported`);
36
+ }
37
+ const [, , isHalfday, dailyHour, , tenMin, HourlyMin] = intervarSplit;
38
+ const intervalMs = (isHalfday && intervalStringMs.isHalfday)
39
+ || (dailyHour && intervalStringMs.dailyHour)
40
+ || (tenMin && intervalStringMs.tenMin)
41
+ || intervalStringMs.everyHour;
42
+ const offsetDay = ((+dailyHour || 0) * 60 + (+tenMin || +HourlyMin)) * 60 * 1000;
43
+ const offsetCur = (date - date.getTimezoneOffset() * 1000 * 60) % intervalMs;
44
+ const waitMs = (offsetDay - offsetCur + intervalMs) % intervalMs;
45
+ return [waitMs, intervalMs];
46
+ },
47
+ number: (interval) => {
48
+ const date = new Date();
49
+ const intervalMs = interval * 1000;
50
+ const dateWithTZ = date - date.getTimezoneOffset() * 1000 * 60;
51
+ const offsetCur = dateWithTZ % intervalMs;
52
+ // start every cron within 1 hour
53
+ const sixtyMinutesStartMs = 3600000;
54
+ const waitMs = (intervalMs - offsetCur) % sixtyMinutesStartMs;
55
+ return [waitMs, intervalMs];
56
+ },
57
+ };
58
+
59
+ async function runCron({
60
+ pg, funcs, func, name, rclient, log,
61
+ }) {
62
+ const unique = await verifyUnique(name, funcs.config, rclient);
63
+
64
+ if (!unique) return;
65
+ const db = pg.options.database;
66
+ log.debug(`cron.${name}`, 1, db);
67
+ try {
68
+ const data = await func({ pg, funcs, log });
69
+ log.debug('cron', { db, name, result: data });
70
+ log.info('cron', { db, name, result: data });
71
+ }
72
+ catch (err) {
73
+ log.debug('cron', { db, name, error: err.toString() });
74
+ log.error('cron', { db, name, error: err.toString() });
75
+ }
76
+ }
77
+
78
+ /**
79
+ * interval:
80
+ * - 02:54 - every day
81
+ * - 2:03 - every day
82
+ * - *1:43 - 2 times a day
83
+ * - *12:03 - 2 times a day
84
+ * - **:54 - every hour
85
+ * - **:*3 - every 10 minutes
86
+ * - 60 - every minute
87
+ * - 10 * 60 - every 10 minutes
88
+ */
89
+
90
+ export default async function addCron(func, interval, fastify) {
91
+ if (!fastify) {
92
+ throw new Error('not enough params: fastify');
93
+ }
94
+
95
+ const { config = {}, log } = fastify;
96
+ const { time = {}, disabled = [] } = config.cron || {};
97
+ const pg = getPG();
98
+ const rclient = getRedis();
99
+
100
+ const name = func.name || func.toString().split('/').at(-1).split('\'')[0];
101
+
102
+ // if (!config.isServer) return;
103
+
104
+ if (disabled.includes(name)) {
105
+ log.debug('cron', { name, message: 'cron disabled' });
106
+ return;
107
+ }
108
+
109
+ cronList[name] = func;
110
+
111
+ const userInterval = time[name] || interval;
112
+ const [waitMs, intervalMs] = interval2ms[typeof interval](userInterval);
113
+
114
+ if (intervalMs < 1000) {
115
+ log.warn('cron', { name, error: `interval ${interval} to small` });
116
+ return;
117
+ }
118
+
119
+ // setTimeout to w8 for the time to start
120
+ setTimeout(() => {
121
+ runCron({
122
+ pg, funcs: fastify, func, name, rclient, log,
123
+ });
124
+ // interval
125
+ setInterval(() => {
126
+ runCron({
127
+ pg, funcs: fastify, func, name, rclient, log,
128
+ });
129
+ }, intervalMs);
130
+ }, waitMs);
131
+ }
package/cron/index.js ADDED
@@ -0,0 +1,10 @@
1
+ import cronApi from './controllers/cronApi.js';
2
+ import addCron from './funcs/addCron.js';
3
+
4
+ async function plugin(fastify, config = {}) {
5
+ const prefix = config.prefix || '/api';
6
+ fastify.decorate('addCron', addCron);
7
+ fastify.get(`${prefix}/cron/:name`, {}, cronApi);
8
+ }
9
+
10
+ export default plugin;
package/index.js CHANGED
@@ -1,87 +1,89 @@
1
- import path from 'path';
2
- import { existsSync, readdirSync, readFileSync } from 'fs';
3
-
4
- import fp from 'fastify-plugin';
5
- import config from './config.js';
6
- // import rclient from './redis/client.js';
7
-
8
- import redisPlugin from './redis/index.js';
9
- import pgPlugin from './pg/index.js';
10
- import tablePlugin from './table/index.js';
11
- import notificationPlugin from './notification/index.js';
12
- import widgetPlugin from './widget/index.js';
13
- import crudPlugin from './crud/index.js';
14
- import policyPlugin from './policy/index.js';
15
- import utilPlugin from './util/index.js';
16
-
17
- import pgClients from './pg/pgClients.js';
18
-
19
- import execMigrations from './migration/exec.migrations.js';
20
-
21
- async function plugin(fastify, opt) {
22
- // console.log(opt);
23
- config.pg = opt.pg;
24
- config.redis = opt.redis;
25
- config.root = opt.root;
26
- config.mapServerRoot = opt.mapServerRoot;
27
-
28
- // independent npm start / unit test
29
- if (!fastify.config) {
30
- fastify.decorate('config', config);
31
- }
32
-
33
- fastify.register(import('@opengis/fastify-hb'));
34
- fastify.decorate('getFolder', (req, type = 'server') => {
35
- if (!['server', 'local'].includes(type)) throw new Error('params type is invalid');
36
- const types = { local: req.root, server: req.mapServerRoot };
37
- const filepath = path.posix.join(types[type] || '/data/local', req.folder || config.folder || '');
38
- return filepath;
39
- });
40
-
41
- fastify.addHook('onListen', async () => {
42
- const { client } = pgClients;
43
- if (client?.pk?.['crm.cls']) {
44
- const clsDir = path.join(process.cwd(), 'server/templates/cls');
45
- const files = existsSync(clsDir) ? readdirSync(clsDir) : [];
46
- if (files.length) {
47
- const res = await Promise.all(files.map(async (filename) => {
48
- const filepath = path.join(clsDir, filename);
49
- const data = JSON.parse(readFileSync(filepath));
50
- return { name: path.parse(filename).name, data };
51
- }));
52
- await client.query('truncate table crm.cls');
53
- const { rows } = await client.query(`insert into crm.cls(name, type)
54
- select value->>'name', 'json' from json_array_elements($1) returning cls_id as id, name`, [JSON.stringify(res).replace(/'/g, "''")]);
55
- rows.forEach((row) => Object.assign(row, { data: res.find((cls) => row.name === cls.name)?.data }));
56
- const sql = `insert into crm.cls(code, name, parent)
57
- select json_array_elements(value->'data')->>'id', json_array_elements(value->'data')->>'text', value->>'name' from json_array_elements($1)`;
58
- await client.query(sql, [JSON.stringify(rows).replace(/'/g, "''")]);
59
- }
60
- }
61
- // call from another repo / project
62
- fastify.execMigrations = execMigrations;
63
- // execute core migrations
64
- await fastify.execMigrations();
65
- });
66
- if (!fastify.funcs) {
67
- fastify.addHook('onRequest', async (req) => {
68
- req.funcs = fastify;
69
- if (!req.user && req.session?.passport?.user) {
70
- const { user } = req.session?.passport || {};
71
- req.user = user;
72
- }
73
- });
74
- // fastify.decorateRequest('funcs', fastify);
75
- }
76
-
77
- policyPlugin(fastify);
78
- redisPlugin(fastify);
79
- await pgPlugin(fastify, opt);
80
- tablePlugin(fastify, opt);
81
- crudPlugin(fastify, opt);
82
- notificationPlugin(fastify, opt);
83
- widgetPlugin(fastify, opt);
84
- utilPlugin(fastify, opt);
85
- }
86
- export default fp(plugin);
87
- // export { rclient };
1
+ import path from 'path';
2
+ import { existsSync, readdirSync, readFileSync } from 'fs';
3
+
4
+ import fp from 'fastify-plugin';
5
+ import config from './config.js';
6
+ // import rclient from './redis/client.js';
7
+
8
+ import redisPlugin from './redis/index.js';
9
+ import pgPlugin from './pg/index.js';
10
+ import tablePlugin from './table/index.js';
11
+ import notificationPlugin from './notification/index.js';
12
+ import widgetPlugin from './widget/index.js';
13
+ import crudPlugin from './crud/index.js';
14
+ import policyPlugin from './policy/index.js';
15
+ import utilPlugin from './util/index.js';
16
+ import cronPlugin from './cron/index.js';
17
+
18
+ import pgClients from './pg/pgClients.js';
19
+
20
+ import execMigrations from './migration/exec.migrations.js';
21
+
22
+ async function plugin(fastify, opt) {
23
+ // console.log(opt);
24
+ config.pg = opt.pg;
25
+ config.redis = opt.redis;
26
+ config.root = opt.root;
27
+ config.mapServerRoot = opt.mapServerRoot;
28
+
29
+ // independent npm start / unit test
30
+ if (!fastify.config) {
31
+ fastify.decorate('config', config);
32
+ }
33
+
34
+ fastify.register(import('@opengis/fastify-hb'));
35
+ fastify.decorate('getFolder', (req, type = 'server') => {
36
+ if (!['server', 'local'].includes(type)) throw new Error('params type is invalid');
37
+ const types = { local: req.root, server: req.mapServerRoot };
38
+ const filepath = path.posix.join(types[type] || '/data/local', req.folder || config.folder || '');
39
+ return filepath;
40
+ });
41
+
42
+ fastify.addHook('onListen', async () => {
43
+ const { client } = pgClients;
44
+ if (client?.pk?.['crm.cls']) {
45
+ const clsDir = path.join(process.cwd(), 'server/templates/cls');
46
+ const files = existsSync(clsDir) ? readdirSync(clsDir) : [];
47
+ if (files.length) {
48
+ const res = await Promise.all(files.map(async (filename) => {
49
+ const filepath = path.join(clsDir, filename);
50
+ const data = JSON.parse(readFileSync(filepath));
51
+ return { name: path.parse(filename).name, data };
52
+ }));
53
+ await client.query('truncate table crm.cls');
54
+ const { rows } = await client.query(`insert into crm.cls(name, type)
55
+ select value->>'name', 'json' from json_array_elements($1) returning cls_id as id, name`, [JSON.stringify(res).replace(/'/g, "''")]);
56
+ rows.forEach((row) => Object.assign(row, { data: res.find((cls) => row.name === cls.name)?.data }));
57
+ const sql = `insert into crm.cls(code, name, parent)
58
+ select json_array_elements(value->'data')->>'id', json_array_elements(value->'data')->>'text', value->>'name' from json_array_elements($1)`;
59
+ await client.query(sql, [JSON.stringify(rows).replace(/'/g, "''")]);
60
+ }
61
+ }
62
+ // call from another repo / project
63
+ fastify.execMigrations = execMigrations;
64
+ // execute core migrations
65
+ await fastify.execMigrations();
66
+ });
67
+ if (!fastify.funcs) {
68
+ fastify.addHook('onRequest', async (req) => {
69
+ req.funcs = fastify;
70
+ if (!req.user && req.session?.passport?.user) {
71
+ const { user } = req.session?.passport || {};
72
+ req.user = user;
73
+ }
74
+ });
75
+ // fastify.decorateRequest('funcs', fastify);
76
+ }
77
+
78
+ policyPlugin(fastify);
79
+ redisPlugin(fastify);
80
+ await pgPlugin(fastify, opt);
81
+ tablePlugin(fastify, opt);
82
+ crudPlugin(fastify, opt);
83
+ notificationPlugin(fastify, opt);
84
+ widgetPlugin(fastify, opt);
85
+ utilPlugin(fastify, opt);
86
+ cronPlugin(fastify, opt);
87
+ }
88
+ export default fp(plugin);
89
+ // export { rclient };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengis/fastify-table",
3
- "version": "1.0.68",
3
+ "version": "1.0.70",
4
4
  "type": "module",
5
5
  "description": "core-plugins",
6
6
  "main": "index.js",
@@ -3,6 +3,7 @@ import getFilterSQL from '../funcs/getFilterSQL/index.js';
3
3
  import getMeta from '../../pg/funcs/getMeta.js';
4
4
  import metaFormat from '../funcs/metaFormat/index.js';
5
5
  import setToken from '../../crud/funcs/setToken.js';
6
+ import gisIRColumn from './utils/gisIRColumn.js';
6
7
 
7
8
  const maxLimit = 100;
8
9
  export default async function dataAPI(req) {
@@ -28,6 +29,12 @@ export default async function dataAPI(req) {
28
29
  const cardSqlFiltered = opt?.id || params.id ? (cardSql?.filter?.((el) => !el?.disabled && el?.name && el?.sql?.replace) || []) : [];
29
30
  const cardSqlTable = cardSqlFiltered.length ? cardSqlFiltered.map((el, i) => ` left join lateral (select json_agg(row_to_json(q)) as ${el.name} from (${el.sql})q) ct${i} on 1=1 `).join('') || '' : '';
30
31
 
32
+ if (params.id && columnList.includes(params.id)) {
33
+ return gisIRColumn({
34
+ pg, funcs, layer: params.table, column: params.id, sql: query.sql,
35
+ });
36
+ }
37
+
31
38
  const fData = query.filter ? await getFilterSQL({
32
39
  filter: query.filter,
33
40
  table: params.table,
@@ -0,0 +1,68 @@
1
+ import pgClients from '../../../pg/pgClients.js';
2
+
3
+ import getTemplate from './getTemplate.js';
4
+ import getSelect from './getSelect.js';
5
+ import getFilterSQL from '../../funcs/getFilterSQL/index.js';
6
+
7
+ export default async function gisIRColumn({
8
+ pg = pgClients.client, funcs = {}, layer, column, sql, query = '1=1',
9
+ }) {
10
+ const time = Date.now();
11
+
12
+ const { config = {} } = funcs;
13
+
14
+ const sel = await getSelect(query.cls || column);
15
+
16
+ const body = await getTemplate('table', layer);
17
+ const fData = await getFilterSQL({
18
+ table: body?.table || layer, query: body?.query,
19
+ });
20
+
21
+ const { tlist } = await pg.one(`select array_agg((select nspname from pg_namespace where oid=relnamespace)||'.'||relname) tlist from pg_class
22
+ where relkind in ('r','v','m')`);
23
+
24
+ const tableName = body?.table || layer;
25
+ if (!tlist.includes(body?.table || layer)) return { error: `table not found: ${tableName}`, status: 400 };
26
+
27
+ // eslint-disable-next-line max-len
28
+ const { fields } = await pg.query(`select * from (${fData?.optimizedSQL || `select * from ${body?.table || layer}`})q limit 0`);
29
+
30
+ const col = fields.find((el) => el.name === column);
31
+
32
+ if (!col) return { status: 404, message: 'not found' };
33
+ const colField = pg.pgType[col.dataTypeID]?.includes('[]') ? `unnest(${column})` : column;
34
+
35
+ const q = `select ${colField} as id, count(*)::int from ${tableName} t where ${body?.query || 'true'}
36
+ group by ${colField} order by count desc limit 15`;
37
+
38
+ if (sql) return q;
39
+
40
+ if (!body?.columns?.length) {
41
+ const { rows } = await pg.query(q);
42
+ if (sel?.arr?.length) {
43
+ rows.forEach((el) => {
44
+ const data = sel?.find((item) => item.id?.toString() === el.id?.toString());
45
+ Object.assign(el, data || {});
46
+ });
47
+ }
48
+ return {
49
+ count: rows?.reduce((acc, el) => acc + el.count, 0),
50
+ sql: config.local ? q : undefined,
51
+ rows,
52
+ };
53
+ }
54
+
55
+ const { rows } = await pg.query(q);
56
+ const cls = query.cls || body?.columns?.find((el) => el.name === column)?.data || col.data || col.option;
57
+ const select = await getSelect(cls, { val: rows.map((el) => el.id), ar: 1 });
58
+ rows.forEach((el) => {
59
+ const data = select?.arr ? select.arr?.find((item) => item.id?.toString() === el.id?.toString()) : undefined;
60
+ Object.assign(el, data || {});
61
+ });
62
+ return {
63
+ time: Date.now() - time,
64
+ count: rows.reduce((acc, el) => acc + el.count, 0),
65
+ sql: config.local ? q : undefined,
66
+ rows,
67
+ };
68
+ }