@opengis/gis 0.2.177 → 0.2.178
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 +1 -1
- package/server/routes/registers/functions/getFilterSQLParametrized/formatValue.js +312 -0
- package/server/routes/registers/functions/getFilterSQLParametrized/getFilterQuery.js +126 -0
- package/server/routes/registers/functions/getFilterSQLParametrized/getRangeQuery.js +353 -0
- package/server/routes/registers/functions/getFilterSQLParametrized/index.js +81 -0
- package/server/routes/registers/functions/getTableColumnMeta.js +2 -8
- package/server/routes/registers/functions/gis.suggest.js +4 -13
- package/server/routes/registers/functions/handleRegistryRequest.js +13 -5
- package/server/routes/registers/registers.route.test.js +32 -3
- package/server/routes/registers/registers.schema.js +7 -2
package/package.json
CHANGED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { pgClients } from "@opengis/fastify-table/utils.js";
|
|
2
|
+
|
|
3
|
+
import getRangeQuery from "./getRangeQuery.js";
|
|
4
|
+
|
|
5
|
+
const selectFilterTypes = [
|
|
6
|
+
"check",
|
|
7
|
+
"autocomplete",
|
|
8
|
+
"tags",
|
|
9
|
+
"avatar",
|
|
10
|
+
"radio",
|
|
11
|
+
"select",
|
|
12
|
+
"button",
|
|
13
|
+
];
|
|
14
|
+
const matchNullObj = {
|
|
15
|
+
null: "is null",
|
|
16
|
+
notnull: "is not null",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default function formatValue(
|
|
20
|
+
{
|
|
21
|
+
table,
|
|
22
|
+
filter = {},
|
|
23
|
+
name,
|
|
24
|
+
value,
|
|
25
|
+
dataTypeID,
|
|
26
|
+
uid = "2",
|
|
27
|
+
args: argsParams,
|
|
28
|
+
},
|
|
29
|
+
pg = pgClients.client
|
|
30
|
+
) {
|
|
31
|
+
const { sql, select, strict, options, optionsFromColumns, api } = filter;
|
|
32
|
+
|
|
33
|
+
const filterType = filter.type?.toLowerCase?.() || "text";
|
|
34
|
+
|
|
35
|
+
const args = Array.from(argsParams || []);
|
|
36
|
+
|
|
37
|
+
const sval =
|
|
38
|
+
typeof value === "string"
|
|
39
|
+
? `%${decodeURIComponent(value.replace(/%/g, "%25")).replace(
|
|
40
|
+
/%/g,
|
|
41
|
+
"\\%"
|
|
42
|
+
)}%`
|
|
43
|
+
: undefined;
|
|
44
|
+
|
|
45
|
+
if (filterType === "text" && typeof value === "string" && filter?.columns) {
|
|
46
|
+
args.push(sval);
|
|
47
|
+
const columns = filter.columns.split(",");
|
|
48
|
+
|
|
49
|
+
const query = `(${columns
|
|
50
|
+
.map((el) => `"${el}"::text ilike $${args.length}`)
|
|
51
|
+
.join(" or ")})`;
|
|
52
|
+
return { op: "~", query, values: [sval] };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const fieldType =
|
|
56
|
+
pg?.pgType?.[dataTypeID] ||
|
|
57
|
+
pg?.pgType?.[{ date: 1114 }[filterType] || 25];
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
!name ||
|
|
61
|
+
!value ||
|
|
62
|
+
(!dataTypeID && !sql && !options) ||
|
|
63
|
+
(!selectFilterTypes.includes(filterType) &&
|
|
64
|
+
options?.find?.((el) => el?.sql))
|
|
65
|
+
) {
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// my rows
|
|
70
|
+
if (value === "me" && uid) {
|
|
71
|
+
args.push(uid);
|
|
72
|
+
return { op: "=", query: `${name}::text = $${args.length}`, values: [uid] };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const matchNull = matchNullObj[value];
|
|
76
|
+
|
|
77
|
+
const includesNull =
|
|
78
|
+
!matchNull &&
|
|
79
|
+
value &&
|
|
80
|
+
value.includes(",") &&
|
|
81
|
+
(value.startsWith("null") ||
|
|
82
|
+
value.includes(",null") ||
|
|
83
|
+
value.startsWith("notnull") ||
|
|
84
|
+
value.includes(",notnull"));
|
|
85
|
+
|
|
86
|
+
const includesNull1 = includesNull
|
|
87
|
+
? value
|
|
88
|
+
.split(",")
|
|
89
|
+
.map((el) => matchNullObj[el])
|
|
90
|
+
.filter(Boolean)
|
|
91
|
+
: undefined;
|
|
92
|
+
|
|
93
|
+
// skip if requested both: is null and not null
|
|
94
|
+
const includesNullQuery =
|
|
95
|
+
includesNull1 && includesNull1.length > 1 ? undefined : includesNull1?.[0];
|
|
96
|
+
|
|
97
|
+
// geometry
|
|
98
|
+
if (["geometry"].includes(fieldType || "")) {
|
|
99
|
+
if (matchNull) {
|
|
100
|
+
return { op: "nullable", query: `${name} ${matchNull}` };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const bbox = value?.[0]?.split?.("_");
|
|
104
|
+
|
|
105
|
+
if (bbox?.length !== 4) {
|
|
106
|
+
return {};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const query = ` ${name} && 'box(${bbox[0]} ${bbox[1]},${bbox[2]} ${bbox[3]})'::box2d `;
|
|
110
|
+
return { op: "&&", query };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const op = filterType === "text" && !strict ? "~" : "=";
|
|
114
|
+
// text
|
|
115
|
+
if (["text"].includes(filterType)) {
|
|
116
|
+
// with sql subquery
|
|
117
|
+
if (typeof sql === "string") {
|
|
118
|
+
args.push(value);
|
|
119
|
+
const querysql = sql
|
|
120
|
+
.replace(/= ?any\(\$1\)/g, `::text=any($${args.length}::text[])`)
|
|
121
|
+
.replace(/\$1/g, `$${args.length}`);
|
|
122
|
+
return { op: "=", query: querysql, values: [[value]] };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// default
|
|
126
|
+
if (!select) {
|
|
127
|
+
const value1 = op === "~" ? `%${value}%` : value;
|
|
128
|
+
args.push(value1);
|
|
129
|
+
const query = `${name}::text ${op === "~" ? "ilike" : "="} $${args.length
|
|
130
|
+
}`;
|
|
131
|
+
return {
|
|
132
|
+
op,
|
|
133
|
+
query,
|
|
134
|
+
values: [value1],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// nullable
|
|
140
|
+
if (matchNull) {
|
|
141
|
+
const query = `${name} ${matchNull}`;
|
|
142
|
+
return { op, query };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// boolean
|
|
146
|
+
if (fieldType === "boolean" && ["true", "false"].includes(value)) {
|
|
147
|
+
const query = `${name} IS ${value === "true" ? "TRUE" : "FALSE"}`;
|
|
148
|
+
return { op, query };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// boolean + nullable
|
|
152
|
+
const value1 = ["true", "false"].find((el) => value.includes(el));
|
|
153
|
+
const values = ["true", "false"].filter((el) => value.includes(el));
|
|
154
|
+
|
|
155
|
+
// boolean: true, false and null => skip
|
|
156
|
+
if (fieldType === "boolean" && values.length === 2 && !!includesNullQuery) {
|
|
157
|
+
return { op, query: "3=3" };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// boolean: both true and false => not null
|
|
161
|
+
if (fieldType === "boolean" && values.length === 2) {
|
|
162
|
+
return { op, query: `${name} is not null` };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// boolean: not null over any value
|
|
166
|
+
if (
|
|
167
|
+
fieldType === "boolean" &&
|
|
168
|
+
value1 &&
|
|
169
|
+
includesNullQuery === "is not null"
|
|
170
|
+
) {
|
|
171
|
+
const query = `${name} ${includesNullQuery}`;
|
|
172
|
+
return { op, query };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// boolean: true or null
|
|
176
|
+
if (
|
|
177
|
+
fieldType === "boolean" &&
|
|
178
|
+
value1 &&
|
|
179
|
+
includesNullQuery === "is null" &&
|
|
180
|
+
value1 === "true"
|
|
181
|
+
) {
|
|
182
|
+
return { op, query: `${name} IS NOT FALSE` };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// boolean: false or null
|
|
186
|
+
if (
|
|
187
|
+
fieldType === "boolean" &&
|
|
188
|
+
value1 &&
|
|
189
|
+
includesNullQuery === "is null" &&
|
|
190
|
+
value1 === "false"
|
|
191
|
+
) {
|
|
192
|
+
return { op, query: `${name} IS NOT TRUE` };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// select / cls - any filter type w/ out sql subquery
|
|
196
|
+
if (
|
|
197
|
+
value &&
|
|
198
|
+
select &&
|
|
199
|
+
Array.isArray(options) &&
|
|
200
|
+
!options.find((el) => el?.sql)
|
|
201
|
+
) {
|
|
202
|
+
const value1 = value.split(",");
|
|
203
|
+
// by id (checkbox)
|
|
204
|
+
if (options.find((el) => el?.id && value1.includes(el.id))) {
|
|
205
|
+
args.push(value1);
|
|
206
|
+
if (fieldType && fieldType.endsWith("[]")) {
|
|
207
|
+
const query = `EXISTS ( SELECT 1 FROM unnest(${name}) AS elem WHERE elem in (with q(id, name) as (${select}) select id from q where id = any($${args.length}) ) )`;
|
|
208
|
+
return {
|
|
209
|
+
op,
|
|
210
|
+
query,
|
|
211
|
+
values: [value1],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const query = `${name}::text in ( ( with q(id,name) as (${select}) select id from q where id = any($${args.length}) ) )`;
|
|
216
|
+
return {
|
|
217
|
+
op,
|
|
218
|
+
query,
|
|
219
|
+
values: [value1],
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
// by text
|
|
223
|
+
args.push(`%${value1[0]}%`);
|
|
224
|
+
return {
|
|
225
|
+
op,
|
|
226
|
+
query: `${name}::text in ( ( with q(id,name) as (${select}) select id from q where name::text ilike $${args.length} ) )`,
|
|
227
|
+
values: [`%${value1[0]}%`],
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// date: 01.01.2024-31.12.2024 / number range: 100-500
|
|
232
|
+
if (["date", "datepicker", "range"].includes(filterType)) {
|
|
233
|
+
const { query, values } = getRangeQuery({
|
|
234
|
+
value,
|
|
235
|
+
name,
|
|
236
|
+
fieldType,
|
|
237
|
+
filterType,
|
|
238
|
+
sql,
|
|
239
|
+
args,
|
|
240
|
+
});
|
|
241
|
+
return { op: "between", query, values };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/* select query - from admin.cls / filter options */
|
|
245
|
+
if (selectFilterTypes.includes(filterType)) {
|
|
246
|
+
// multiple checks with predefined query
|
|
247
|
+
if (options?.find?.((el) => el?.sql)) {
|
|
248
|
+
const query =
|
|
249
|
+
options
|
|
250
|
+
.filter((el) => value.split(",").includes(el.id?.toString?.()))
|
|
251
|
+
.map((el) => el.sql || "false")
|
|
252
|
+
.join(" and ") || "false";
|
|
253
|
+
return { op: "=", query };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// with sql subquery
|
|
257
|
+
if (sql) {
|
|
258
|
+
args.push(value);
|
|
259
|
+
const querysql = sql
|
|
260
|
+
.replace(/= ?any\(\$1\)/g, `::text=any($${args.length}::text[])`)
|
|
261
|
+
.replace(/\$1/g, `$${args.length}`);
|
|
262
|
+
return { op: "=", query: querysql, values: [[value]] };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// if pseudo-suggest column contain commas
|
|
266
|
+
if (optionsFromColumns && options?.length) {
|
|
267
|
+
const optionValues = options.map((e) => e.id);
|
|
268
|
+
const vals = optionValues.filter((e) => value.includes(e));
|
|
269
|
+
args.push(vals);
|
|
270
|
+
return {
|
|
271
|
+
op: "=",
|
|
272
|
+
query: `${name}::text=any($${args.length}::text[])`,
|
|
273
|
+
values: vals,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// default
|
|
278
|
+
args.push(value);
|
|
279
|
+
|
|
280
|
+
if (fieldType?.includes("[]")) {
|
|
281
|
+
const query = `${name}::text[] && $${args.length}::text[]`;
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
op: "=",
|
|
285
|
+
query,
|
|
286
|
+
values: [value.split(",")],
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (api && api.startsWith('/api/gis-suggest/')) {
|
|
291
|
+
const query = fieldType?.includes("[]") ? `EXISTS (
|
|
292
|
+
SELECT 1
|
|
293
|
+
FROM unnest(${name}) AS value
|
|
294
|
+
WHERE rtrim(
|
|
295
|
+
replace(replace(replace(encode(value::bytea, 'base64'), '+', ''), '-', ''), '/', ''),
|
|
296
|
+
'='
|
|
297
|
+
) = ANY($${args.length})
|
|
298
|
+
)`
|
|
299
|
+
: `rtrim( replace( replace( replace( encode(${name}::bytea, 'base64'), '+', '' ), '-', '' ), '/', '' ), '=' )=any($${args.length})`;
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
op: "=",
|
|
303
|
+
query,
|
|
304
|
+
values: [value.split(",")],
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const query = `${name}::text=any($${args.length})`;
|
|
309
|
+
return { op: "=", query, values: [value.split(",")] };
|
|
310
|
+
}
|
|
311
|
+
return {};
|
|
312
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/* eslint-disable no-continue */
|
|
2
|
+
|
|
3
|
+
import { pgClients } from "@opengis/fastify-table/utils.js";
|
|
4
|
+
|
|
5
|
+
import formatValue from "./formatValue.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {Number} opt.json - (1|0) 1 - Результат - Object, 0 - String
|
|
9
|
+
* @param {String} opt.query - запит до таблиці
|
|
10
|
+
* @param {String} opt.hash - інформація з хешу по запиту
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
function getFilterQuery(
|
|
14
|
+
{
|
|
15
|
+
filter: filterStr = "",
|
|
16
|
+
table = "",
|
|
17
|
+
tableSQL = [],
|
|
18
|
+
fields,
|
|
19
|
+
filterList,
|
|
20
|
+
uid,
|
|
21
|
+
objectId,
|
|
22
|
+
args,
|
|
23
|
+
},
|
|
24
|
+
pg = pgClients.client
|
|
25
|
+
) {
|
|
26
|
+
// if (!filterStr) return null; // filter list API
|
|
27
|
+
const mainOperators = ["=", "~", ">", "<"];
|
|
28
|
+
const filterQueryArray =
|
|
29
|
+
decodeURIComponent(
|
|
30
|
+
filterStr
|
|
31
|
+
?.replace?.(/%/g, "%25")
|
|
32
|
+
?.replace?.(/%/g, "\\%")
|
|
33
|
+
?.replace?.(/(^,)|(,$)/g, "")
|
|
34
|
+
)
|
|
35
|
+
?.replace?.(/'/g, "''")
|
|
36
|
+
?.split?.(/[;|]/) || [];
|
|
37
|
+
// default filter value form checkboxes, autocomplete or sql-defined only - if not called from card
|
|
38
|
+
const filterDefaultQueryArray = !objectId
|
|
39
|
+
? filterList
|
|
40
|
+
?.filter?.(
|
|
41
|
+
(el) =>
|
|
42
|
+
el.name &&
|
|
43
|
+
!filterStr?.includes?.(`${el.name}=`) &&
|
|
44
|
+
el.default &&
|
|
45
|
+
(el.options || el.sql)
|
|
46
|
+
)
|
|
47
|
+
?.map?.((el) => `${el.name}=${el.default}`) || []
|
|
48
|
+
: [];
|
|
49
|
+
|
|
50
|
+
// concat default + request filters
|
|
51
|
+
const arr = filterQueryArray.concat(filterDefaultQueryArray).filter(Boolean);
|
|
52
|
+
|
|
53
|
+
const resultList = [];
|
|
54
|
+
|
|
55
|
+
for (let i = 0; i < arr.length; i += 1) {
|
|
56
|
+
const item = arr[i];
|
|
57
|
+
if (!item) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const splitby = mainOperators?.find((el) => item.indexOf(el) !== -1) || "=";
|
|
61
|
+
const [name] = item.split(splitby);
|
|
62
|
+
|
|
63
|
+
// skip already added filter
|
|
64
|
+
if (resultList.find((el) => el.name === name)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// filter
|
|
69
|
+
const filter = filterList?.find?.((el) => el.type && el.name === name) || {
|
|
70
|
+
type: "text",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// find all value
|
|
74
|
+
const value = arr
|
|
75
|
+
.filter((el) => el.split(splitby)?.[0] === name)
|
|
76
|
+
.map((el) => el.substring((name?.length || 0) + 1))
|
|
77
|
+
.join(",");
|
|
78
|
+
|
|
79
|
+
// find field and skip not exists
|
|
80
|
+
const { dataTypeID } =
|
|
81
|
+
fields?.find((el) => el.name === name) ||
|
|
82
|
+
{};
|
|
83
|
+
|
|
84
|
+
// format query
|
|
85
|
+
const {
|
|
86
|
+
op,
|
|
87
|
+
query,
|
|
88
|
+
values: filterValues,
|
|
89
|
+
} = formatValue(
|
|
90
|
+
{
|
|
91
|
+
table,
|
|
92
|
+
filter,
|
|
93
|
+
name,
|
|
94
|
+
value,
|
|
95
|
+
dataTypeID,
|
|
96
|
+
uid,
|
|
97
|
+
args,
|
|
98
|
+
},
|
|
99
|
+
pg
|
|
100
|
+
) || {};
|
|
101
|
+
|
|
102
|
+
if (!query) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (
|
|
107
|
+
args &&
|
|
108
|
+
filterValues &&
|
|
109
|
+
Array.isArray(filterValues) &&
|
|
110
|
+
filterValues.length
|
|
111
|
+
) {
|
|
112
|
+
filterValues.forEach((val) => args.push(val));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
resultList.push({
|
|
116
|
+
name,
|
|
117
|
+
value,
|
|
118
|
+
values: filterValues,
|
|
119
|
+
query,
|
|
120
|
+
operator: op,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return resultList;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export default getFilterQuery;
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/* eslint-disable no-restricted-globals */
|
|
2
|
+
const dateTypeList = [
|
|
3
|
+
"date",
|
|
4
|
+
"timestamp",
|
|
5
|
+
"timestamp without time zone",
|
|
6
|
+
"timestamp with time zone",
|
|
7
|
+
];
|
|
8
|
+
const numberTypeList = [
|
|
9
|
+
"float8",
|
|
10
|
+
"int4",
|
|
11
|
+
"int8",
|
|
12
|
+
"numeric",
|
|
13
|
+
"double precision",
|
|
14
|
+
"integer",
|
|
15
|
+
"bigint",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const isValidDate = (dateStr) => {
|
|
19
|
+
if (!dateStr) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (["min", "max", "cd", "cw", "cm", "cq", "cy"].includes(dateStr)) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// iso date: 2024-01-01
|
|
28
|
+
if (dateStr.includes("-")) {
|
|
29
|
+
return new Date(dateStr).toString() !== "Invalid Date";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// locale date: 01.01.2024
|
|
33
|
+
const [dd, mm, yyyy] = dateStr.split(".");
|
|
34
|
+
return new Date(`${yyyy}-${mm}-${dd}`).toString() !== "Invalid Date";
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const isValidNumber = (numStr) =>
|
|
38
|
+
["min", "max"].includes(numStr) ? true : !isNaN(numStr);
|
|
39
|
+
|
|
40
|
+
function dt(y, m, d) {
|
|
41
|
+
return new Date(Date.UTC(y, m, d)).toISOString().slice(0, 10);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const dp = {
|
|
45
|
+
d: new Date().getDate(),
|
|
46
|
+
w: new Date().getDate() - (new Date().getDay() || 7) + 1,
|
|
47
|
+
m: new Date().getMonth(),
|
|
48
|
+
q: parseFloat((new Date().getMonth() / 4).toFixed()) * 3,
|
|
49
|
+
y: new Date().getFullYear(),
|
|
50
|
+
};
|
|
51
|
+
function formatDateISOString(date) {
|
|
52
|
+
if (!date?.includes(".")) return date;
|
|
53
|
+
const [day, month, year] = date.split(".");
|
|
54
|
+
return `${year}-${month}-${day}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function checkValid(
|
|
58
|
+
value,
|
|
59
|
+
fieldType,
|
|
60
|
+
filterType,
|
|
61
|
+
sql,
|
|
62
|
+
) {
|
|
63
|
+
const sep =
|
|
64
|
+
(value.includes(",") ? "," : null) ||
|
|
65
|
+
(value.includes("_") ? "_" : null) ||
|
|
66
|
+
(value.includes("-") ? "-" : null);
|
|
67
|
+
|
|
68
|
+
// number range w/o valid separator => skip invalid filter
|
|
69
|
+
if (filterType === "range" && !sep) {
|
|
70
|
+
return { isvalid: false };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// date range, specific options: current day, week, month, year etc.
|
|
74
|
+
if (["date", "datepicker"].includes(filterType) && value === "cd") {
|
|
75
|
+
return {
|
|
76
|
+
min: dt(dp.y, dp.m, dp.d),
|
|
77
|
+
max: dt(dp.y, dp.m, dp.d),
|
|
78
|
+
isvalid: true,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (["date", "datepicker"].includes(filterType) && value === "cw") {
|
|
83
|
+
return {
|
|
84
|
+
min: dt(dp.y, dp.m, dp.w),
|
|
85
|
+
max: dt(dp.y, dp.m, dp.w + 6),
|
|
86
|
+
isvalid: true,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (["date", "datepicker"].includes(filterType) && value === "cm") {
|
|
91
|
+
return {
|
|
92
|
+
min: dt(dp.y, dp.m, 1),
|
|
93
|
+
max: dt(dp.y, dp.m + 1, 0),
|
|
94
|
+
isvalid: true,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (["date", "datepicker"].includes(filterType) && value === "cq") {
|
|
99
|
+
return {
|
|
100
|
+
min: dt(dp.y, dp.q, 1),
|
|
101
|
+
max: dt(dp.y, dp.q + 3, 0),
|
|
102
|
+
isvalid: true,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (["date", "datepicker"].includes(filterType) && value === "cy") {
|
|
107
|
+
return {
|
|
108
|
+
min: dt(dp.y, 0, 1),
|
|
109
|
+
max: dt(dp.y, 11, 31),
|
|
110
|
+
isvalid: true,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// specific period from now - days, months, years
|
|
115
|
+
if (
|
|
116
|
+
["date", "datepicker"].includes(filterType) &&
|
|
117
|
+
value.match(/^(?<val>[1-9]\d*)(?<unit>d|m|y)$/)?.groups?.unit
|
|
118
|
+
) {
|
|
119
|
+
const { val, unit } =
|
|
120
|
+
value.match(/^(?<val>[1-9]\d*)(?<unit>d|m|y)$/)?.groups || {};
|
|
121
|
+
const min =
|
|
122
|
+
(unit === "d" ? dt(dp.y, dp.m, dp.d - +val) : null) ||
|
|
123
|
+
(unit === "m" ? dt(dp.y, dp.m - +val, dp.d) : null) ||
|
|
124
|
+
(unit === "y" ? dt(dp.y - +val, dp.m, dp.d) : null);
|
|
125
|
+
return {
|
|
126
|
+
min,
|
|
127
|
+
max: dt(dp.y, dp.m, dp.d),
|
|
128
|
+
isvalid: isValidDate(min),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// specific quarter - before skip date value validation
|
|
133
|
+
if (
|
|
134
|
+
["date", "datepicker"].includes(filterType) &&
|
|
135
|
+
value.match(/^(?<year>\d{4})-q(?<quarter>[1-4])$/)?.groups?.quarter
|
|
136
|
+
) {
|
|
137
|
+
const { year, quarter } =
|
|
138
|
+
value.match(/^(?<year>\d{4})-q(?<quarter>[1-4])$/)?.groups || {};
|
|
139
|
+
const startMonth = +quarter * 3;
|
|
140
|
+
return {
|
|
141
|
+
min: dt(year, startMonth - 3, 1),
|
|
142
|
+
max: dt(year, startMonth, 0),
|
|
143
|
+
isvalid: true,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// date value validation
|
|
148
|
+
if (
|
|
149
|
+
["date", "datepicker"].includes(filterType) &&
|
|
150
|
+
!sep &&
|
|
151
|
+
!isValidDate(value)
|
|
152
|
+
) {
|
|
153
|
+
return { isvalid: false };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// specific date / day
|
|
157
|
+
if (
|
|
158
|
+
["date", "datepicker"].includes(filterType) &&
|
|
159
|
+
value.match(/^\d{4}-\d{2}-\d{2}$/)?.[0]
|
|
160
|
+
) {
|
|
161
|
+
return {
|
|
162
|
+
min: new Date(value).toISOString().split("T")[0],
|
|
163
|
+
max: new Date(value).toISOString().split("T")[0],
|
|
164
|
+
isvalid: true,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// specific month
|
|
169
|
+
if (
|
|
170
|
+
["date", "datepicker"].includes(filterType) &&
|
|
171
|
+
value.match(/^\d{4}-\d{2}$/)?.[0]
|
|
172
|
+
) {
|
|
173
|
+
return {
|
|
174
|
+
min: new Date(value).toISOString().split("T")[0],
|
|
175
|
+
max: dt(new Date(value).getFullYear(), new Date(value).getMonth() + 1, 0),
|
|
176
|
+
isvalid: true,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// specific year
|
|
181
|
+
if (
|
|
182
|
+
["date", "datepicker"].includes(filterType) &&
|
|
183
|
+
value.match(/^\d{4}$/)?.[0]
|
|
184
|
+
) {
|
|
185
|
+
return {
|
|
186
|
+
min: dt(value, 0, 1),
|
|
187
|
+
max: dt(value, 11, 31),
|
|
188
|
+
isvalid: true,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (sep) {
|
|
193
|
+
const [minValue = "min", maxValue = "max"] = value.split(sep);
|
|
194
|
+
// range for numbers
|
|
195
|
+
if (filterType === "range" && fieldType) {
|
|
196
|
+
const isvalid =
|
|
197
|
+
(numberTypeList.includes(fieldType) || sql) &&
|
|
198
|
+
isValidNumber(minValue) &&
|
|
199
|
+
isValidNumber(maxValue);
|
|
200
|
+
return { min: minValue, max: maxValue, isvalid };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// date range, example: 01.01.2024-31.12.2024
|
|
204
|
+
if (["date", "datepicker"].includes(filterType)) {
|
|
205
|
+
const min = minValue === "min" ? minValue : formatDateISOString(minValue);
|
|
206
|
+
const max = maxValue === "max" ? maxValue : formatDateISOString(maxValue);
|
|
207
|
+
const isvalid =
|
|
208
|
+
fieldType &&
|
|
209
|
+
(dateTypeList.includes(fieldType) || sql) &&
|
|
210
|
+
isValidDate(min) &&
|
|
211
|
+
isValidDate(max);
|
|
212
|
+
return { min, max, isvalid };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return { isvalid: false };
|
|
217
|
+
}
|
|
218
|
+
export default function getRangeQuery({
|
|
219
|
+
value,
|
|
220
|
+
name,
|
|
221
|
+
fieldType,
|
|
222
|
+
filterType,
|
|
223
|
+
sql,
|
|
224
|
+
args: argsParams,
|
|
225
|
+
}) {
|
|
226
|
+
const { min, max, isvalid } = checkValid(
|
|
227
|
+
value,
|
|
228
|
+
fieldType,
|
|
229
|
+
filterType,
|
|
230
|
+
sql,
|
|
231
|
+
);
|
|
232
|
+
if (!isvalid) {
|
|
233
|
+
return { query: "false" };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const args = Array.from(argsParams || []);
|
|
237
|
+
|
|
238
|
+
const checkBoth =
|
|
239
|
+
sql && typeof sql === "string" && sql.includes("$1") && sql.includes("$2");
|
|
240
|
+
|
|
241
|
+
// with sql subquery
|
|
242
|
+
if (
|
|
243
|
+
filterType &&
|
|
244
|
+
["date", "datepicker"].includes(filterType) &&
|
|
245
|
+
typeof sql === "string"
|
|
246
|
+
) {
|
|
247
|
+
if (checkBoth) {
|
|
248
|
+
args.push(min);
|
|
249
|
+
args.push(max);
|
|
250
|
+
} else {
|
|
251
|
+
args.push(min || max);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
query: sql,
|
|
256
|
+
values: checkBoth ? [min, max] : [min || max],
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
// with sql subquery
|
|
260
|
+
if (filterType && ["range"].includes(filterType) && sql) {
|
|
261
|
+
if (checkBoth) {
|
|
262
|
+
args.push(min);
|
|
263
|
+
args.push(max);
|
|
264
|
+
} else {
|
|
265
|
+
args.push(min || max);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
query: sql,
|
|
270
|
+
values: checkBoth ? [min, max] : [min || max],
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// preset date values
|
|
275
|
+
if (
|
|
276
|
+
filterType &&
|
|
277
|
+
["date", "datepicker"].includes(filterType) &&
|
|
278
|
+
["cd", "cw", "cm", "cq", "cy"].includes(value)
|
|
279
|
+
) {
|
|
280
|
+
if (value === "cd") {
|
|
281
|
+
args.push(min);
|
|
282
|
+
} else {
|
|
283
|
+
args.push(min);
|
|
284
|
+
args.push(max);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const query =
|
|
288
|
+
value === "cd"
|
|
289
|
+
? `${name}::date = $${args.length}::date`
|
|
290
|
+
: `${name}::date between $${args.length - 1}::date and $${args.length
|
|
291
|
+
}::date`;
|
|
292
|
+
return { query, values: value === "cd" ? [min] : [min, max] };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// same date
|
|
296
|
+
if (
|
|
297
|
+
filterType &&
|
|
298
|
+
["date", "datepicker"].includes(filterType) &&
|
|
299
|
+
max &&
|
|
300
|
+
min &&
|
|
301
|
+
min === max
|
|
302
|
+
) {
|
|
303
|
+
args.push(min);
|
|
304
|
+
return { query: `${name}::date = $${args.length}::date`, values: [min] };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// skip, same as 1=1
|
|
308
|
+
if (
|
|
309
|
+
filterType &&
|
|
310
|
+
["range"].includes(filterType) &&
|
|
311
|
+
min === "min" &&
|
|
312
|
+
max === "max"
|
|
313
|
+
) {
|
|
314
|
+
return { query: "true" };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// default
|
|
318
|
+
if (filterType && max === "max") {
|
|
319
|
+
args.push(min);
|
|
320
|
+
const query = ["date", "datepicker"].includes(filterType)
|
|
321
|
+
? `${name} >= $${args.length}::date`
|
|
322
|
+
: `${name} >= $${args.length}`;
|
|
323
|
+
return {
|
|
324
|
+
query,
|
|
325
|
+
values: [min],
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (filterType && min === "min") {
|
|
330
|
+
args.push(max);
|
|
331
|
+
const query = ["date", "datepicker"].includes(filterType)
|
|
332
|
+
? `${name} <= $${args.length}::date`
|
|
333
|
+
: `${name} <= $${args.length}`;
|
|
334
|
+
return { query, values: [max] };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (filterType && ["date", "datepicker"].includes(filterType)) {
|
|
338
|
+
args.push(min);
|
|
339
|
+
args.push(max);
|
|
340
|
+
return {
|
|
341
|
+
query: `${name}::date between $${args.length - 1}::date and $${args.length
|
|
342
|
+
}::date`,
|
|
343
|
+
values: [min, max],
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
args.push(min);
|
|
348
|
+
args.push(max);
|
|
349
|
+
return {
|
|
350
|
+
query: `${name} between $${args.length - 1} and $${args.length}`,
|
|
351
|
+
values: [min, max],
|
|
352
|
+
};
|
|
353
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { BadRequestError } from "@opengis/fastify-table/errors.js";
|
|
2
|
+
import { pgClients } from "@opengis/fastify-table/utils.js";
|
|
3
|
+
|
|
4
|
+
// filter util
|
|
5
|
+
import getFilterQuery from "./getFilterQuery.js";
|
|
6
|
+
|
|
7
|
+
export default function getFilterSQLParametrized({
|
|
8
|
+
table,
|
|
9
|
+
fields = [],
|
|
10
|
+
filter,
|
|
11
|
+
search,
|
|
12
|
+
searchColumn: searchColumn1,
|
|
13
|
+
filterList,
|
|
14
|
+
query,
|
|
15
|
+
uid,
|
|
16
|
+
}, pg = pgClients.client) {
|
|
17
|
+
if (!pg) {
|
|
18
|
+
throw new Error("empty pg");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!table) {
|
|
22
|
+
throw BadRequestError("param table is required");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!pg.pk[table]) {
|
|
26
|
+
throw BadRequestError("param table is invalid");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const args = Array.from([]);
|
|
30
|
+
|
|
31
|
+
const autoSearchColumn = fields
|
|
32
|
+
?.filter((el) => pg.pgType?.[el.dataTypeID] === "text")
|
|
33
|
+
?.map((el) => `"${el.name}"`)
|
|
34
|
+
.join(",");
|
|
35
|
+
|
|
36
|
+
const searchColumn =
|
|
37
|
+
searchColumn1 ||
|
|
38
|
+
autoSearchColumn;
|
|
39
|
+
|
|
40
|
+
const where = [];
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if (search && searchColumn && typeof search === "string") {
|
|
44
|
+
args.push(
|
|
45
|
+
`%${decodeURIComponent(search.replace(/%/g, "%25")).replace(
|
|
46
|
+
/%/g,
|
|
47
|
+
"\\%"
|
|
48
|
+
)}%`
|
|
49
|
+
);
|
|
50
|
+
where.push(
|
|
51
|
+
`(${searchColumn
|
|
52
|
+
.split(",")
|
|
53
|
+
?.map((name) => `${name}::text ilike $${args.length}`)
|
|
54
|
+
?.join(" or ")})`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const filters = getFilterQuery(
|
|
59
|
+
{
|
|
60
|
+
filter,
|
|
61
|
+
table,
|
|
62
|
+
fields,
|
|
63
|
+
filterList,
|
|
64
|
+
uid,
|
|
65
|
+
args,
|
|
66
|
+
},
|
|
67
|
+
pg
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const filterQuery = filters
|
|
71
|
+
.filter((el) => el.query)
|
|
72
|
+
.map((el) => `${el.query}`)
|
|
73
|
+
.join(" and ");
|
|
74
|
+
|
|
75
|
+
const q = where
|
|
76
|
+
.concat([query, filterQuery])
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
.join(" and ");
|
|
79
|
+
|
|
80
|
+
return { q, args };
|
|
81
|
+
}
|
|
@@ -101,20 +101,14 @@ export default async function getTableColumnMeta(
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
const original = filtered
|
|
104
|
-
? `with c(id,text) as (select "${queryColumn.replace(
|
|
105
|
-
/"/g,
|
|
106
|
-
""
|
|
107
|
-
)}" as id, "${queryColumn.replace(
|
|
104
|
+
? `with c(id,text) as (select rtrim( replace( replace( replace( encode("${queryColumn.replace(/"/g, '')}"::bytea, 'base64'), '+', '' ), '-', '' ), '/', '' ), '=' ) as id, "${queryColumn.replace(
|
|
108
105
|
/"/g,
|
|
109
106
|
""
|
|
110
107
|
)}" as text from ${tableName} t ${sqlTable} group by "${queryColumn.replace(
|
|
111
108
|
/"/g,
|
|
112
109
|
""
|
|
113
110
|
)}") select id, text from c`
|
|
114
|
-
: `with c(id,text) as (select "${queryColumn.replace(
|
|
115
|
-
/"/g,
|
|
116
|
-
""
|
|
117
|
-
)}" as id, "${queryColumn.replace(
|
|
111
|
+
: `with c(id,text) as (select rtrim( replace( replace( replace( encode("${queryColumn.replace(/"/g, '')}"::bytea, 'base64'), '+', '' ), '-', '' ), '/', '' ), '=' ) as id, "${queryColumn.replace(
|
|
118
112
|
/"/g,
|
|
119
113
|
""
|
|
120
114
|
)}" as text, count(*) from ${tableName} t ${sqlTable} group by "${queryColumn.replace(
|
|
@@ -163,10 +163,11 @@ export default async function gisSuggest({ token, key, val, limit: limit1 = 50 }
|
|
|
163
163
|
};
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
// table:column as select query
|
|
166
167
|
const args = [];
|
|
167
168
|
const where = [];
|
|
168
169
|
|
|
169
|
-
//
|
|
170
|
+
// search by text support
|
|
170
171
|
if (key && typeof key === "string") {
|
|
171
172
|
args.push(key.toLowerCase());
|
|
172
173
|
where.push(searchQuery);
|
|
@@ -183,16 +184,6 @@ export default async function gisSuggest({ token, key, val, limit: limit1 = 50 }
|
|
|
183
184
|
where.push(`${clsMeta.pk}=any($${args.length})`);
|
|
184
185
|
}
|
|
185
186
|
|
|
186
|
-
// filter
|
|
187
|
-
if (pg.pk?.[registry.table_name] && !cls && dataTypeID) {
|
|
188
|
-
where.push(
|
|
189
|
-
`id in (select ${pg.pgType[dataTypeID]?.includes("[]")
|
|
190
|
-
? `unnest("${name}")`
|
|
191
|
-
: `"${name}"`
|
|
192
|
-
} from ${(registry.table_name).replace(/'/g, "''")})`
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
187
|
// order by value if not specified explicitly
|
|
197
188
|
const order = clsMeta.original.toLowerCase().includes("order by")
|
|
198
189
|
? ""
|
|
@@ -209,8 +200,8 @@ export default async function gisSuggest({ token, key, val, limit: limit1 = 50 }
|
|
|
209
200
|
"''"
|
|
210
201
|
)} o
|
|
211
202
|
WHERE ${pg.pgType[dataTypeID]?.includes("[]")
|
|
212
|
-
? `c.
|
|
213
|
-
: `o."${name}" IS NOT DISTINCT FROM c.
|
|
203
|
+
? `c.text = ANY(o."${name}")`
|
|
204
|
+
: `o."${name}" IS NOT DISTINCT FROM c.text`
|
|
214
205
|
} ) and ${whereQuery || "true"}
|
|
215
206
|
${order} LIMIT $${args.length}::bigint`
|
|
216
207
|
: `with c(id,text) as ( ${clsMeta.original} where ${whereQuery} ${order}) select * from c LIMIT $${args.length}::bigint`;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
|
|
3
|
-
import { getMeta, getRedis, metaFormat
|
|
3
|
+
import { getMeta, getRedis, metaFormat } from "@opengis/fastify-table/utils.js";
|
|
4
4
|
import { populateFilterOptions } from './classifiers.js';
|
|
5
5
|
import { extractVisibleColumns } from './columns.js';
|
|
6
|
+
import getFilterSQLParametrized from './getFilterSQLParametrized/index.js';
|
|
6
7
|
|
|
7
8
|
const columnType = {
|
|
8
9
|
text: 'text',
|
|
@@ -72,7 +73,10 @@ export async function handleRegistryRequest({
|
|
|
72
73
|
WHERE ${pk} = $1
|
|
73
74
|
`;
|
|
74
75
|
const rows = await pg.query(sql, [object_id]).then(el => el.rows || []);
|
|
75
|
-
|
|
76
|
+
|
|
77
|
+
if (!rows.length) {
|
|
78
|
+
throw new Error('Object not found');
|
|
79
|
+
}
|
|
76
80
|
|
|
77
81
|
await metaFormat({
|
|
78
82
|
rows, table: table_name, cls: classifiers, sufix: true,
|
|
@@ -95,13 +99,15 @@ export async function handleRegistryRequest({
|
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
const { search, filter } = query || {};
|
|
102
|
+
|
|
98
103
|
const { q: sqlFilter, args = [] } = (filter || search)
|
|
99
|
-
?
|
|
104
|
+
? getFilterSQLParametrized({
|
|
100
105
|
table: table_name,
|
|
106
|
+
fields: fields1,
|
|
101
107
|
search,
|
|
102
108
|
filter,
|
|
103
109
|
filterList: activeFilters,
|
|
104
|
-
})
|
|
110
|
+
}, pg)
|
|
105
111
|
: { q: '1=1', args: [] };
|
|
106
112
|
|
|
107
113
|
const whereConditions = [whereQuery, sqlFilter].filter(Boolean).join(' AND ');
|
|
@@ -113,7 +119,9 @@ export async function handleRegistryRequest({
|
|
|
113
119
|
const sqlSelect = `SELECT "${pk}" as id ${selectColumns.length ? `, ${selectColumns.join(", ")}` : ''} ${geom ? `, st_asgeojson(${geom})::json as geom` : ''} ${sqlBase}`;
|
|
114
120
|
const dataQuery = `${sqlSelect} ${sqlOrder} ${sqlLimit}`;
|
|
115
121
|
|
|
116
|
-
if (sql)
|
|
122
|
+
if (sql && process.platform === 'win32') {
|
|
123
|
+
return dataQuery;
|
|
124
|
+
}
|
|
117
125
|
|
|
118
126
|
const rows = await pg.query(dataQuery, args.concat([limit, offset])).then(el => el.rows || []);
|
|
119
127
|
const total = await pg.query(`SELECT COUNT(*) ${sqlBase}`, args).then(el => +(el.rows[0]?.count || 0));
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from 'vitest';
|
|
2
2
|
|
|
3
|
-
import { getPGAsync } from '@opengis/fastify-table/utils.js';
|
|
3
|
+
import { getPGAsync, getRedis } from '@opengis/fastify-table/utils.js';
|
|
4
4
|
|
|
5
5
|
import { build, injectWithHeaders } from '../../../test/helper.js';
|
|
6
6
|
|
|
7
|
-
let pg;
|
|
7
|
+
let pg, rclient;
|
|
8
8
|
|
|
9
9
|
beforeAll(async () => {
|
|
10
10
|
await build();
|
|
11
11
|
|
|
12
12
|
pg = await getPGAsync();
|
|
13
|
+
rclient = getRedis();
|
|
13
14
|
|
|
14
15
|
await cleanUp('before');
|
|
15
16
|
});
|
|
@@ -80,7 +81,8 @@ describe('GIS routes', () => {
|
|
|
80
81
|
const payload = {
|
|
81
82
|
register_key: 'unit-test',
|
|
82
83
|
name: 'unit-test',
|
|
83
|
-
table_name: table
|
|
84
|
+
table_name: table,
|
|
85
|
+
filters: [{ name: pg.pk[table], ua: 'test filter', type: 'select' }]
|
|
84
86
|
};
|
|
85
87
|
|
|
86
88
|
const res = await injectWithHeaders({
|
|
@@ -130,6 +132,33 @@ describe('GIS routes', () => {
|
|
|
130
132
|
expect(res.body.length).toBeGreaterThan(0);
|
|
131
133
|
});
|
|
132
134
|
|
|
135
|
+
test('GET /gis-suggest/:token returns 200', async () => {
|
|
136
|
+
const { table, filter } = await pg.query('select table_name as table, filters->0->>\'name\' as filter from gis.registers where register_id=$1', [testId]).then((r) => r.rows[0]);
|
|
137
|
+
expect(filter).toBeTypeOf('string');
|
|
138
|
+
const hash = 'unit-test';
|
|
139
|
+
const redisKey = `${pg.options.database}:registry-filter:${hash}`;
|
|
140
|
+
await rclient.set(redisKey, JSON.stringify({ register: testId, filter }), 'EX', 1 * 60);
|
|
141
|
+
const res = await injectWithHeaders({
|
|
142
|
+
method: 'GET',
|
|
143
|
+
url: `/api/gis-suggest/${hash}`,
|
|
144
|
+
});
|
|
145
|
+
await rclient.del(redisKey);
|
|
146
|
+
expect(res.statusCode).toBe(200);
|
|
147
|
+
expect(res.body).toHaveProperty('time');
|
|
148
|
+
expect(res.body).toHaveProperty('limit');
|
|
149
|
+
expect(res.body).toHaveProperty('count');
|
|
150
|
+
expect(res.body).toHaveProperty('total');
|
|
151
|
+
expect(res.body).toHaveProperty('data');
|
|
152
|
+
expect(res.body.data).toBeInstanceOf(Array);
|
|
153
|
+
expect(res.body.data.length).toBeGreaterThan(0);
|
|
154
|
+
expect(res.body.data[0]).toHaveProperty('count');
|
|
155
|
+
expect(res.body.data[0]).toHaveProperty('id');
|
|
156
|
+
expect(res.body.data[0]).toHaveProperty('text');
|
|
157
|
+
|
|
158
|
+
const base64url = await pg.query(`select rtrim( replace( replace( replace( encode("${filter.replace(/"/g, '')}"::bytea, 'base64'), '+', '' ), '-', '' ), '/', '' ), '=' ) as id from ${table} where ${filter}::text=$1`, [res.body.data[0].text]).then((r) => r.rows[0].id);
|
|
159
|
+
expect(res.body.data[0].id).toBe(base64url);
|
|
160
|
+
});
|
|
161
|
+
|
|
133
162
|
test('DELETE /gis-registry/:slug returns 200', async () => {
|
|
134
163
|
const res = await injectWithHeaders({
|
|
135
164
|
method: 'DELETE',
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
const queryFilterSchema = {
|
|
1
|
+
const queryFilterSchema = {
|
|
2
|
+
type: ["string", "null"],
|
|
3
|
+
pattern:
|
|
4
|
+
'^([\\w-]+)=([А-Яа-яҐґЄєІіЇї\\w\\s\\\\/\\[\\]\\(\\)\\{\\}\\|,.!?;:—_=@%#$&^*+`~]*)$',
|
|
5
|
+
};
|
|
2
6
|
const querySearchSchema = { type: 'string', pattern: '^([А-Яа-яҐґЄєІіЇї\\d\\w\\s\\/\\[\\]\\(\\)\\{\\}\\|,.!?;:—_=-@%#$&^*+=`~]+)$' };
|
|
3
7
|
|
|
4
8
|
const keySchema = {
|
|
@@ -261,7 +265,8 @@ export const gisSuggestSchema = {
|
|
|
261
265
|
additionalProperties: false,
|
|
262
266
|
properties: {
|
|
263
267
|
key: keySchema,
|
|
264
|
-
|
|
268
|
+
json: { type: 'integer', enum: [1] },
|
|
269
|
+
val: querySearchSchema,
|
|
265
270
|
limit: { type: 'integer', minimum: 1, maximum: 100 },
|
|
266
271
|
},
|
|
267
272
|
},
|