@opengis/fastify-table 1.1.20 → 1.1.21

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,5 +1,9 @@
1
1
  # fastify-table
2
2
 
3
+ ## 1.1.21 - 03.10.2024
4
+
5
+ - add mention in comment notification hook
6
+
3
7
  ## 1.1.20 - 02.10.2024
4
8
 
5
9
  - code optimization
package/index.js CHANGED
@@ -1,5 +1,6 @@
1
- import path from 'path';
2
- import { existsSync, readdirSync, readFileSync } from 'fs';
1
+ import path from 'node:path';
2
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
3
4
 
4
5
  import fp from 'fastify-plugin';
5
6
  import config from './config.js';
@@ -18,7 +19,7 @@ import hookPlugin from './hook/index.js';
18
19
 
19
20
  import pgClients from './pg/pgClients.js';
20
21
 
21
- import execMigrations from './migration/exec.migrations.js';
22
+ import { addTemplateDir, execMigrations } from './utils.js';
22
23
 
23
24
  async function plugin(fastify, opt) {
24
25
  // console.log(opt);
@@ -68,10 +69,8 @@ async function plugin(fastify, opt) {
68
69
  await client.query(sql, [JSON.stringify(rows).replace(/'/g, "''")]);
69
70
  }
70
71
  }
71
- // call from another repo / project
72
- fastify.execMigrations = execMigrations;
73
72
  // execute core migrations
74
- await fastify.execMigrations();
73
+ await execMigrations();
75
74
  });
76
75
  if (!fastify.funcs) {
77
76
  fastify.addHook('onRequest', async (req) => {
@@ -94,6 +93,11 @@ async function plugin(fastify, opt) {
94
93
  utilPlugin(fastify, opt);
95
94
  cronPlugin(fastify, opt);
96
95
  hookPlugin(fastify, opt);
96
+
97
+ // core templates && cls
98
+ const filename = fileURLToPath(import.meta.url);
99
+ const cwd = path.dirname(filename);
100
+ addTemplateDir(path.join(cwd, 'module/core'));
97
101
  }
98
102
  export default fp(plugin);
99
103
  // export { rclient };
@@ -0,0 +1,2 @@
1
+ select uid, coalesce(sur_name,'')||coalesce(' '||user_name,'') as text, email from admin.users
2
+ where enabled order by coalesce(sur_name,'')||coalesce(' '||user_name,'')
@@ -0,0 +1,30 @@
1
+ export default async function readNotifications({
2
+ pg, params = {}, query = {}, session = {},
3
+ }) {
4
+ const { uid } = session.passport?.user || {};
5
+
6
+ if (!uid) {
7
+ return { message: 'access restricted', status: 403 };
8
+ }
9
+
10
+ try {
11
+ const { userId } = await pg.query('select uid as "userId" from admin.users where $1 in (uid,login) limit 1', [uid])
12
+ .then((res) => res.rows?.[0] || {});
13
+
14
+ if (!userId) {
15
+ return { message: 'access restricted: 2', status: 403 };
16
+ }
17
+
18
+ const q = `update crm.notifications set read=true where read is not true
19
+ and ${params?.id ? 'notification_id=$2' : '1=1'} and addressee_id=$1`;
20
+
21
+ if (query.sql) return q;
22
+
23
+ const { rowCount = 0 } = await pg.query(q, [userId, params?.id].filter((el) => el));
24
+
25
+ return { message: `${rowCount} unread notifications marked as read`, status: 200 };
26
+ }
27
+ catch (err) {
28
+ return { error: err.toString(), status: 500 };
29
+ }
30
+ }
@@ -1,16 +1,61 @@
1
+ // for example only
2
+ /*
3
+ const res = await funcs.dataInsert({
4
+ pg,
5
+ table: 'crm.notifications',
6
+ data: {
7
+ subject: 'notif title',
8
+ body: 'notif body',
9
+ link: 'http://localhost:3000/api/notification',
10
+ addressee_id: userId,
11
+ author_id: userId,
12
+ },
13
+ });
14
+ */
15
+
16
+ import { getSelectVal } from '../../utils.js';
17
+
18
+ const maxLimit = 100;
19
+
1
20
  export default async function userNotifications({
2
- pg, session = {}, query = {},
21
+ pg, query = {}, session = {},
3
22
  }) {
4
23
  const time = Date.now();
24
+
25
+ const { uid } = session.passport?.user || {};
26
+
27
+ if (!uid) {
28
+ return { message: 'access restricted', status: 403 };
29
+ }
30
+
31
+ const limit = Math.min(maxLimit, +(query.limit || 5));
32
+ const offset = query.page && query.page > 0 ? (query.page - 1) * limit : 0;
33
+
5
34
  try {
6
- const { uid } = session.passport?.user || {};
7
- if (!uid) return { error: 'access restricted', status: 403 };
35
+ const { userId } = await pg.query('select uid as "userId" from admin.users where $1 in (uid,login) limit 1', [uid])
36
+ .then((res) => res.rows?.[0] || {});
37
+
38
+ if (!userId) {
39
+ return { message: 'access restricted: 2', status: 403 };
40
+ }
41
+
42
+ const q = `select notification_id as id, subject, body, cdate,
43
+ author_id, read, link, entity_id, (select avatar from admin.users where uid=a.author_id limit 1) as avatar from crm.notifications a where addressee_id=$1 order by cdate desc limit ${limit} offset ${offset}`;
44
+
45
+ if (query.sql) return q;
46
+
47
+ const { rows = [] } = await pg.query(q, [userId]);
48
+
49
+ const values = rows.map((el) => el.author_id)
50
+ ?.filter((el, idx, arr) => el && arr.indexOf(el) === idx);
8
51
 
9
- const queryFunc = query.nocache ? pg.query : pg.queryCache;
52
+ if (values?.length) {
53
+ const vals = await getSelectVal({ name: 'core.user_mentioned', values });
54
+ rows.forEach((row) => {
55
+ Object.assign(row, { author: vals?.[row.author_id] });
56
+ });
57
+ }
10
58
 
11
- // queryCache not supports $1 params
12
- const { rows } = await queryFunc(`select notification_id as id, notification_type as type,
13
- notification_status as status, title, body, link, uid from crm.notification where uid='${uid}'`);
14
59
  return { time: Date.now() - time, total: rows?.length, rows };
15
60
  }
16
61
  catch (err) {
@@ -1,8 +1,19 @@
1
1
  export default async function addNotification({
2
- pg, session = {}, title, body, link, notificationType, uid: uid1,
2
+ pg, funcs, session = {}, subject, body, link, uid, entity,
3
3
  }) {
4
- const uid = uid1 || session.passport?.user?.uid || {};
5
- const { id, status } = await pg.one(`insert into crm.notification(title, body, link, notification_type, uid)
6
- values($1,$2,$3,$4,$5) returning notification_id as id, notification_status as status`, [title, body, link, notificationType, uid]);
7
- return { id, status };
4
+ const { uid: author } = session.passport?.user || {};
5
+ const res = await funcs.dataInsert({
6
+ pg,
7
+ table: 'crm.notifications',
8
+ data: {
9
+ subject,
10
+ body,
11
+ link,
12
+ addressee_id: uid,
13
+ author_id: author,
14
+ entity_id: entity,
15
+ uid: author,
16
+ },
17
+ });
18
+ return res?.rows?.[0] || {};
8
19
  }
@@ -0,0 +1,63 @@
1
+ /* eslint-disable camelcase */
2
+ import {
3
+ getSelect, addNotification, sendNotification,
4
+ } from '../../utils.js';
5
+
6
+ function sequence(arr, data, fn) {
7
+ return arr.reduce((promise, row) => promise.then(() => fn({
8
+ ...data, ...row,
9
+ })), Promise.resolve());
10
+ }
11
+
12
+ export default async function onWidgetSet({ req, type, data = {} }) {
13
+ const values = data.body?.match(/\B@[a-zA-z0-9а-яА-яіїІЇєЄ]+ [a-zA-z0-9а-яА-яіїІЇєЄ]+/g)
14
+ ?.filter((el, idx, arr) => el?.replace && arr.indexOf(el) === idx)
15
+ ?.map((el) => el.replace(/@/g, ''));
16
+
17
+ if (type !== 'comment' || !values?.length) {
18
+ return null;
19
+ }
20
+
21
+ const {
22
+ pg, funcs, path: link, session = {}, log,
23
+ } = req;
24
+ const { config = {} } = funcs;
25
+
26
+ const { sql } = await getSelect('core.user_mentioned');
27
+
28
+ const { rows = [] } = await pg.query(`with data (id,name,email) as (${sql})
29
+ select id, name, email from data where name = any($1)`, [values]);
30
+
31
+ if (!rows?.length) {
32
+ return null;
33
+ }
34
+
35
+ const message = `${data.body?.substring(0, 50)}...`;
36
+ const subject = 'You were mentioned';
37
+
38
+ await sequence(rows, {}, async ({ id, name, email }) => {
39
+ const res = await addNotification({
40
+ pg, funcs, session, subject, entity: data.entity_id, body: message, link, uid: id,
41
+ });
42
+ try {
43
+ const res1 = await sendNotification({
44
+ pg,
45
+ funcs,
46
+ log,
47
+ to: email,
48
+ // template,
49
+ message,
50
+ title: subject,
51
+ data,
52
+ nocache: 1,
53
+ });
54
+ if (config.local) console.info('comment notification', name, id, email, res1);
55
+ await pg.query('update crm.notifications set sent=true where notification_id=$1', [res?.notification_id]);
56
+ }
57
+ catch (err) {
58
+ console.error('comment notification send error', err.toString());
59
+ }
60
+ });
61
+ console.log('comment notification add', rows);
62
+ return null;
63
+ }
@@ -1,11 +1,18 @@
1
1
  // api
2
- import userNotifications from './controllers/userNotifications.js';
2
+ import readNotifications from './controllers/readNotifications.js'; // mark as read
3
+ import userNotifications from './controllers/userNotifications.js'; // check all, backend pagination
3
4
  import testEmail from './controllers/testEmail.js';
4
5
  // funcs
5
6
  import addNotification from './funcs/addNotification.js'; // add to db
6
- import notification from './funcs/sendNotification.js'; // send
7
+ import notification from './funcs/sendNotification.js'; // send notification
8
+
9
+ import onWidgetSet from './hook/onWidgetSet.js'; // send notification on comment
10
+ import { addHook } from '../utils.js';
7
11
 
8
12
  const tableSchema = {
13
+ params: {
14
+ id: { type: 'string' },
15
+ },
9
16
  querystring: {
10
17
  nocache: { type: 'string', pattern: '^(\\d+)$' },
11
18
  },
@@ -17,11 +24,20 @@ async function plugin(fastify, config = {}) {
17
24
  method: 'GET',
18
25
  url: `${prefix}/notification`,
19
26
  config: {
20
- policy: ['user'], // implement user auth check policy??
27
+ policy: ['user'],
21
28
  },
22
29
  schema: tableSchema,
23
30
  handler: userNotifications,
24
31
  });
32
+ fastify.route({
33
+ method: 'GET',
34
+ url: `${prefix}/notification-read/:id?`,
35
+ config: {
36
+ policy: ['user'],
37
+ },
38
+ schema: tableSchema,
39
+ handler: readNotifications,
40
+ });
25
41
  fastify.route({
26
42
  method: 'GET',
27
43
  url: `${prefix}/test-email`,
@@ -33,6 +49,7 @@ async function plugin(fastify, config = {}) {
33
49
 
34
50
  fastify.decorate('addNotification', addNotification);
35
51
  fastify.decorate('notification', notification);
52
+ addHook('onWidgetSet', onWidgetSet);
36
53
  }
37
54
 
38
55
  export default plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengis/fastify-table",
3
- "version": "1.1.20",
3
+ "version": "1.1.21",
4
4
  "type": "module",
5
5
  "description": "core-plugins",
6
6
  "main": "index.js",
@@ -24,60 +24,69 @@ export default function checkPolicy(req) {
24
24
 
25
25
  /*= == 0.Check superadmin access === */
26
26
  if (policy.includes('superadmin') && user?.user_type !== 'superadmin') {
27
- log.warn({
28
- name: 'api/superadmin', params, query, body: JSON.stringify(req?.body || {}).substring(30), message: 'access restricted: 0',
27
+ log.warn('api/superadmin', {
28
+ path, params, query, body: JSON.stringify(req?.body || {}).substring(30), message: 'access restricted: 0',
29
29
  });
30
30
  return { message: 'access restricted: 0', status: 403 };
31
31
  }
32
32
 
33
33
  /*= == 1.File injection === */
34
34
  if (JSON.stringify(params || {})?.includes('../') || JSON.stringify(query || {})?.includes('../') || path?.includes('../')) {
35
- log.warn({
36
- name: 'injection/file', params, query, message: 'access restricted: 1',
35
+ log.warn('injection/file', {
36
+ path, params, query, message: 'access restricted: 1',
37
37
  });
38
38
  return { message: 'access restricted: 1', status: 403 };
39
39
  }
40
40
 
41
- /*= == 1.1 File === */
41
+ /* === 1.1 File === */
42
42
  const allowExtPublic = ['.png', '.jpg', '.svg'];
43
43
  const ext = path.toLowerCase().substr(-4);
44
44
  if (path.includes('files/') && allowExtPublic.includes(ext)) return null;
45
45
 
46
- /*= == 2.SQL Injection policy: no-sql === */
46
+ /* === 2.SQL Injection policy: no-sql === */
47
47
  if (!policy.includes('no-sql')) {
48
48
  // skip polyline param - data filter (geometry bounds)
49
49
  const stopWords = block.filter((el) => path.replace(query.polyline, '').includes(el));
50
50
  if (stopWords?.length) {
51
- log.warn({ name: 'injection/sql', stopWords, message: 'access restricted: 2' });
51
+ log.warn('injection/sql', { stopWords, message: 'access restricted: 2', path });
52
52
  return { message: 'access restricted: 2', status: 403 };
53
53
  }
54
54
  }
55
- /* Check is Not API */
56
- const isApi = ['/files/', '/api/format/', '/api-user/', '/logger', '/file/'].filter((el) => path.includes(el)).length;
57
- if (!isApi) return null;
58
-
59
- /*= == 3. policy: referer === */
60
- if (!hs?.referer?.includes?.(hostname) && policy.includes('referer') && !config.local && !config.debug) {
61
- log.warn({ name: 'referer', message: 'access restricted: 3' });
62
- return { message: 'access restricted: 3', status: 403 };
55
+ /* policy: skip if not API */
56
+ const isApi = ['/files/', '/api/format/', '/api', '/api-user/', '/logger', '/file/'].filter((el) => path.includes(el)).length;
57
+ if (!isApi) {
58
+ return null;
63
59
  }
64
60
 
65
- /*= == policy: public === */
61
+ /* === policy: public === */
66
62
  if (policy.includes('public')) {
67
63
  return null;
68
64
  }
69
65
 
70
- /*= == 4. policy: site auth === */
71
- if (!policy.includes('site') && sid === 1 && isUser && !config.local && !config.debug) {
72
- log.warn({ name: 'site', message: 'access restricted: 4' });
66
+ /* === 3. policy: user === */
67
+ if (!user && policy.includes('user') && false) {
68
+ log.warn('policy/user', { message: 'access restricted: 3', path });
69
+ return { message: 'access restricted: 3', status: 403 };
70
+ }
71
+
72
+ /* === 4. policy: referer === */
73
+ if (!hs?.referer?.includes?.(hostname) && policy.includes('referer') && !config.local && !config.debug) {
74
+ log.warn('policy/referer', { message: 'access restricted: 4', uid: user?.uid });
73
75
  return { message: 'access restricted: 4', status: 403 };
74
76
  }
75
77
 
76
- /*= == 5. base policy: block api === */
77
- if (sid === 35 && !isUser && isServer && !config.local && !config.debug) {
78
- log.warn({ name: 'api', message: 'access restricted: 5' });
78
+ /* === 5. policy: site auth === */
79
+ if (!policy.includes('site') && sid === 1 && isUser && !config.local && !config.debug) {
80
+ log.warn('policy/site', { message: 'access restricted: 5', path, uid: user?.uid });
79
81
  return { message: 'access restricted: 5', status: 403 };
80
82
  }
81
83
 
84
+ /* === 6. base policy: block api, except login === */
85
+ if (sid === 35 && !isUser && isServer && !config.local && !config.debug
86
+ && !path.startsWith(`${config.prefix || '/api'}/login`)) {
87
+ log.warn('policy/api', { message: 'access restricted: 6', path, uid: user?.uid });
88
+ return { message: 'access restricted: 6', status: 403 };
89
+ }
90
+
82
91
  return null;
83
92
  }
package/policy/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  import checkPolicy from './funcs/checkPolicy.js';
4
4
 
5
5
  async function plugin(fastify) {
6
- fastify.addHook('onRequest', async (request, reply) => {
6
+ fastify.addHook('preParsing', async (request, reply) => {
7
7
  const hookData = checkPolicy(request);
8
8
  if (hookData?.status && hookData?.message) {
9
9
  return reply.status(hookData?.status).send(hookData.message);
@@ -6,12 +6,20 @@ CREATE TABLE IF NOT EXISTS crm.notifications();
6
6
  ALTER TABLE crm.notifications DROP CONSTRAINT IF EXISTS crm_notifications_pkey;
7
7
  ALTER TABLE crm.notifications ADD COLUMN IF NOT EXISTS notification_id text NOT NULL DEFAULT next_id();
8
8
 
9
- ALTER TABLE crm.notifications ADD COLUMN IF NOT EXISTS notification_user_id text;
10
- ALTER TABLE crm.notifications ADD COLUMN IF NOT EXISTS notification_type text DEFAULT 'notify'::text;
11
- ALTER TABLE crm.notifications ADD COLUMN IF NOT EXISTS notification_status text DEFAULT 'not sent'::text;
9
+ -- drop deprecated columns
10
+ ALTER TABLE crm.notifications DROP COLUMN IF EXISTS notification_user_id;
11
+ ALTER TABLE crm.notifications DROP COLUMN IF EXISTS notification_type;
12
+ ALTER TABLE crm.notifications DROP COLUMN IF EXISTS notification_status;
13
+
14
+ -- add actual columns
15
+ ALTER TABLE crm.notifications ADD COLUMN IF NOT EXISTS addressee_id text;
16
+ ALTER TABLE crm.notifications ADD COLUMN IF NOT EXISTS read boolean;
17
+ ALTER TABLE crm.notifications ADD COLUMN IF NOT EXISTS sent boolean;
12
18
  ALTER TABLE crm.notifications ADD COLUMN IF NOT EXISTS subject text;
13
19
  ALTER TABLE crm.notifications ADD COLUMN IF NOT EXISTS body text;
14
20
  ALTER TABLE crm.notifications ADD COLUMN IF NOT EXISTS link text;
21
+ ALTER TABLE crm.notifications ADD COLUMN IF NOT EXISTS author_id text;
22
+ ALTER TABLE crm.notifications ADD COLUMN IF NOT EXISTS entity_id text;
15
23
 
16
24
  ALTER TABLE crm.notifications ADD COLUMN IF NOT EXISTS uid text;
17
25
  ALTER TABLE crm.notifications ADD COLUMN IF NOT EXISTS files json;
@@ -20,6 +28,15 @@ ALTER TABLE crm.notifications ADD COLUMN IF NOT EXISTS editor_id text;
20
28
  ALTER TABLE crm.notifications ADD COLUMN IF NOT EXISTS editor_date timestamp without time zone;
21
29
  ALTER TABLE crm.notifications ADD CONSTRAINT crm_notifications_pkey PRIMARY KEY (notification_id);
22
30
 
31
+ COMMENT ON COLUMN crm.notifications.addressee_id is 'ID користувача отримувача повідомлення';
32
+ COMMENT ON COLUMN crm.notifications.read is 'Чи було повідомлення прочитане';
33
+ COMMENT ON COLUMN crm.notifications.sent is 'Чи було повідомлення відправлене';
34
+ COMMENT ON COLUMN crm.notifications.subject is 'Тема повідомлення';
35
+ COMMENT ON COLUMN crm.notifications.body is 'Зміст повідомлення';
36
+ COMMENT ON COLUMN crm.notifications.link is 'Посилання на об''єкт';
37
+ COMMENT ON COLUMN crm.notifications.author_id is 'ID користувача автора повідомлення';
38
+ COMMENT ON COLUMN crm.notifications.entity_id is 'ID на об''єкту';
39
+
23
40
  -- crm.files
24
41
  -- DROP TABLE IF EXISTS crm.files;
25
42
  CREATE TABLE IF NOT EXISTS crm.files();
package/utils.js CHANGED
@@ -24,8 +24,13 @@ import gisIRColumn from './table/controllers/utils/gisIRColumn.js';
24
24
  import getMeta from './pg/funcs/getMeta.js';
25
25
  import getAccess from './crud/funcs/getAccess.js';
26
26
  import getSelectVal from './table/funcs/metaFormat/getSelectVal.js';
27
+ import getSelect from './table/controllers/utils/getSelect.js';
28
+ import getSelectMeta from './table/controllers/utils/getSelectMeta.js';
27
29
  import applyHook from './hook/funcs/applyHook.js';
28
30
  import addHook from './hook/funcs/addHook.js';
31
+ import execMigrations from './migration/exec.migrations.js';
32
+ import addNotification from './notification/funcs/addNotification.js';
33
+ import sendNotification from './notification/funcs/sendNotification.js';
29
34
 
30
35
  export default null;
31
36
  export {
@@ -49,6 +54,11 @@ export {
49
54
  getMeta,
50
55
  getAccess,
51
56
  getSelectVal,
57
+ getSelectMeta,
58
+ getSelect,
52
59
  applyHook,
53
60
  addHook,
61
+ execMigrations,
62
+ addNotification,
63
+ sendNotification,
54
64
  };
@@ -1,8 +1,8 @@
1
1
  import path from 'path';
2
2
 
3
- import getMeta from '../../pg/funcs/getMeta.js';
4
- import dataInsert from '../../crud/funcs/dataInsert.js';
5
- import dataUpdate from '../../crud/funcs/dataUpdate.js';
3
+ import {
4
+ getMeta, dataInsert, dataUpdate, applyHook,
5
+ } from '../../utils.js';
6
6
 
7
7
  const tableList = {
8
8
  comment: 'crm.communications',
@@ -21,8 +21,8 @@ export default async function widgetSet(req) {
21
21
  } = req;
22
22
  const { user = {} } = session.passport || {};
23
23
  const { type, id, objectid } = params;
24
- if (!['comment', 'checklist', 'file', 'gallery'].includes(type)) return { error: 'param type not valid', status: 400 };
25
- if (!objectid) return { error: 'id required', status: 400 };
24
+ if (!['comment', 'checklist', 'file', 'gallery'].includes(type)) return { message: 'param type not valid', status: 400 };
25
+ if (!objectid) return { message: 'id required', status: 400 };
26
26
 
27
27
  const table = tableList[type];
28
28
 
@@ -57,6 +57,7 @@ export default async function widgetSet(req) {
57
57
 
58
58
  const data = { ...body, uid: user?.uid, entity_id: objectid };
59
59
 
60
+ await applyHook('onWidgetSet', { req, type, data });
60
61
  const result = id
61
62
  ? await dataUpdate({
62
63
  table, data, id, uid: user?.uid,