@opengis/gis 0.2.185 → 0.2.187

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.
Binary file
@@ -1,13 +1,21 @@
1
1
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { existsSync } from 'node:fs';
3
4
  import { fileURLToPath } from 'node:url';
4
5
 
5
6
  import * as solidIcons from '@fortawesome/free-solid-svg-icons';
6
7
  import sharp from 'sharp';
8
+ import pathBounds from 'svg-path-bounds';
9
+ import { openSync } from 'fontkit';
10
+
7
11
  import { BadRequestError, NotFoundError } from '@opengis/fastify-table/errors.js';
8
12
 
9
- const iconFilenamePattern = /^([a-z][a-z0-9]*)-(s|m|l|xl)-(fa|ar)-([\p{L}\p{N}-]+)\+([a-f0-9]{3,8})\.(png|svg)$/iu;
13
+ import fonts from '../fonts/index.js';
14
+
15
+ const iconFilenamePattern = /^([a-z][a-z0-9]*)-(sm|s|m|l|xl)-(fa|ar|ab|ty|mi|gl|ma|mn)-([\p{L}\p{N}_-]+)\+([a-z]+|[a-f0-9]{3,8})\.(png|svg)$/iu;
16
+
10
17
  const validColorLengths = new Set([3, 4, 6, 8]);
18
+ const validColors = new Set(['red', 'green', 'black', 'gray', 'yellow', 'white', 'blue', 'orange', 'purple', 'pink', 'brown']);
11
19
 
