@opengis/fastify-table 1.3.65 → 1.3.67

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengis/fastify-table",
3
- "version": "1.3.65",
3
+ "version": "1.3.67",
4
4
  "type": "module",
5
5
  "description": "core-plugins",
6
6
  "keywords": [
@@ -3,8 +3,25 @@ import config from '../../../../config.js';
3
3
  const emailReg = /(?:[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-zA-Z0-9-]*[a-zA-Z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/g;
4
4
 
5
5
  function checkField(key, val, options, idx, body) {
6
+ const type = options?.type?.toLowerCase?.();
7
+ const isrequired = options?.validators?.includes?.('required');
8
+
6
9
  // validators: [required]
7
- if (options?.validators?.includes('required') && !val) {
10
+ if (type === 'switcher') {
11
+ // skip nulls at non-required switchers, restrict invalid values
12
+ if (val && typeof val !== 'boolean') {
13
+ return {
14
+ error: 'not a boolean', key, idx,
15
+ };
16
+ }
17
+ // restrict nulls at required switchers
18
+ if (isrequired && typeof val !== 'boolean') {
19
+ return {
20
+ error: 'empty required', key, idx,
21
+ };
22
+ }
23
+ }
24
+ else if (isrequired && !val) {
8
25
  if (options?.conditions) {
9
26
  const allowed = JSON.parse(options?.conditions?.[2] || null);
10
27
  const check = options?.conditions?.[1] === 'in' && Array.isArray(allowed)
@@ -61,4 +78,4 @@ function checkBody({ body = {}, arr = [], idx }) {
61
78
  export default function validateData({ body = {}, schema = {} }) {
62
79
  const res = checkBody({ body, arr: Object.keys(schema).map(key => ({ ...schema[key], key })) });
63
80
  return res;
64
- }
81
+ }
@@ -122,7 +122,7 @@ export default async function loggerSystem(req) {
122
122
  cpu: cpuInfo[0].model,
123
123
  ram: formatMemoryUsage(totalMemory),
124
124
  db: dbVerion,
125
- redis: rclient.server_info?.redis_version,
125
+ redis: rclient?.server_info?.redis_version,
126
126
  node: process.version,
127
127
  },
128
128
  top: topProcess,
@@ -3,7 +3,7 @@ import xssInjection from '../xssInjection.js';
3
3
 
4
4
  function checkXSS({ body, schema = {} }) {
5
5
  const data = typeof body === 'string' ? body : JSON.stringify(body);
6
- const stopWords = xssInjection.filter((el) => data.toLowerCase().includes(el));
6
+ const stopWords = xssInjection.filter((el) => data?.toLowerCase?.()?.includes?.(el));
7
7
 
8
8
  // check sql injection
9
9
  const stopSpecialSymbols = data.match(/\p{S}OR\p{S}|\p{P}OR\p{P}| OR |\+OR\+/gi);
@@ -13,7 +13,7 @@ function checkXSS({ body, schema = {} }) {
13
13
  const skipScreening = config.skipScreening || ['Summernote', 'Tiny', 'Ace', 'Texteditor'];
14
14
  Object.keys(body)
15
15
  .filter((key) => ['<', '>'].find((el) => body[key]?.includes?.(el))
16
- && !skipScreening.includes(schema?.[key]?.type))
16
+ && !skipScreening.includes(schema?.[key]?.type))
17
17
  ?.forEach((key) => {
18
18
  Object.assign(body, { [key]: body[key].replace(/</g, '&lt;').replace(/>/g, '&gt;') });
19
19
  });
@@ -24,9 +24,9 @@ function checkXSS({ body, schema = {} }) {
24
24
 
25
25
  const field = Object.keys(body)
26
26
  ?.find((key) => body[key]?.toLowerCase
27
- && !disabledCheckFields.includes(key)
28
- && (skipScreening.includes(schema?.[key]?.type) ? stopWords.find(el => !['href=', 'src='].includes(el)) : true)
29
- && body[key].toLowerCase().includes(stopWords[0]));
27
+ && !disabledCheckFields.includes(key)
28
+ && (skipScreening.includes(schema?.[key]?.type) ? stopWords.find(el => !['href=', 'src='].includes(el)) : true)
29
+ && body[key].toLowerCase().includes(stopWords[0]));
30
30
  if (field) {
31
31
  console.error(stopWords[0], field, body[field]);
32
32
  return { error: `rule: ${stopWords[0]} | attr: ${field} | val: ${body[field]}`, body };
@@ -0,0 +1,87 @@
1
+ import path from 'node:path';
2
+ import { readdirSync, readFileSync } from 'node:fs';
3
+ import yaml from 'js-yaml';
4
+
5
+ import getTemplatePath from './getTemplatePath.js';
6
+ import loadTemplate from './loadTemplate.js';
7
+ import mdToHTML from '../../md/funcs/mdToHTML.js';
8
+
9
+ function readFileData(file) {
10
+ const data = readFileSync(file, 'utf-8');
11
+ const ext = file.substring(file.lastIndexOf('.') + 1);
12
+
13
+ if (ext === 'yml') {
14
+ return yaml.load(data);
15
+ }
16
+ if (ext === 'json') {
17
+ return JSON.parse(data);
18
+ }
19
+ if (ext === 'md') {
20
+ const match = data.match(/^---\r?\n([\s\S]*?)---\r?\n([\s\S]*)$/);
21
+
22
+ const parseLinesRecursively = (lines, index = 0, result = {}) => (index >= lines.length
23
+ ? result
24
+ : (([key, ...rest]) => (key && rest.length
25
+ ? parseLinesRecursively(lines, index + 1, { ...result, [key.trim()]: rest.join(':').trim() })
26
+ : parseLinesRecursively(lines, index + 1, result)))((lines[index] || '').split(':')));
27
+
28
+ const header = parseLinesRecursively(match[1]?.split?.(/\r?\n/) || []);
29
+ const body = match[2]?.replace(/\r\n/g, '')?.startsWith('```html') ? match[2]?.match?.(/```html\r?\n([\s\S]*?)```/)?.[1] : match[2];
30
+
31
+ return { ...header, html: mdToHTML(body) };
32
+ }
33
+ return data;
34
+ }
35
+
36
+ const isProduction = process.env.NODE_ENV === 'production';
37
+
38
+ function getTemplateData(template) {
39
+ // dir template: dashboard, card
40
+ if (template[0][3]) {
41
+ const files = readdirSync(template[0][1]);
42
+ const data = files.map(async el => readFileData(path.join(template[0][1], el)));
43
+ return files.map((el, i) => [el, data[i]]);
44
+ }
45
+
46
+ // one file template: table, form
47
+ if (template.length === 1) {
48
+ const data = readFileData(template[0][1]);
49
+ return data;
50
+ }
51
+
52
+ // multi file template: select, etc
53
+ if (template.length > 1) {
54
+ const data = template.map(el => readFileData(el[1]));
55
+ if (Array.isArray(data[0])) {
56
+ return data[0];
57
+ }
58
+ const result = {};
59
+ template.forEach((el, i) => {
60
+ Object.assign(result, typeof data[i] === 'object' ? data[i] : { [el[2]]: data[i] });
61
+ });
62
+ return result;
63
+ }
64
+ return null;
65
+ }
66
+ export default function getTemplateSync(type, name) {
67
+ if (!type) return null;
68
+ if (!name) return null;
69
+
70
+ const key = `${type}:${name}`;
71
+ if (name === 'cache' && !isProduction) return loadTemplate; // all cache debug
72
+ if (loadTemplate[key] && isProduction && false) return loadTemplate[key]; // from cache
73
+
74
+ // type one or multi
75
+ const templateList = Array.isArray(type)
76
+ ? type.map(el => getTemplatePath(el)).filter(list => list?.filter(el => el[0] === name).length)[0] || []
77
+ : getTemplatePath(type);
78
+
79
+ // find template
80
+ const template = templateList?.filter(el => el[0] === name);
81
+ if (name === 'list' && !isProduction) return templateList; // all template debug
82
+
83
+ if (!template.length) return null; // not found
84
+
85
+ loadTemplate[key] = getTemplateData(template);
86
+ return loadTemplate[key];
87
+ }
@@ -3,7 +3,9 @@ import {
3
3
  } from '../../../../utils.js';
4
4
 
5
5
  export default async function deleteCrud(req, reply) {
6
- const { pg = pgClients.client, user, params = {}, headers = {} } = req || {};
6
+ const {
7
+ pg = pgClients.client, user, params = {}, headers = {},
8
+ } = req || {};
7
9
 
8
10
  const hookData = await applyHook('preDelete', {
9
11
  pg, table: params?.table, id: params?.id, user,
@@ -36,7 +38,7 @@ export default async function deleteCrud(req, reply) {
36
38
  }).catch(err => {
37
39
  if (err.message?.includes?.('foreign key' || 'unique')) {
38
40
  const constraint = err.message.match(/constraint "([^"]+)"/g);
39
- return reply.status(400).send('Видалення заборонено для збереження цілісності БД: ' + constraint);
41
+ return reply.status(400).send(`Видалення заборонено для збереження цілісності БД: ${constraint}`);
40
42
  }
41
43
  if (config.trace) console.error(err.toString());
42
44
  return err.toString();
@@ -2,13 +2,17 @@ import {
2
2
  applyHook, getAccess, getTemplate, checkXSS, dataInsert, getToken, config, pgClients, logger, validateData,
3
3
  } from '../../../../utils.js';
4
4
 
5
- export default async function insert(req) {
5
+ export default async function insert(req, reply) {
6
6
  const {
7
7
  pg = pgClients.client, user = {}, params = {}, body = {}, headers = {},
8
8
  } = req || {};
9
- if (!user) return { message: 'access restricted', status: 403 };
9
+ if (!user) {
10
+ return reply.status(403).send('access restricted');
11
+ }
10
12
 
11
- const hookData = await applyHook('preInsert', { pg, table: params?.table, user, body });
13
+ const hookData = await applyHook('preInsert', {
14
+ pg, table: params?.table, user, body,
15
+ });
12
16
 
13
17
  if (hookData?.message && hookData?.status) {
14
18
  return { message: hookData?.message, status: hookData?.status };
@@ -23,12 +27,12 @@ export default async function insert(req) {
23
27
 
24
28
  const { actions = [] } = await getAccess({ table: add, user }, pg) || {};
25
29
 
26
- if (!actions.includes('add') && !config?.local && !tokenData) {
27
- return { message: 'access restricted', status: 403 };
30
+ if (!actions.includes('add') && !config.local && !config.debug && !tokenData) {
31
+ return reply.status(403).send('access restricted');
28
32
  }
29
33
 
30
34
  if (!add) {
31
- return { message: 'table is required', status: 400 };
35
+ return reply.status(400).send('table is required');
32
36
  }
33
37
 
34
38
  const loadTemplate = await getTemplate('table', add);
@@ -43,8 +47,10 @@ export default async function insert(req) {
43
47
  const xssCheck = checkXSS({ body, schema });
44
48
 
45
49
  if (xssCheck.error && formData?.xssCheck !== false) {
46
- logger.file('injection/xss', { table, form: form || loadTemplate?.form, body, uid: user?.uid, msg: xssCheck.error });
47
- return { message: 'Дані містять заборонені символи. Приберіть їх та спробуйте ще раз', status: 409 };
50
+ logger.file('injection/xss', {
51
+ table, form: form || loadTemplate?.form, body, uid: user?.uid, msg: xssCheck.error,
52
+ });
53
+ return reply.status(409).send('Дані містять заборонені символи. Приберіть їх та спробуйте ще раз');
48
54
  }
49
55
 
50
56
  const fieldCheck = validateData({ body, schema });
@@ -56,7 +62,7 @@ export default async function insert(req) {
56
62
  uid: user?.uid,
57
63
  ...fieldCheck,
58
64
  });
59
- return { message: 'Дані не пройшли валідацію. Приберіть некоректні дані та спробуйте ще раз', status: 409 };
65
+ return reply.status(409).send('Дані не пройшли валідацію. Приберіть некоректні дані та спробуйте ще раз');
60
66
  }
61
67
 
62
68
  if (![add, table].includes('admin.users')) {
@@ -76,7 +82,10 @@ export default async function insert(req) {
76
82
  tokenData,
77
83
  referer,
78
84
  });
79
- if (!res) return { message: 'nothing added ' };
85
+
86
+ if (!res) {
87
+ return reply.status(400).send('nothing added');
88
+ }
80
89
 
81
90
  // admin.custom_column
82
91
  await applyHook('afterInsert', {
@@ -33,17 +33,20 @@ export default async function tableAPI(req) {
33
33
 
34
34
  if (tokenData && !id) return { message: {} };
35
35
  if (!tableName && !id) {
36
- return { message: 'not enough params', status: 400 };
36
+ return reply.status(400).send('not enough params');
37
37
  }
38
38
 
39
39
  const { actions = [], query: accessQuery } = await getAccess({ table: templateName, id, user }, pg) || {};
40
40
 
41
41
  if (!actions.includes('edit') && !config?.local && !tokenData) {
42
- return { message: 'access restricted', status: 403 };
42
+ return reply.status(403).send('access restricted');
43
43
  }
44
44
 
45
45
  const { pk, columns: dbColumns = [] } = await getMeta({ pg, table: tableName });
46
- if (!pk) return { message: `table not found: ${table}`, status: 404 };
46
+
47
+ if (!pk) {
48
+ return reply.status(404).send(`table not found: ${table}`);
49
+ }
47
50
 
48
51
  // const cols = columns.map((el) => el.name || el).join(',');
49
52
  const formName = hookData?.form || tokenData?.form || form;
@@ -72,7 +75,10 @@ export default async function tableAPI(req) {
72
75
  }
73
76
 
74
77
  const data = await pg.query(q.replace(/{{uid}}/, user?.uid), [id]).then(el => el.rows[0]);
75
- if (!data) return { message: 'not found', status: 404 };
78
+
79
+ if (!data) {
80
+ return reply.status(404).send(`object not found: ${id}`);
81
+ }
76
82
 
77
83
  Object.keys(schema).filter(key => schema[key]?.type === 'DataTable').forEach(key => {
78
84
  if (data[key] && !Array.isArray(data[key])) { data[key] = null; }
@@ -4,12 +4,15 @@ import {
4
4
  import config from '../../../../config.js';
5
5
  import insert from './insert.js';
6
6
 
7
- export default async function update(req) {
7
+ export default async function update(req, reply) {
8
8
  const {
9
9
  pg = pgClients.client, user, params = {}, body = {}, headers = {}, unittest,
10
10
  } = req;
11
11
 
12
- if (!user) return { message: 'access restricted', status: 403 };
12
+ if (!user) {
13
+ return reply.status(403).send('access restricted');
14
+ }
15
+
13
16
  const hookData = await applyHook('preUpdate', {
14
17
  pg, table: params?.table, id: params?.id, user,
15
18
  });
@@ -27,19 +30,20 @@ export default async function update(req) {
27
30
 
28
31
  const { actions = [] } = await getAccess({ table: edit, id, user }, pg) || {};
29
32
 
30
- if (!actions.includes('edit') && !config?.local && !tokenData) {
31
- return { message: 'access restricted', status: 403 };
33
+ if (!actions.includes('edit') && !config.local && !config.debug && !tokenData) {
34
+ return reply.status(403).send('access restricted');
32
35
  }
33
36
 
34
37
  if (!edit) {
35
- return { message: 'table is required', status: 400 };
38
+ return reply.status(400).send('table is required');
36
39
  }
37
40
 
38
41
  if (!id && tokenData?.table) {
39
42
  return insert(req);
40
43
  }
44
+
41
45
  if (!id) {
42
- return { message: 'id is required', status: 404 };
46
+ return reply.status(400).send('id is required');
43
47
  }
44
48
 
45
49
  const loadTemplate = await getTemplate('table', edit);
@@ -59,7 +63,7 @@ export default async function update(req) {
59
63
 
60
64
  if (xssCheck.error && formData?.xssCheck !== false) {
61
65
  logger.file('injection/xss', { msg: xssCheck.error, table }, req);
62
- return { message: 'Дані містять заборонені символи. Приберіть їх та спробуйте ще раз', status: 409 };
66
+ return reply.status(409).send('Дані містять заборонені символи. Приберіть їх та спробуйте ще раз');
63
67
  }
64
68
 
65
69
  const fieldCheck = validateData({ body, schema });
@@ -71,7 +75,7 @@ export default async function update(req) {
71
75
  uid: user?.uid,
72
76
  ...fieldCheck,
73
77
  });
74
- return { message: 'Дані не пройшли валідацію. Приберіть некоректні дані та спробуйте ще раз', status: 409 };
78
+ return reply.status(409).send('Дані не пройшли валідацію. Приберіть некоректні дані та спробуйте ще раз');
75
79
  }
76
80
 
77
81
  const res = await dataUpdate({
@@ -100,7 +104,9 @@ export default async function update(req) {
100
104
  // insert new extra data
101
105
  if (Array.isArray(body[key]) && body[key]?.length) {
102
106
  const extraRows = await Promise.all(body[key]?.map?.(async (row) => {
103
- const extraRes = await dataInsert({ pg, table: schema[key].table, data: { ...row, [schema[key].parent_id]: objId }, uid, tokenData, referer });
107
+ const extraRes = await dataInsert({
108
+ pg, table: schema[key].table, data: { ...row, [schema[key].parent_id]: objId }, uid, tokenData, referer,
109
+ });
104
110
  return extraRes?.rows?.[0];
105
111
  }));
106
112
  Object.assign(res.extra, { [key]: extraRows.filter((el) => el) });
package/utils.js CHANGED
@@ -19,6 +19,7 @@ import redisClients from './server/plugins/redis/funcs/redisClients.js';
19
19
 
20
20
  // template
21
21
  import getTemplate from './server/plugins/table/funcs/getTemplate.js';
22
+ import getTemplateSync from './server/plugins/table/funcs/getTemplateSync.js';
22
23
  import getTemplates from './server/plugins/table/funcs/getTemplates.js';
23
24
  import getTemplatePath from './server/plugins/table/funcs/getTemplatePath.js';
24
25
  import addTemplateDir from './server/plugins/table/funcs/addTemplateDir.js';
@@ -104,6 +105,7 @@ export {
104
105
 
105
106
  // template
106
107
  getTemplate,
108
+ getTemplateSync,
107
109
  getTemplates,
108
110
  getTemplatePath,
109
111
  addTemplateDir,