@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 +9 -1
- package/cron/controllers/cronApi.js +22 -0
- package/cron/controllers/utils/cronList.js +1 -0
- package/cron/funcs/addCron.js +131 -0
- package/cron/index.js +10 -0
- package/index.js +89 -87
- package/package.json +1 -1
- package/table/controllers/data.js +7 -0
- package/table/controllers/utils/gisIRColumn.js +68 -0
package/Changelog.md
CHANGED
|
@@ -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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
config.
|
|
25
|
-
config.
|
|
26
|
-
config.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
fastify.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
@@ -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
|
+
}
|