@opengis/fastify-table 1.1.38 → 1.1.40

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/helper.js CHANGED
@@ -1,11 +1,11 @@
1
- // This file contains code that we reuse
2
- // between our tests.
1
+ import path from 'node:path';
3
2
  import Fastify from 'fastify';
4
3
  import config from './test/config.js';
5
4
  import appService from './index.js';
6
5
 
7
6
  import rclient from './redis/client.js';
8
7
  import pgClients from './pg/pgClients.js';
8
+ import { addTemplateDir } from './utils.js';
9
9
 
10
10
  // automatically build and tear down our instance
11
11
  async function build(t) {
@@ -14,6 +14,8 @@ async function build(t) {
14
14
  process.env.NODE_ENV = 'production';
15
15
  const app = Fastify({ logger: false });
16
16
  app.register(appService, config);
17
+ const cwd = process.cwd();
18
+ addTemplateDir(path.join(cwd, 'module/test'));
17
19
  // close the app after we are done
18
20
  t.after(() => {
19
21
  // console.log('close app');
@@ -0,0 +1,26 @@
1
+ [
2
+ {
3
+ "id": "2",
4
+ "text": "Хвойні",
5
+ "en": "Conifers",
6
+ "color": "#006400"
7
+ },
8
+ {
9
+ "id": "3",
10
+ "text": "Чагарники",
11
+ "en": "Shrubs",
12
+ "color": "#6B8E23"
13
+ },
14
+ {
15
+ "id": "4",
16
+ "text": "Плодові",
17
+ "en": "Fruit",
18
+ "color": "#808000"
19
+ },
20
+ {
21
+ "id": "1",
22
+ "text": "Листяні",
23
+ "en": "Deciduous",
24
+ "color": "#228B22"
25
+ }
26
+ ]
@@ -0,0 +1,265 @@
1
+ {
2
+ "key": "rz_id",
3
+ "table": "itree.rest_zones",
4
+ "query": "verif",
5
+ "sql": [
6
+ {
7
+ "sql": "select json_agg(file_path) as img_list from admin.doc_file where object_id=t.rz_id and file_status=1",
8
+ "name": "image_sql"
9
+ }
10
+ ],
11
+ "form": "rest_zone.form",
12
+ "meta": {
13
+ "bbox": "geom",
14
+ "search": "name",
15
+ "title": "name"
16
+ },
17
+ "columns": [
18
+ {
19
+ "ua": "Назва об'єкта",
20
+ "title": "Назва об'єкта",
21
+ "name": "name",
22
+ "meta": "title",
23
+ "html": "<a href=\"/rest_zone/mode=card/view={{rz_id}}\">{{name}}</a>",
24
+ "format": "html"
25
+ },
26
+ {
27
+ "en": "Green zone category",
28
+ "ua": "Категорія зеленої зони",
29
+ "meta": "category",
30
+ "name": "category",
31
+ "data": "itree.recrzone_category",
32
+ "format": "select"
33
+ },
34
+ {
35
+ "ru": "Вид насаджень",
36
+ "ua": "Вид насаджень",
37
+ "name": "plant_type",
38
+ "meta": "type",
39
+ "data": "itree.plant_type",
40
+ "format": "select"
41
+ },
42
+ {
43
+ "name": "uid",
44
+ "ua": "Хто додав",
45
+ "width": "70",
46
+ "data": "contact_id",
47
+ "format": "select"
48
+ },
49
+ {
50
+ "ru": "Балансоутримувач об'єкта",
51
+ "ua": "Балансоутримувач об'єкта",
52
+ "hidden": true,
53
+ "meta": "other",
54
+ "name": "balancer",
55
+ "format": "text"
56
+ },
57
+ {
58
+ "ua": "Відповідальний за утримання",
59
+ "name": "maintain_respons_gp",
60
+ "meta": "other",
61
+ "hidden": true,
62
+ "format": "text"
63
+ },
64
+ {
65
+ "ua": "Площа, га",
66
+ "name": "area",
67
+ "meta": "other",
68
+ "format": "text"
69
+ },
70
+ {
71
+ "ru": "Форма власності",
72
+ "ua": "Форма власності",
73
+ "name": "ownership",
74
+ "meta": "other",
75
+ "data": "itree.ownership",
76
+ "format": "select"
77
+ },
78
+ {
79
+ "ru": "Категорія земель",
80
+ "ua": "Категорія земель",
81
+ "name": "landcategory",
82
+ "meta": "other",
83
+ "hidden": true,
84
+ "data": "itree.landcategory",
85
+ "format": "select"
86
+ },
87
+ {
88
+ "ru": "Цільве призначення",
89
+ "ua": "Цільве призначення",
90
+ "name": "purpose",
91
+ "meta": "other",
92
+ "hidden": true,
93
+ "data": "itree.purpose_type",
94
+ "format": "select"
95
+ },
96
+ {
97
+ "ru": "Обмеження щодо використання",
98
+ "ua": "Обмеження щодо використання",
99
+ "name": "restriction",
100
+ "hidden": true,
101
+ "meta": "other",
102
+ "data": "itree.restriction",
103
+ "format": "select"
104
+ },
105
+ {
106
+ "ru": "Площа, зайнята зеленими насадженнями, кв.м",
107
+ "ua": "Площа, зайнята зеленими насадженнями, кв.м",
108
+ "name": "plants_square",
109
+ "meta": "other",
110
+ "hidden": true,
111
+ "format": "text"
112
+ },
113
+ {
114
+ "ru": "Загальний стан насаджень",
115
+ "ua": "Загальний стан насаджень",
116
+ "name": "plants_cond",
117
+ "meta": "other",
118
+ "data": "itree.plants_cond",
119
+ "format": "badge"
120
+ },
121
+ {
122
+ "en": "Photo",
123
+ "ru": "Фото",
124
+ "ua": "Фото",
125
+ "html": "{{#each img_list}}<a class='btn btn-xs btn-default' href=\"{{this}}\" download><i class='fa fa-file-text'></i></a><a target=\"_blank\" class='btn btn-xs btn-default' href=\"{{this}}\"><i class='fa fa-eye'></i></a><br>{{/each}}",
126
+ "name": "image",
127
+ "format": "html"
128
+ },
129
+ {
130
+ "ru": "Видовий склад рослин",
131
+ "ua": "Видовий склад рослин",
132
+ "name": "composition",
133
+ "hidden": true,
134
+ "meta": "other",
135
+ "data": "itree.composition",
136
+ "format": "badge"
137
+ },
138
+ {
139
+ "ru": "Кількість особливо цінних порід та чагарників, шт",
140
+ "ua": "Кількість особливо цінних порід та чагарників, шт",
141
+ "name": "espec_valuable",
142
+ "hidden": true,
143
+ "meta": "other",
144
+ "format": "text"
145
+ },
146
+ {
147
+ "ru": "Повнота насаджень (кількість дерев на 1 гектар), шт/га",
148
+ "ua": "Повнота насаджень (кількість дерев на 1 гектар), шт/га",
149
+ "name": "planting_density",
150
+ "hidden": true,
151
+ "meta": "other",
152
+ "format": "text"
153
+ },
154
+ {
155
+ "ru": "Площа, зайнята деревами, кв.м",
156
+ "ua": "Площа, зайнята деревами, кв.м",
157
+ "name": "tree_square",
158
+ "hidden": true,
159
+ "meta": "other",
160
+ "format": "text"
161
+ },
162
+ {
163
+ "ru": "Балансова вартість",
164
+ "ua": "Балансова вартість",
165
+ "name": "book_value",
166
+ "meta": "other",
167
+ "hidden": true,
168
+ "format": "text"
169
+ },
170
+ {
171
+ "ru": "Кадастровий номер",
172
+ "ua": "Кадастровий номер",
173
+ "meta": "other",
174
+ "name": "cadnum",
175
+ "hidden": true,
176
+ "format": "text"
177
+ },
178
+ {
179
+ "ru": "Нормативна грошова оцінка",
180
+ "ua": "Нормативна грошова оцінка",
181
+ "name": "monetary_valuation",
182
+ "meta": "other",
183
+ "hidden": true,
184
+ "format": "text"
185
+ },
186
+ {
187
+ "ua": "Верифіковано",
188
+ "html": " {{#ifCond verif '==' true}} \r\n <span class=\"label\" style=\"color:#fff;background:green\"> Так</span> \r\n {{else}} \r\n <span class=\"label\" style=\"color:#fff;background:red\"> Ні</span>\r\n {{/ifCond}}",
189
+ "name": "verif",
190
+ "format": "check"
191
+ }
192
+ ],
193
+ "title": "Зони відпочинку",
194
+ "filterList": [
195
+ {
196
+ "ua": "Назва",
197
+ "name": "name",
198
+ "type": "Text"
199
+ },
200
+ {
201
+ "ua": "Категорія зеленої зони",
202
+ "data": "itree.recrzone_category",
203
+ "name": "category",
204
+ "sort": "asc",
205
+ "type": "Check"
206
+ },
207
+ {
208
+ "ua": "Вид насаджень",
209
+ "data": "itree.plant_type",
210
+ "name": "plant_type",
211
+ "sort": "asc",
212
+ "type": "Check"
213
+ },
214
+ {
215
+ "ua": "Загальний стан насаджень",
216
+ "data": "itree.plants_cond",
217
+ "name": "plants_cond",
218
+ "sort": "asc",
219
+ "type": "Check"
220
+ },
221
+ {
222
+ "ua": "Видовий склад рослин",
223
+ "data": "itree.composition",
224
+ "name": "composition",
225
+ "sort": "asc",
226
+ "type": "Check"
227
+ },
228
+ {
229
+ "ua": "Форма власності",
230
+ "data": "itree.ownership",
231
+ "name": "ownership",
232
+ "type": "Check"
233
+ },
234
+ {
235
+ "ua": "Категорія земель",
236
+ "data": "itree.landcategory",
237
+ "name": "landcategory",
238
+ "type": "Check"
239
+ },
240
+ {
241
+ "ua": "Цільове призначення",
242
+ "data": "itree.purpose",
243
+ "name": "purpose",
244
+ "type": "Check"
245
+ }
246
+ ],
247
+ "filterCustom": [
248
+ {
249
+ "name": "actual",
250
+ "label": "Поточний рік",
251
+ "sql": "cdate > '2020-01-01'::date"
252
+ }
253
+ ],
254
+ "filterState": [
255
+ {
256
+ "name": "actual",
257
+ "label": "Поточний рік 2",
258
+ "sql": "cdate > '2022-09-12'::date"
259
+ }
260
+ ],
261
+ "actions": [
262
+ "edit",
263
+ "del"
264
+ ]
265
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengis/fastify-table",
3
- "version": "1.1.38",
3
+ "version": "1.1.40",
4
4
  "type": "module",
5
5
  "description": "core-plugins",
6
6
  "main": "index.js",
@@ -53,10 +53,13 @@ export default async function dataAPI(req) {
53
53
  });
54
54
  }
55
55
 
56
- const fData = query.filter || query.search ? await getFilterSQL({
56
+ const checkFilter = [query.filter, query.state, query.search, query.custom].filter((el) => el).length;
57
+ const fData = checkFilter ? await getFilterSQL({
58
+ table: params.table,
57
59
  filter: query.filter,
60
+ state: query.state,
58
61
  search: query.search,
59
- table: params.table,
62
+ custom: query.custom,
60
63
  json: 1,
61
64
  }) : {};
62
65
 
@@ -69,15 +72,13 @@ export default async function dataAPI(req) {
69
72
  const [orderColumn, orderDir] = (query.order || loadTable.order || '').split(/[- ]/);
70
73
 
71
74
  const order = columnList.includes(orderColumn) && orderColumn?.length ? `order by ${orderColumn} ${query.desc || orderDir === 'desc' ? 'desc' : ''}` : '';
72
- const state = loadTable.filterState && query.state ? loadTable.filterState[query.state]?.sql : null;
73
- const custom = loadTable.filterCustom && query.custom ? loadTable.filterCustom[query.custom]?.sql : null;
74
75
  const search = loadTable.meta?.search && query.search ? `(${loadTable.meta?.search.split(',').map(el => `${el} ilike '%${query.search}%'`).join(' or ')})` : null;
75
76
  const queryBbox = query?.bbox ? query.bbox.replace(/ /g, ',').split(',')?.map((el) => el - 0) : [];
76
77
  const queryPolyline = meta?.bbox && query?.polyline ? `ST_Contains(ST_MakePolygon(ST_LineFromEncodedPolyline('${query?.polyline}')),${meta.bbox})` : undefined;
77
78
  const bbox = meta?.bbox && queryBbox.filter((el) => !Number.isNaN(el))?.length === 4 ? `${meta.bbox} && 'box(${queryBbox[0]} ${queryBbox[1]},${queryBbox[2]} ${queryBbox[3]})'::box2d ` : undefined;
78
79
 
79
80
  const access = await getAccess(req, params.table);
80
- const where = [(opt?.id || params.id ? ` "${pk}" = $1` : null), keyQuery, loadTable.query, fData.q, state, custom, search, access?.query || '1=1', bbox, queryPolyline].filter((el) => el);
81
+ const where = [(opt?.id || params.id ? ` "${pk}" = $1` : null), keyQuery, loadTable.query, fData.q, search, access?.query || '1=1', bbox, queryPolyline].filter((el) => el);
81
82
  const cardColumns = cardSqlFiltered.length ? `,${cardSqlFiltered.map((el) => el.name)}` : '';
82
83
  const q = `select ${pk ? `"${pk}" as id,` : ''} ${columnList.includes('geom') ? 'st_asgeojson(geom)::json as geom,' : ''} ${query.id || query.key ? '*' : sqlColumns || cols || '*'} ${metaCols} ${cardColumns} from ${table} t ${sqlTable} ${cardSqlTable} where ${where.join(' and ') || 'true'} ${order} ${offset} limit ${limit}`;
83
84
 
@@ -5,19 +5,20 @@ import { applyHook } from '../../utils.js';
5
5
 
6
6
  export default async function tableAPI(req) {
7
7
  const {
8
- pg, params = {}, query = {}, opt = {},
8
+ pg, params = {}, query = {},
9
9
  } = req;
10
10
  if (!params.id) return { message: 'not enough params', status: 400 };
11
11
 
12
- const loadTable = await getTemplate('table', opt?.table || params.table) || {};
13
- if (!loadTable) {
14
- if (!pg.pk?.[opt?.table || params.table]) { return { message: 'not found', status: 404 }; }
15
- }
16
-
17
12
  const check = await applyHook('preTable', { req });
18
13
  if (check?.message && check?.status) {
19
14
  return { message: check?.message, status: check?.status };
20
15
  }
16
+ const { opt = {} } = req;
17
+
18
+ const loadTable = await getTemplate('table', opt?.table || params.table) || {};
19
+ if (!loadTable) {
20
+ if (!pg.pk?.[opt?.table || params.table]) { return { message: 'not found', status: 404 }; }
21
+ }
21
22
 
22
23
  const {
23
24
  table, /* columns, */ form,
@@ -1,18 +1,13 @@
1
- /* const getTable = require('../../controllers/utils/getTable');
2
- const pg = require('../../../pg/client');
3
- const config = require('../../../../config'); */
4
-
5
1
  import getTemplate from '../../controllers/utils/getTemplate.js';
6
2
  import pgClients from '../../../pg/pgClients.js';
7
- import config from '../../../config.js';
3
+
8
4
  // filter util
9
5
  import getTableSql from './util/getTableSql.js';
10
- // import getCustomQuery from './util/getCustomQuery.js';
11
6
  import getFilterQuery from './util/getFilterQuery.js';
12
7
  import getOptimizedQuery from './util/getOptimizedQuery.js';
13
8
 
14
9
  async function getFilterSQL({
15
- table, filter, pg = pgClients.client, search, filterList, query,
10
+ table, filter, pg = pgClients.client, search, filterList, query, custom, state,
16
11
  }) {
17
12
  if (!table) return { error: 'param table is required', status: 400 };
18
13
 
@@ -25,8 +20,7 @@ async function getFilterSQL({
25
20
  return ` left join lateral (${el.filter ? el.sql.replace(/limit 1/ig, '') : el.sql}) as ${el.name} on 1=1 `;
26
21
  }).join(' ')
27
22
  : '';
28
- const fieldQuery = config.allTemplates?.table?.[table] ? `${config.allTemplates?.table?.[table]} limit 0`
29
- : `select * from ${body?.table || table} ${sqlList ? ` t ${sqlList}` : ''} where 1=1 limit 0`;
23
+ const fieldQuery = `select * from ${body?.table || table} ${sqlList ? ` t ${sqlList}` : ''} where 1=1 limit 0`;
30
24
  const { fields = [] } = await pg.query(fieldQuery);
31
25
 
32
26
  const { fields: fieldsModel } = pg.pk[body?.table] ? await pg.query(`select * from ${body.table} limit 0`) : {};
@@ -45,18 +39,36 @@ async function getFilterSQL({
45
39
  return pk && !fieldsList.includes(name) ? `${pk} in (select ${pk} from (${fieldQuery})q where ${name} ${sval})` : `${name} ${sval}`;
46
40
  }).join(' or ')} )` : '';
47
41
 
42
+ const filterList1 = await Promise.all((filterList || (body?.filter_list || []).concat(body?.filterInline || []).concat(body?.filterCustom || []).concat(body?.filterState || []).concat(body?.filterList || [])
43
+ .concat(body?.filters || [])) /* .concat(extraFilters || []).concat(customFilters || []) */
44
+
45
+ ?.map(async (el) => {
46
+ if (!el?.data) return el;
47
+ const cls = await getTemplate(['cls', 'select'], el.data);
48
+ if (Array.isArray(cls) && cls?.length) {
49
+ Object.assign(el, { options: cls });
50
+ }
51
+ else if (typeof (cls?.sql || cls) === 'string') {
52
+ Object.assign(el, { sql: cls?.sql || cls });
53
+ }
54
+ return el;
55
+ }));
56
+
48
57
  const filters = getFilterQuery({
58
+ pg,
49
59
  filter,
60
+ table,
50
61
  tableSQL,
51
62
  fields,
52
- filterList: filterList || (body?.filter_list || []).concat(body?.filterInline || []).concat(body?.filterCustom || []).concat(body?.filterState || []).concat(body?.filterList || []),
53
- pg,
54
- config,
63
+ filterList: filterList1,
55
64
  });
56
65
 
57
66
  // filter
67
+ const customQuery = body?.filterCustom?.length && custom ? body.filterCustom?.find((el) => el.name === custom)?.sql : null;
68
+ const stateQuery = body?.filterState?.length && state ? body.filterState?.find((el) => el.name === state)?.sql : null;
69
+
58
70
  const filterQuery = filters?.filter((el) => el.query)?.map((el) => `${el.query} `).join(' and ');
59
- const q = [body?.query, query, searchQuery, filterQuery].filter((el) => el).join(' and ');
71
+ const q = [body?.query, query, searchQuery, filterQuery, stateQuery, customQuery].filter((el) => el).join(' and ');
60
72
 
61
73
  // table
62
74
  const modelQuery = body?.model || body?.table || table;
@@ -19,9 +19,15 @@ function formatDateISOString(date) {
19
19
  }
20
20
 
21
21
  function formatValue({
22
- filterType, filter, name, value, operator = '=', fieldType = 'text', uid = 1, optimize,
22
+ pg, table, filter = {}, name, value, operator = '=', dataTypeID, uid = 1, optimize,
23
23
  }) {
24
- if (!name || !value) return {};
24
+ const { data, sql, extra } = filter;
25
+ const pk = pg?.pk && table ? pg.pk[table] : undefined;
26
+
27
+ if (!dataTypeID && !extra) return {};
28
+ const fieldType = extra ? pg.pgType?.[{ Date: 1114 }[filter?.type] || 25] : pg.pgType?.[dataTypeID];
29
+ if (!name || !value || !fieldType) return {};
30
+ const filterType = filter.type?.toLowerCase();
25
31
 
26
32
  // current day, week, month, year etc.
27
33
  if (dateTypeList.includes(fieldType) && !value?.includes('_') && ['cd', 'cw', 'cm', 'cq', 'cy'].includes(value)) {
@@ -32,26 +38,32 @@ function formatValue({
32
38
  cq: `${name}::date >= '${dt(dp.y, dp.q, 1)}'::date and ${name} <= '${dt(dp.y, dp.q + 3, 0)}'::date`,
33
39
  cy: `${name}::date >= '${dt(dp.y, 0, 1)}'::date and ${name}::date <= '${dt(dp.y, 11, 31)}'::date`,
34
40
  }[value];
35
- return { op: '=', query };
41
+ return { op: '=', query, extra };
36
42
  }
43
+
37
44
  // date range
38
45
  if (dateTypeList.includes(fieldType) && value?.includes('_')) {
39
46
  const [min, max] = value.split('_');
40
47
  const query = `${name} >= '${min}'::date and ${name} <= '${max}'::date`;
41
- return { op: 'between', query };
48
+ return { op: 'between', query, extra };
42
49
  }
50
+
43
51
  // v3 filter date range, example - "01.01.2024-31.12.2024"
44
52
  if (dateTypeList.includes(fieldType) && value?.includes('.') && value?.indexOf('-') === 10 && value?.length === 21) {
45
53
  const [startDate, endDate] = value.split('-');
46
54
  const min = formatDateISOString(startDate);
47
55
  const max = formatDateISOString(endDate);
48
- const query = `${name}::date >= '${min}'::date and ${name}::date <= '${max}'::date`;
49
- return { op: 'between', query };
56
+ const query = extra && pk
57
+ ? `${pk} in (select object_id from crm.extra_data where property_key='${name}' and value_date::date >= '${min}'::date and value_date::date <= '${max}'::date)`
58
+ : `${name}::date >= '${min}'::date and ${name}::date <= '${max}'::date`;
59
+ return { op: 'between', query, extra };
50
60
  }
61
+
51
62
  // my rows
52
63
  if (value === 'me' && uid && fieldType === 'text') {
53
- return { op: '=', query: `${name}::text = '${uid}'` };
64
+ return { op: '=', query: extra ? `uid = '${uid}'` : `${name}::text = '${uid}'`, extra };
54
65
  }
66
+
55
67
  const formatType = {
56
68
  float8: 'numeric',
57
69
  int4: 'numeric',
@@ -68,41 +80,47 @@ function formatValue({
68
80
  query: fieldType?.includes('[]')
69
81
  ? `${optimize.pk} && (select array_agg(${optimize.pk}) from ${optimize.table} where ${name} ${val} )`
70
82
  : `${optimize.pk} in (select ${optimize.pk} from ${optimize.table} where ${name} ${val} )`,
83
+ extra,
71
84
  };
72
85
  }
73
86
 
74
- if (filter.sql) {
75
- return { op: '~', query: filter.sql.replace('{val}', value) };
76
- }
77
87
  if (fieldType?.includes('[]')) {
78
- return { op: 'in', query: `'{${value}}'::text[] && ${name}::text[]` };
88
+ return { op: 'in', query: `'{${value}}'::text[] && ${name}::text[]`, extra };
79
89
  }
90
+
80
91
  // multiple items of 1 param
81
92
  if (value?.indexOf(',') !== -1) {
82
93
  const values = value.split(',').filter((el) => el !== 'null');
83
- const query = value?.indexOf('null' !== -1)
94
+ if (extra && pk) {
95
+ const query = value?.indexOf('null') !== -1
96
+ ? `${pk} in (select object_id from crm.extra_data where property_key='${name}' and ( value_text is null or value_text in (${values?.map((el) => `'"${el}"'`).join(',')}) ) )`
97
+ : `${pk} in (select object_id from crm.extra_data where property_key='${name}' and value_text in (${values?.map((el) => `'"${el}"'`).join(',')}) )`;
98
+ return { op: 'in', query, extra };
99
+ }
100
+ const query = value?.indexOf('null') !== -1
84
101
  ? `( ${name} is null or ${name}::text in (${values?.map((el) => `'${el}'`).join(',')}) )`
85
102
  : `${name}::text in (${value.split(',')?.map((el) => `'${el}'`).join(',')})`;
86
- return { op: 'in', query };
103
+ return { op: 'in', query, extra };
87
104
  }
88
105
 
89
106
  // v3 filter number range, example - "100_500"
90
107
  if (numberTypeList.includes(fieldType) && value?.indexOf('_') !== -1) {
91
108
  const [min, max] = value.split('_');
92
109
  const query = (max === 'max' ? `${name} > ${min}` : null) || (min === 'min' ? `${name} < ${max}` : null) || `${name} between ${min} and ${max}`;
93
- return { op: 'between', query };
110
+ return { op: 'between', query, extra };
94
111
  }
112
+
95
113
  // number range
96
114
  if (numberTypeList.includes(fieldType) && value?.indexOf('-') !== -1) {
97
115
  const [min, max] = value.split('-');
98
116
  if (min === 'min' && max === 'max') return {};
99
117
  const query = (max === 'max' ? `${name} > ${min}` : null) || (min === 'min' ? `${name} < ${max}` : null) || `${name} between ${min} and ${max}`;
100
- return { op: 'between', query };
118
+ return { op: 'between', query, extra };
101
119
  }
102
120
 
103
121
  if (['<', '>'].includes(operator)) {
104
122
  const query = `${name} ${operator} '${value}'::${formatType}`;
105
- return { op: operator, query };
123
+ return { op: operator, query, extra };
106
124
  }
107
125
 
108
126
  if (operator === '=' && filterType !== 'text' && !filter?.data) {
@@ -110,12 +128,19 @@ function formatValue({
110
128
  null: `${name} is null`,
111
129
  notnull: `${name} is not null`,
112
130
  }[value] || `${name}::${formatType}='${value}'::${formatType}`;
113
- return { op: '=', query };
131
+ return { op: '=', query, extra };
114
132
  }
115
133
 
116
134
  if (['~', '='].includes(operator)) {
117
135
  const operator1 = (filterType === 'text' && (filter?.id || filter?.name) && operator === '=' ? '~' : operator);
118
136
  const match = operator1 === '=' ? `='${value}'` : `ilike '%${value}%'`;
137
+ if (extra && pk) {
138
+ const query = data && sql
139
+ ? `${pk} in (select object_id from crm.extra_data where property_key='${name}' and value_text in ( ( with q(id,name) as (${sql}) select id from q where name ${match})))`
140
+ : `${pk} in (select object_id from crm.extra_data where property_key='${name}' and value_text ${match})`;
141
+ return { op: 'ilike', query, extra };
142
+ }
143
+
119
144
  const query = filter?.data && filter?.sql
120
145
  ? `${filter?.name || filter?.id} in ( ( with q(id,name) as (${filter?.sql}) select id from q where name::text ${match}) )` // filter with cls
121
146
  : `${name}::text ${match}`; // simple filter
@@ -127,7 +152,7 @@ function formatValue({
127
152
  if (name.includes('.')) {
128
153
  const [col, prop] = name.split('.');
129
154
  const query = ` ${col}->>'${prop}' in ('${value.join("','")}')`;
130
- return { op: 'in', query };
155
+ return { op: 'in', query, extra };
131
156
  }
132
157
 
133
158
  // geometry
@@ -136,7 +161,7 @@ function formatValue({
136
161
 
137
162
  if (bbox?.length === 4) {
138
163
  const query = ` ${name} && 'box(${bbox[0]} ${bbox[1]},${bbox[2]} ${bbox[3]})'::box2d `;
139
- return { op: '&&', query };
164
+ return { op: '&&', query, extra };
140
165
  }
141
166
  }
142
167
  return {};
@@ -1,4 +1,5 @@
1
1
  /* eslint-disable no-continue */
2
+
2
3
  /**
3
4
  * @param {Number} opt.json - (1|0) 1 - Результат - Object, 0 - String
4
5
  * @param {String} opt.query - запит до таблиці
@@ -8,18 +9,13 @@
8
9
  import formatValue from './formatValue.js';
9
10
 
10
11
  function getQuery({
11
- filter: filterStr, tableSQL, fields, filterList, config = {}, pg,
12
+ pg, filter: filterStr, table, tableSQL, fields, filterList,
12
13
  }) {
13
14
  if (!filterStr) return null; // filter list API
14
15
 
15
16
  const mainOperators = ['=', '~', '>', '<'];
16
17
 
17
- // v3 filter
18
- const { allTemplates } = config;
19
-
20
- const filterQueryArray = config.v3?.filter
21
- ? decodeURI(filterStr?.replace(/(^,)|(,$)/g, '')).replace(/'/g, '').split(/[;|]/)
22
- : decodeURI(filterStr?.replace(/(^,)|(,$)/g, '')?.replace(/,null/g, '')).replace(/'/g, '').split(/[;|]/);
18
+ const filterQueryArray = decodeURI(filterStr?.replace(/(^,)|(,$)/g, '')).replace(/'/g, '').split(/[;|]/);
23
19
 
24
20
  const resultList = [];
25
21
 
@@ -33,6 +29,9 @@ function getQuery({
33
29
  continue;
34
30
  }
35
31
 
32
+ // filter
33
+ const filter = filterList?.find((el) => [el.id, el.name].includes(name)) || { type: 'text' };
34
+
36
35
  // find all value
37
36
  const value = filterQueryArray.filter((el) => el.startsWith(name)).map((el) => el.substring(name.length + 1)).join(',');
38
37
 
@@ -40,30 +39,24 @@ function getQuery({
40
39
 
41
40
  // find field and skip not exists
42
41
  const { dataTypeID } = fields?.find((el) => el.name === name) || fields?.find((el) => el.name === optimize?.pk) || {};
43
- if (!dataTypeID) continue;
44
42
 
45
- const type = pg.pgType?.[dataTypeID];
46
-
47
- // filter
48
- const filter = filterList?.find((el) => el.id === name) || { type: 'text' };
49
- const filterType = filter.type?.toLowerCase();
50
43
  // format query
51
-
52
- const { op, query } = formatValue({
53
- clsList: allTemplates?.cls || [],
44
+ const {
45
+ op, query, filterType, fieldType,
46
+ } = formatValue({
47
+ pg,
48
+ table,
54
49
  filter,
55
50
  optimize,
56
- filterType,
57
51
  name,
58
- value: decodeURIComponent(value), // decodeURIComponent(value)?.replace(new RegExp(String.raw`\b${name}=\b`, 'g'), '') for checkboxes?
52
+ value: decodeURIComponent(value),
59
53
  operator,
60
- fieldType: type || 'text',
54
+ dataTypeID,
61
55
  }) || {};
62
- // console.log({ query, value });
63
56
  if (!query) continue;
64
57
 
65
58
  resultList.push({
66
- name, value, query, operator: op, filterType, type,
59
+ name, value, query, operator: op, filterType, type: fieldType,
67
60
  });
68
61
  }
69
62
 
@@ -5,34 +5,72 @@ import init from '../../pg/funcs/init.js';
5
5
 
6
6
  import build from '../../helper.js';
7
7
  import config from '../config.js';
8
+ import { getTemplate } from '../../utils.js';
9
+
10
+ const table = 'test.rest_zone.table';
8
11
 
9
12
  test('api table', async (t) => {
10
13
  const app = await build(t);
11
14
  await init(pgClients.client);
12
15
 
16
+ const body = await getTemplate('table', table);
17
+ assert.equal(body?.table, 'itree.rest_zones');
18
+ assert.ok(pgClients.client?.pk?.[body?.table], 'invalid db - skip filter unit test');
19
+
20
+ if (pgClients.client?.pk?.[body?.table]) {
21
+ const custom = body?.filterCustom?.[0] || {};
22
+ assert.ok(custom.name && custom.sql, 'invalid test template - empty filterCustom');
23
+ const state = body?.filterState?.[0] || {};
24
+ assert.ok(state.name && state.sql, 'invalid test template - empty filterState');
25
+
26
+ await t.test('GET /data (filter + custom + state + sql)', async () => {
27
+ const res = await app.inject({
28
+ method: 'GET',
29
+ url: `${config.prefix || '/api'}/data/${table}`,
30
+ query: {
31
+ custom: custom.name,
32
+ state: state.name,
33
+ filter: 'balancer~1;composition=1',
34
+ sql: 1,
35
+ },
36
+ });
37
+ assert.ok(res.body?.includes(custom.sql), 'filterCustom not ok');
38
+ assert.ok(res.body?.includes(state.sql), 'filterState not ok');
39
+ assert.ok(res.body?.includes('balancer::text ilike \'%1%\' and \'{1}\'::text[] && composition::text[]'), 'filter not ok');
40
+ });
41
+ }
42
+
13
43
  const bbox = '20.276526313524393 40.6651677831094,35.27752631352439 50.66616778310941';
14
- const { count = 0 } = await pgClients.client.query(`select count(*) from gis.dataset
15
- where geom && 'box(${bbox})'::box2d`)
16
- .then((res) => res.rows[0] || {});
44
+ const { count = 0 } = pgClients.client?.pk[body?.table] ? await pgClients.client.query(`select count(*) from itree.rest_zones
45
+ where verif and '{1}'::text[] && composition::text[] and geom && 'box(${bbox})'::box2d`).then((res) => res.rows[0] || {}) : {};
17
46
 
18
47
  await t.test('GET /data (meta bbox / cls)', async () => {
19
48
  const res = await app.inject({
20
49
  method: 'GET',
21
- url: `${config.prefix || '/api'}/data/test.dataset.table?bbox=${bbox}`,
50
+ url: `${config.prefix || '/api'}/data/${table}`,
51
+ query: { bbox, filter: 'composition=1' },
22
52
  });
23
53
  const json = res.json();
24
- assert.ok(json?.rows?.length === +count, 'meta bbox - not ok');
25
- assert.ok(json?.rows?.length ? json?.rows?.[0]?.dataset_id_text : true, 'meta cls - not ok');
54
+ const res1 = await app.inject({
55
+ method: 'GET',
56
+ url: `${config.prefix || '/api'}/data/${table}`,
57
+ query: { bbox, sql: 1 },
58
+ });
59
+ const { body: sql } = res1;
60
+ assert.ok(sql?.includes(bbox), 'meta bbox sql - not ok');
61
+ assert.ok(json?.rows?.length && +count >= json?.rows?.length, 'meta bbox - not ok');
62
+ assert.ok(json?.rows?.length ? json?.rows?.[0]?.composition_text : true, 'meta cls - not ok');
26
63
  });
27
64
 
28
65
  const polyline = 'wfvkH_jsvCoKvj@oKfiB?fw@?nd@?nK?nK~WgEfE?wQfw@gEfE?vQwQvj@wQvcAoKvQoKnKgEnd@_cBgw@wj@fEwj@nKg^vQg^oK?vQgEnKgpAvj@?vcAfEf^fEf^?vj@vQvj@vQgE~Wvj@?~p@gE~iAfEnK?~iA_XnvA?nd@fEf^~p@~bBvQ?nd@wQ?vQfEf^fEnK?vQ?vj@oKfEoKfEvQnd@nKvj@?vQfEvQgEf^gEf^?vQfEvQfEvQfE~W?fE?fE?~W?f^?nd@?nK?nKfEfEg^oKwj@nK_X~W~p@~WvQnKoKn}@wQvj@oK~Wg^vcAgEnK?~Wfw@vgD~p@oKfpAvQnd@nKoKvQoKvQ?fE?fEnKvj@wQvQ?fEod@fw@~Wn}@fEn}@_Xv|A_jA~fEwQwQ_XoKwQoKoKoKoKoKg^gE_XgEoKg^gEgEgE?gEfEgEfEgEf^_XoKwQ?gEgEoK?wQ?gE~WoKoKoKwQfEgEnK_X?gEgE?oKfEoKoKgE_XgEgEwQfEoKod@_Xgw@gEoKgE_Xg^wQoKg^_XnvAgE?gEvQgE?od@fpA?vQ?f^oKvcAwj@oKg^?wQn}@gEf^oK?w|AnKgE??nKwj@gEgEg^oKwQoKoK?gEwQoaD?oKod@oKod@gEod@wQo}@oKfEf^gEfEgpAnd@_XfEgE?wQnKo}@_q@?gEgEwj@?oK?oK?_X?oKfEoK?oKfEoK?oK?oK?gEfEoKwQfEoK?gEwQwj@nKo}@nKwQ~Wod@nKoKfEgw@ovAgEwQgE??oKoKgEoKgEgEwQ?oKfEoKnKwQfEwQoK?g^fE?wQfEg^fEod@gE??fEoKvQ?nKoKnKgEfEgEgE?g^nK_q@nKod@?oKoKoK~Wgw@fEoKnd@fE?_XfEoKfEwQfEoKfEwQfEoKvQ?fEfE?vj@gE~W~WvQvQwj@vQvQf^o}@nKvQ?_XgEg^gEg^nKod@?wQgEgEgE?oK_XgEoK?oKgEoKgE_XoKg^oKg^oKod@?gEfEoKvQgEnK?nKfE?_Xnd@gw@fEwQf^wj@fEwQnd@w|A?gEgEgEgEwQfEoKfEgEnKoKnKwQfEgE?oKfEoKnKgEnKwQ_XwQnKwcAod@wQvQod@~Wgw@nKgw@oKg^f^ovAgEwQ?_X?od@gEwj@gEwQvQg^fEoKnKoKvQnK~WfEnKvQ~Wod@f^wj@~WoKnKg^~W?vQgE~WoKnKoK~WoKfEwQvQwQfEgEnKwQnKwQvQwQnKoKnKgEnKgEnKgEvQnKnK?fE?fE?~WwQnKgEnKgEfE?fE?nK?fE?fE?vQgEfE?nKfEfE?fE?vQ?~W?~WgEfEgEnKgEnKwQfEnKnKfE?wj@?o}@g^o}@_Xod@?gEf^_X?o}@?wQ~W?~WgE~WgEfE?f^gEf^oK~Wg^nKgE?oK~WwQ?od@nK_XfEwQnKoKfEgEfE~W?f^f^fEfEfEnKfEnK?gEf^vQfEnK?~WnKvQnK?vQf^nKfEfEgE~WnKfEnK?fEgEfE?nK?fEfEfEgEnK?nKgEnK?nKfEf^?vQgEvQ?nd@gEnKo}@vQgE~WgEf^gE'; // UA26040270000047749
29
66
 
30
- const { count1 } = await pgClients.client.query('select count(*) as count1 from gis.dataset where ST_Contains(ST_MakePolygon(ST_LineFromEncodedPolyline($1)),geom)', [polyline])
67
+ const { count1 } = await pgClients.client.query('select count(*) as count1 from itree.rest_zones where verif and ST_Contains(ST_MakePolygon(ST_LineFromEncodedPolyline($1)),geom)', [polyline])
31
68
  .then((res) => res.rows[0] || {});
32
69
  await t.test('GET /data (meta polyline)', async () => {
33
70
  const res = await app.inject({
34
71
  method: 'GET',
35
- url: `${config.prefix || '/api'}/data/test.dataset.table?polyline=${polyline}`,
72
+ url: `${config.prefix || '/api'}/data/${table}`,
73
+ query: { polyline },
36
74
  });
37
75
  const json = res.json();
38
76
  assert.ok(json?.rows?.length === +count1, 'meta bbox (polyline) - not ok');
package/utils.js CHANGED
@@ -33,6 +33,7 @@ import execMigrations from './migration/exec.migrations.js';
33
33
  import addNotification from './notification/funcs/addNotification.js';
34
34
  import sendNotification from './notification/funcs/sendNotification.js';
35
35
  import logger from './logger/getLogger.js';
36
+ // const logger = false; // test!
36
37
 
37
38
  export default null;
38
39
  export {