12
20
  const iconSizeByCode = {
13
21
  s: 24,
@@ -57,31 +65,75 @@ function parseIconFilename(filename) {
57
65
  const match = iconFilenamePattern.exec(filename);
58
66
  if (!match) return null;
59
67
 
60
- const [, pinName, sizeCode, iconType, iconValue, hexColor, format] = match;
61
- if (!validColorLengths.has(hexColor.length)) return null;
68
+ const [, pinName, sizeCode, iconType, iconValue, colorEl, format] = match;
69
+ const isHexColor = colorEl && /[a-f0-9]{3,8}/.test(colorEl.toLowerCase()); // hex or plain text
70
+ if (isHexColor && !validColorLengths.has(colorEl.length)) return null;
71
+ if (!isHexColor && !validColors.has(colorEl)) return null;
72
+
73
+ const iconFont = iconType.toLowerCase();
74
+
75
+ const iconValue1 = iconFont === 'fa'
76
+ ? iconValue.toLowerCase()
77
+ : iconValue.toUpperCase();
62
78
 
63
79
  return {
64
80
  filename: filename.toLowerCase(),
65
81
  pinName: pinName.toLowerCase(),
66
82
  size: iconSizeByCode[sizeCode.toLowerCase()],
67
- iconType: iconType.toLowerCase(),
68
- iconValue: iconType.toLowerCase() === 'fa'
69
- ? iconValue.toLowerCase()
70
- : iconValue.toUpperCase(),
71
- color: `#${hexColor}`,
83
+ iconType: iconFont,
84
+ iconValue: iconValue1,
85
+ color: isHexColor ? `#${colorEl}` : colorEl,
72
86
  format: format.toLowerCase(),
73
87
  };
74
88
  }
75
89
 
76
- function findIcon(iconName) {
90
+ const test = {};
91
+
92
+ function findIcon(iconName, iconFont) {
77
93
  const exportName = `fa${iconName.replace(/(^|-)([a-z0-9])/g, (_, __, char) => char.toUpperCase())}`;
78
94
  const exportedIcon = solidIcons[exportName];
79
95
 
80
96
  if (exportedIcon?.prefix === 'fas') return exportedIcon;
81
97
 
82
- return Object.values(solidIcons).find(icon => (
98
+ const icon1 = Object.values(solidIcons).find(icon => (
83
99
  icon?.prefix === 'fas' && icon.iconName === iconName
84
100
  ));
101
+
102
+ if (icon1) return icon1;
103
+
104
+ const { ar, font: fontName } = fonts[iconFont] || {};
105
+ const iconCode = fonts[iconFont].ar?.[iconName.toLowerCase()];
106
+
107
+ if (!fontName || !iconCode || !ar?.[iconName.toLowerCase()]) {
108
+ return null;
109
+ }
110
+
111
+ const dirname = path.dirname(fileURLToPath(import.meta.url));
112
+ if (process.platform === 'win32') test[fontName] = 1;
113
+ const fontPath = path.join(dirname, `../fonts/${fontName}`);
114
+
115
+ if (!existsSync(fontPath)) {
116
+ return null;
117
+ }
118
+
119
+ const font = openSync(fontPath);
120
+ const unicodePoint = Number.parseInt(iconCode, 16);
121
+ const glyph = font.glyphForCodePoint(unicodePoint);
122
+
123
+ const { minX, minY, maxX, maxY } = glyph.bbox;
124
+
125
+ return {
126
+ prefix: font,
127
+ iconName: iconName.toLowerCase(),
128
+ icon: [
129
+ maxX - minX,
130
+ maxY - minY,
131
+ iconName.toLowerCase(),
132
+ 99,
133
+ glyph.path.toSVG(),
134
+ ],
135
+ bbox: glyph.bbox,
136
+ };
85
137
  }
86
138
 
87
139
  async function readFileIfExists(filePath, encoding) {
@@ -107,30 +159,66 @@ function getPinPlacement(pinName, width, height) {
107
159
  };
108
160
  }
109
161
 
110
- function createFontAwesomeSvg(icon, placement, color) {
111
- const [width, height, , , pathData] = icon.icon;
112
- const paths = Array.isArray(pathData) ? pathData : [pathData];
113
- const x = placement.x - placement.iconSize / 2;
114
- const y = placement.y - placement.iconSize / 2;
115
-
116
- return `<svg data-gis-icon-layer="foreground" x="${x}" y="${y}" width="${placement.iconSize}" height="${placement.iconSize}" viewBox="0 0 ${width} ${height}" fill="${color}">
117
- ${paths.map(iconPath => `<path d="${iconPath}"/>`).join('')}
118
- </svg>`;
119
- }
120
-
121
162
  function createSymbolSvg(symbol, placement, color) {
122
163
  const fontSize = Math.round(placement.iconSize * 0.75);
123
164
 
124
165
  return `<text data-gis-icon-layer="foreground" x="${placement.x}" y="${placement.y}" fill="${color}" font-family="Arial, sans-serif" font-size="${fontSize}" font-weight="700" text-anchor="middle" dominant-baseline="middle">${symbol}</text>`;
125
166
  }
126
167
 
168
+ function createFontIconSvg(icon, placement, color) {
169
+ const [, , , , pathData] = icon.icon;
170
+ const paths = Array.isArray(pathData) ? pathData : [pathData];
171
+
172
+ const bounds = paths.reduce(
173
+ (result, path) => {
174
+ const [minX, minY, maxX, maxY] = pathBounds(path);
175
+
176
+ return {
177
+ minX: Math.min(result.minX, minX),
178
+ minY: Math.min(result.minY, minY),
179
+ maxX: Math.max(result.maxX, maxX),
180
+ maxY: Math.max(result.maxY, maxY),
181
+ };
182
+ },
183
+ {
184
+ minX: Infinity,
185
+ minY: Infinity,
186
+ maxX: -Infinity,
187
+ maxY: -Infinity,
188
+ },
189
+ );
190
+
191
+ const width = bounds.maxX - bounds.minX;
192
+ const height = bounds.maxY - bounds.minY;
193
+
194
+ const scale = placement.iconSize / Math.max(width, height);
195
+
196
+ const centerX = (bounds.minX + bounds.maxX) / 2;
197
+ const centerY = (bounds.minY + bounds.maxY) / 2;
198
+
199
+ return `<g
200
+ data-gis-icon-layer="foreground"
201
+ transform="
202
+ translate(${placement.x} ${placement.y})
203
+ scale(${scale} ${-scale})
204
+ translate(${-centerX} ${-centerY})
205
+ "
206
+ fill="${color}"
207
+ >
208
+ ${paths.map(path => `<path d="${path}"/>`).join('')}
209
+ </g>`;
210
+ }
211
+
127
212
  function createForegroundSvg(options, placement) {
128
- if (options.iconType === 'ar') {
213
+ if (options.iconValue && options.iconValue.length === 1) {
129
214
  return createSymbolSvg(options.iconValue, placement, options.color);
130
215
  }
131
216
 
132
- const icon = findIcon(options.iconValue);
133
- return icon ? createFontAwesomeSvg(icon, placement, options.color) : null;
217
+ const icon = findIcon(options.iconValue, options.iconType);
218
+
219
+ return icon
220
+ ? createFontIconSvg(icon, placement, options.color)
221
+ : null;
134
222
  }
135
223
 
136
224
  function setSvgViewport(svg, size, width, height) {
@@ -152,19 +240,36 @@ async function createPinSvg({
152
240
  }) {
153
241
  const templatePath = path.join(pinDirectory, `${pinName}.svg`);
154
242
  const template = await readFileIfExists(templatePath, 'utf8');
243
+
155
244
  if (!template) return null;
156
245
 
157
246
  const width = getSvgAttribute(template, 'width', 512);
158
247
  const height = getSvgAttribute(template, 'height', 512);
248
+
159
249
  const placement = getPinPlacement(pinName, width, height);
250
+
160
251
  const coloredTemplate = placement.recolorBackground
161
252
  ? template.replace(/#ff0000/gi, color)
162
253
  : template;
163
- const svg = setSvgViewport(coloredTemplate, size, width, height);
164
- const foreground = createForegroundSvg({ ...iconOptions, color }, placement);
254
+
255
+ const svg = setSvgViewport(
256
+ coloredTemplate,
257
+ size,
258
+ width,
259
+ height
260
+ );
261
+
262
+ const foreground = createForegroundSvg(
263
+ { ...iconOptions, color },
264
+ placement,
265
+ );
266
+
165
267
  if (!foreground) return null;
166
268
 
167
- return svg.replace(/<\/svg>\s*$/, `${foreground}</svg>`);
269
+ return svg.replace(
270
+ /<\/svg>\s*$/i,
271
+ `${foreground}</svg>`
272
+ );
168
273
  }
169
274
 
170
275
  async function createIcon(options) {
@@ -204,7 +309,8 @@ export default async function gisIconApi({ name, nocache }) {
204
309
  throw BadRequestError('invalid icon name');
205
310
  }
206
311
 
207
- const image = await getIcon(options, nocache);
312
+ // fixed icon size of 32px
313
+ const image = await getIcon({ ...options, size: 32 }, nocache);
208
314
 
209
315
  if (!image) {
210
316
  throw NotFoundError('icon not found');
@@ -149,6 +149,9 @@ export default function route(app) {
149
149
  sql: query.sql,
150
150
  pointZoom: query.pointZoom,
151
151
  }, pg);
152
+ if (process.platform === 'win32' && typeof result === 'string') {
153
+ return result;
154
+ }
152
155
  return reply.headers(result.headers).send(result.data);
153
156
  });
154
157
  }
@@ -56,6 +56,7 @@ describe('map api test', () => {
56
56
  const res = await injectWithHeaders({
57
57
  method: 'GET',
58
58
  url: `/api/gis-icon/pin6-m-fa-plus-square+7ed957.png`,
59
+ query: { nocache: 1 },
59
60
  headers: { uid: user.id, user_type: user.type }
60
61
  });
61
62
  expect(res.statusCode).toBe(200);
@@ -67,6 +68,7 @@ describe('map api test', () => {
67
68
  const res = await injectWithHeaders({
68
69
  method: 'GET',
69
70
  url: `/api/icon/pin6-m-fa-plus-square+7ed957.png`,
71
+ query: { nocache: 1 },
70
72
  headers: { uid: user.id, user_type: user.type }
71
73
  });
72
74
  expect(res.statusCode).toBe(200);
@@ -78,6 +80,7 @@ describe('map api test', () => {
78
80
  const res = await injectWithHeaders({
79
81
  method: 'GET',
80
82
  url: `/api/gis-icon/pin6-m-fa-plus-square+7ed957.svg`,
83
+ query: { nocache: 1 },
81
84
  headers: { uid: user.id, user_type: user.type }
82
85
  });
83
86
  expect(res.statusCode).toBe(200);
@@ -90,6 +93,7 @@ describe('map api test', () => {
90
93
  const res = await injectWithHeaders({
91
94
  method: 'GET',
92
95
  url: `/api/gis-icon/pin6-m-ar-E+ff4500.svg`,
96
+ query: { nocache: 1 },
93
97
  headers: { uid: user.id, user_type: user.type }
94
98
  });
95
99
  expect(res.statusCode).toBe(200);
@@ -105,6 +109,7 @@ describe('map api test', () => {
105
109
  const res = await injectWithHeaders({
106
110
  method: 'GET',
107
111
  url: `/api/gis-icon/${pin}-m-fa-plus-square+7ed957.svg`,
112
+ query: { nocache: 1 },
108
113
  headers: { uid: user.id, user_type: user.type }
109
114
  });
110
115
  expect(res.statusCode).toBe(200);
@@ -127,6 +132,7 @@ describe('map api test', () => {
127
132
  const res = await injectWithHeaders({
128
133
  method: 'GET',
129
134
  url: `/api/gis-icon/${pin}-m-fa-plus-square+7ed957.svg`,
135
+ query: { nocache: 1 },
130
136
  headers: { uid: user.id, user_type: user.type }
131
137
  });
132
138
  expect(res.statusCode).toBe(200);
@@ -137,6 +143,190 @@ describe('map api test', () => {
137
143
  },
138
144
  );
139
145
 
146
+ test('GET /gis-icon with typicons font (ty)', async () => {
147
+ const res = await injectWithHeaders({
148
+ method: 'GET',
149
+ url: `/api/gis-icon/pin-m-ty-anchor+ff0000.svg`,
150
+ query: { nocache: 1 },
151
+ headers: { uid: user.id, user_type: user.type }
152
+ });
153
+ expect(res.statusCode).toBe(200);
154
+ expect(res.headers['content-type']).toBe('image/svg+xml');
155
+ expect(res.body).toContain('<svg');
156
+ expect(res.body).toContain('fill="#ff0000"');
157
+ });
158
+
159
+ test('GET /gis-icon with maki font (ma)', async () => {
160
+ const res = await injectWithHeaders({
161
+ method: 'GET',
162
+ url: `/api/gis-icon/pin-m-ma-zoo+00ff00.svg`,
163
+ query: { nocache: 1 },
164
+ headers: { uid: user.id, user_type: user.type }
165
+ });
166
+ expect(res.statusCode).toBe(200);
167
+ expect(res.headers['content-type']).toBe('image/svg+xml');
168
+ expect(res.body).toContain('<svg');
169
+ expect(res.body).toContain('fill="#00ff00"');
170
+ });
171
+
172
+ test('GET /gis-icon with fontawesome font (fa)', async () => {
173
+ const res = await injectWithHeaders({
174
+ method: 'GET',
175
+ url: `/api/gis-icon/pin-m-fa-heart+ff0000.svg`,
176
+ query: { nocache: 1 },
177
+ headers: { uid: user.id, user_type: user.type }
178
+ });
179
+ expect(res.statusCode).toBe(200);
180
+ expect(res.headers['content-type']).toBe('image/svg+xml');
181
+ expect(res.body).toContain('<svg');
182
+ expect(res.body).toContain('fill="#ff0000"');
183
+ });
184
+
185
+ test('GET /gis-icon with map icons font (mi)', async () => {
186
+ const res = await injectWithHeaders({
187
+ method: 'GET',
188
+ url: `/api/gis-icon/pin-m-mi-hospital+0000ff.svg`,
189
+ query: { nocache: 1 },
190
+ headers: { uid: user.id, user_type: user.type }
191
+ });
192
+ expect(res.statusCode).toBe(200);
193
+ expect(res.headers['content-type']).toBe('image/svg+xml');
194
+ expect(res.body).toContain('<svg');
195
+ expect(res.body).toContain('fill="#0000ff"');
196
+ });
197
+
198
+ test('GET /gis-icon with glyphicons font (gl)', async () => {
199
+ const res = await injectWithHeaders({
200
+ method: 'GET',
201
+ url: `/api/gis-icon/pin-m-gl-heart+ffff00.svg`,
202
+ query: { nocache: 1 },
203
+ headers: { uid: user.id, user_type: user.type }
204
+ });
205
+ expect(res.statusCode).toBe(200);
206
+ expect(res.headers['content-type']).toBe('image/svg+xml');
207
+ expect(res.body).toContain('<svg');
208
+ expect(res.body).toContain('fill="#ffff00"');
209
+ });
210
+
211
+ test('GET /gis-icon with mongolia font (mn)', async () => {
212
+ const res = await injectWithHeaders({
213
+ method: 'GET',
214
+ url: `/api/gis-icon/pin-m-mn-book+ffa500.svg`,
215
+ query: { nocache: 1 },
216
+ headers: { uid: user.id, user_type: user.type }
217
+ });
218
+ expect(res.statusCode).toBe(200);
219
+ expect(res.headers['content-type']).toBe('image/svg+xml');
220
+ expect(res.body).toContain('<svg');
221
+ expect(res.body).toContain('fill="#ffa500"');
222
+ });
223
+
224
+ // Test different pin types with various fonts
225
+ test.each([
226
+ ['ty', 'anchor'],
227
+ ['ma', 'zoo'],
228
+ ['fa', 'heart'],
229
+ ['mi', 'hospital'],
230
+ ['gl', 'heart'],
231
+ ['mn', 'book']
232
+ ])('GET /gis-icon with different pins and %s font', async (fontType, icon) => {
233
+ const res = await injectWithHeaders({
234
+ method: 'GET',
235
+ url: `/api/gis-icon/pin2-m-${fontType}-${icon}+7ed957.svg`,
236
+ query: { nocache: 1 },
237
+ headers: { uid: user.id, user_type: user.type }
238
+ });
239
+ expect(res.statusCode).toBe(200);
240
+ expect(res.headers['content-type']).toBe('image/svg+xml');
241
+ expect(res.body).toContain('<svg');
242
+ expect(res.body).toContain('fill="#7ed957"');
243
+ });
244
+
245
+ // Test different sizes
246
+ test.each(['s', 'm', 'l', 'xl'])('GET /gis-icon with different sizes (%s)', async (size) => {
247
+ const res = await injectWithHeaders({
248
+ method: 'GET',
249
+ url: `/api/gis-icon/pin-${size}-fa-star+red.svg`,
250
+ query: { nocache: 1 },
251
+ headers: { uid: user.id, user_type: user.type }
252
+ });
253
+ expect(res.statusCode).toBe(200);
254
+ expect(res.headers['content-type']).toBe('image/svg+xml');
255
+ expect(res.body).toContain('<svg');
256
+ });
257
+
258
+ // Test PNG format
259
+ test('GET /gis-icon with PNG format', async () => {
260
+ const res = await injectWithHeaders({
261
+ method: 'GET',
262
+ url: `/api/gis-icon/pin-m-fa-star+blue.png`,
263
+ query: { nocache: 1 },
264
+ headers: { uid: user.id, user_type: user.type }
265
+ });
266
+ expect(res.statusCode).toBe(200);
267
+ expect(res.headers['content-type']).toBe('image/png');
268
+ expect(res.body.includes('PNG')).toBeTruthy();
269
+ });
270
+
271
+ // Test hex color formats
272
+ test.each([
273
+ ['3-digit hex', 'f00'],
274
+ ['4-digit hex', 'f00f'],
275
+ ['6-digit hex', 'ff0000'],
276
+ ['8-digit hex', 'ff0000ff']
277
+ ])('GET /gis-icon with %s color', async (format, color) => {
278
+ const res = await injectWithHeaders({
279
+ method: 'GET',
280
+ url: `/api/gis-icon/pin-m-fa-star+${color}.svg`,
281
+ query: { nocache: 1 },
282
+ headers: { uid: user.id, user_type: user.type }
283
+ });
284
+ expect(res.statusCode).toBe(200);
285
+ expect(res.headers['content-type']).toBe('image/svg+xml');
286
+ expect(res.body).toContain('<svg');
287
+ });
288
+
289
+ // Test named colors
290
+ test.each([
291
+ 'red', 'green', 'black', 'gray', 'yellow', 'white', 'blue', 'orange', 'purple', 'pink', 'brown'
292
+ ])('GET /gis-icon with named color %s', async (color) => {
293
+ const res = await injectWithHeaders({
294
+ method: 'GET',
295
+ url: `/api/gis-icon/pin-m-fa-star+${color}.svg`,
296
+ query: { nocache: 1 },
297
+ headers: { uid: user.id, user_type: user.type }
298
+ });
299
+ expect(res.statusCode).toBe(200);
300
+ expect(res.headers['content-type']).toBe('image/svg+xml');
301
+ expect(res.body).toContain('<svg');
302
+ });
303
+
304
+ // Test invalid icon name returns 400
305
+ test('GET /gis-icon with invalid name returns 400', async () => {
306
+ const res = await injectWithHeaders({
307
+ method: 'GET',
308
+ url: `/api/gis-icon/invalid-name.svg`,
309
+ query: { nocache: 1 },
310
+ headers: { uid: user.id, user_type: user.type }
311
+ });
312
+ expect(res.statusCode).toBe(400);
313
+ expect(res.json()).toHaveProperty('message');
314
+ expect(res.json().message).toContain('invalid icon name');
315
+ });
316
+
317
+ // Test icon not found returns 404
318
+ test('GET /gis-icon with non-existent icon returns 404', async () => {
319
+ const res = await injectWithHeaders({
320
+ method: 'GET',
321
+ url: `/api/gis-icon/pin-m-fa-nonexistenticon+red.svg`,
322
+ query: { nocache: 1 },
323
+ headers: { uid: user.id, user_type: user.type }
324
+ });
325
+ expect(res.statusCode).toBe(404);
326
+ expect(res.json()).toHaveProperty('message');
327
+ expect(res.json().message).toContain('icon not found');
328
+ });
329
+
140
330
  test('GET /maps', async () => {
141
331
  const res = await injectWithHeaders({
142
332
  method: 'GET', url: `/api/maps`,
@@ -88,11 +88,11 @@ export default async function getTableColumnMeta(
88
88
  ? `with c(id,text) as (select id, id as text from (select unnest("${queryColumn.replace(
89
89
  /"/g,
90
90
  ""
91
- )}") as id from ${tableName} t ${sqlTable})q group by id) select id, text from c`
91
+ )}"::text[]) as id from ${tableName} t ${sqlTable})q group by id) select id, text from c`
92
92
  : `with c(id,text) as (select id, id as text, count(*) from (select unnest("${queryColumn.replace(
93
93
  /"/g,
94
94
  ""
95
- )}") as id from ${tableName} t ${sqlTable})q group by id limit ${defaultLimit}) select * from c`;
95
+ )}"::text[]) as id from ${tableName} t ${sqlTable})q group by id limit ${defaultLimit}) select * from c`;
96
96
 
97
97
  return {
98
98
  arr,
@@ -103,17 +103,17 @@ export default async function getTableColumnMeta(
103
103
  }
104
104
 
105
105
  const original = filtered
106
- ? `with c(id,text) as (select rtrim( replace( replace( replace( encode("${queryColumn.replace(/"/g, '')}"::bytea, 'base64'), '+', '' ), '-', '' ), '/', '' ), '=' ) as id, "${queryColumn.replace(
106
+ ? `with c(id,text) as (select rtrim( replace( replace( replace( encode("${queryColumn.replace(/"/g, '')}"::text::bytea, 'base64'), '+', '' ), '-', '' ), '/', '' ), '=' ) as id, "${queryColumn.replace(
107
107
  /"/g,
108
108
  ""
109
- )}" as text from ${tableName} t ${sqlTable} group by "${queryColumn.replace(
109
+ )}"::text as text from ${tableName} t ${sqlTable} group by "${queryColumn.replace(
110
110
  /"/g,
111
111
  ""
112
112
  )}") select id, text from c`
113
- : `with c(id,text) as (select rtrim( replace( replace( replace( encode("${queryColumn.replace(/"/g, '')}"::bytea, 'base64'), '+', '' ), '-', '' ), '/', '' ), '=' ) as id, "${queryColumn.replace(
113
+ : `with c(id,text) as (select rtrim( replace( replace( replace( encode("${queryColumn.replace(/"/g, '')}"::text::bytea, 'base64'), '+', '' ), '-', '' ), '/', '' ), '=' ) as id, "${queryColumn.replace(
114
114
  /"/g,
115
115
  ""
116
- )}" as text, count(*) from ${tableName} t ${sqlTable} group by "${queryColumn.replace(
116
+ )}"::text as text, count(*) from ${tableName} t ${sqlTable} group by "${queryColumn.replace(
117
117
  /"/g,
118
118
  ""
119
119
  )}" limit ${defaultLimit}) select * from c`;
@@ -83,12 +83,12 @@ export default async function gisSuggest({ token, key, val, uid, lang = 'ua', li
83
83
  if (arr && !cls) {
84
84
  const sqlCls = count
85
85
  ? `select value, count(*) from (select ${pg.pgType?.[dataTypeID]?.includes("[]")
86
- ? `unnest("${name}")`
87
- : `"${name}"`
86
+ ? `unnest("${name}"::text[])`
87
+ : `"${name}"::text`
88
88
  } as value from ${table} where 1=1)q group by value`
89
89
  : `select array_agg(distinct value)::text[] from (select ${pg.pgType?.[dataTypeID]?.includes("[]")
90
- ? `unnest("${name}")`
91
- : `"${name}"`
90
+ ? `unnest("${name}"::text[])`
91
+ : `"${name}"::text`
92
92
  } as value from ${table} where 1=1)q`;
93
93
 
94
94
  if (pg?.pk?.[registry.table_name] && !cls) {
@@ -184,8 +184,8 @@ export default async function gisSuggest({ token, key, val, uid, lang = 'ua', li
184
184
  "''"
185
185
  )} o
186
186
  WHERE ${pg.pgType[dataTypeID]?.includes("[]")
187
- ? `c.text = ANY(o."${name}")`
188
- : `o."${name}" IS NOT DISTINCT FROM c.text`
187
+ ? `c.text = ANY(o."${name}"::text)`
188
+ : `o."${name}"::text IS NOT DISTINCT FROM c.text`
189
189
  } ) and ${whereQuery || "true"}
190
190
  ${order} LIMIT $${args.length}::bigint`
191
191
  : `with c(id,text) as ( ${clsMeta.original} where ${whereQuery} ${order}) select * from c LIMIT $${args.length}::bigint`;