@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengis/gis",
3
- "version": "0.2.177",
3
+ "version": "0.2.178",
4
4
  "type": "module",
5
5
  "author": "Softpro",
6
6
  "main": "./dist/index.js",
@@ -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
- // select, with search by text support
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.id = ANY(o."${name}")`
213
- : `o."${name}" IS NOT DISTINCT FROM c.id`
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, getFilterSQLParametrized } from "@opengis/fastify-table/utils.js";
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
- if (!rows.length) throw new Error('Object not found');
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
- ? await getFilterSQLParametrized({
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) return dataQuery;
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 = { type: 'string', pattern: '^([\\w\\d_-]+)=([А-Яа-яҐґЄєІіЇї\\d\\w\\s\\/\\[\\]\\(\\)\\{\\}\\|,.!?;:—_=-@%#$&^*+=`~]+)$' };
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
- val: { type: "string", pattern: "([\\d\\w]+)$" },
268
+ json: { type: 'integer', enum: [1] },
269
+ val: querySearchSchema,
265
270
  limit: { type: 'integer', minimum: 1, maximum: 100 },
266
271
  },
267
272
  },