@teleporthq/teleport-plugin-next-data-source 0.44.7 → 0.44.12
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/__tests__/items-path.test.ts +3 -2
- package/__tests__/search-relevance.test.ts +260 -0
- package/dist/cjs/fetchers/csv-file.d.ts.map +1 -1
- package/dist/cjs/fetchers/csv-file.js +4 -3
- package/dist/cjs/fetchers/csv-file.js.map +1 -1
- package/dist/cjs/fetchers/firestore.js +1 -1
- package/dist/cjs/fetchers/firestore.js.map +1 -1
- package/dist/cjs/fetchers/google-sheets.d.ts.map +1 -1
- package/dist/cjs/fetchers/google-sheets.js +2 -1
- package/dist/cjs/fetchers/google-sheets.js.map +1 -1
- package/dist/cjs/fetchers/javascript.d.ts.map +1 -1
- package/dist/cjs/fetchers/javascript.js +2 -1
- package/dist/cjs/fetchers/javascript.js.map +1 -1
- package/dist/cjs/fetchers/mariadb.d.ts.map +1 -1
- package/dist/cjs/fetchers/mariadb.js +4 -3
- package/dist/cjs/fetchers/mariadb.js.map +1 -1
- package/dist/cjs/fetchers/mysql.d.ts.map +1 -1
- package/dist/cjs/fetchers/mysql.js +3 -2
- package/dist/cjs/fetchers/mysql.js.map +1 -1
- package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
- package/dist/cjs/fetchers/postgresql.js +3 -2
- package/dist/cjs/fetchers/postgresql.js.map +1 -1
- package/dist/cjs/fetchers/redshift.d.ts.map +1 -1
- package/dist/cjs/fetchers/redshift.js +3 -2
- package/dist/cjs/fetchers/redshift.js.map +1 -1
- package/dist/cjs/fetchers/rest-api.d.ts.map +1 -1
- package/dist/cjs/fetchers/rest-api.js +3 -2
- package/dist/cjs/fetchers/rest-api.js.map +1 -1
- package/dist/cjs/fetchers/supabase.d.ts.map +1 -1
- package/dist/cjs/fetchers/supabase.js +3 -2
- package/dist/cjs/fetchers/supabase.js.map +1 -1
- package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
- package/dist/cjs/fetchers/teleport.js +4 -3
- package/dist/cjs/fetchers/teleport.js.map +1 -1
- package/dist/cjs/fetchers/turso.d.ts.map +1 -1
- package/dist/cjs/fetchers/turso.js +2 -1
- package/dist/cjs/fetchers/turso.js.map +1 -1
- package/dist/cjs/search-relevance.d.ts +40 -0
- package/dist/cjs/search-relevance.d.ts.map +1 -0
- package/dist/cjs/search-relevance.js +46 -0
- package/dist/cjs/search-relevance.js.map +1 -0
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/fetchers/csv-file.d.ts.map +1 -1
- package/dist/esm/fetchers/csv-file.js +4 -3
- package/dist/esm/fetchers/csv-file.js.map +1 -1
- package/dist/esm/fetchers/firestore.js +1 -1
- package/dist/esm/fetchers/firestore.js.map +1 -1
- package/dist/esm/fetchers/google-sheets.d.ts.map +1 -1
- package/dist/esm/fetchers/google-sheets.js +2 -1
- package/dist/esm/fetchers/google-sheets.js.map +1 -1
- package/dist/esm/fetchers/javascript.d.ts.map +1 -1
- package/dist/esm/fetchers/javascript.js +2 -1
- package/dist/esm/fetchers/javascript.js.map +1 -1
- package/dist/esm/fetchers/mariadb.d.ts.map +1 -1
- package/dist/esm/fetchers/mariadb.js +4 -3
- package/dist/esm/fetchers/mariadb.js.map +1 -1
- package/dist/esm/fetchers/mysql.d.ts.map +1 -1
- package/dist/esm/fetchers/mysql.js +3 -2
- package/dist/esm/fetchers/mysql.js.map +1 -1
- package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
- package/dist/esm/fetchers/postgresql.js +3 -2
- package/dist/esm/fetchers/postgresql.js.map +1 -1
- package/dist/esm/fetchers/redshift.d.ts.map +1 -1
- package/dist/esm/fetchers/redshift.js +3 -2
- package/dist/esm/fetchers/redshift.js.map +1 -1
- package/dist/esm/fetchers/rest-api.d.ts.map +1 -1
- package/dist/esm/fetchers/rest-api.js +3 -2
- package/dist/esm/fetchers/rest-api.js.map +1 -1
- package/dist/esm/fetchers/supabase.d.ts.map +1 -1
- package/dist/esm/fetchers/supabase.js +3 -2
- package/dist/esm/fetchers/supabase.js.map +1 -1
- package/dist/esm/fetchers/teleport.d.ts.map +1 -1
- package/dist/esm/fetchers/teleport.js +4 -3
- package/dist/esm/fetchers/teleport.js.map +1 -1
- package/dist/esm/fetchers/turso.d.ts.map +1 -1
- package/dist/esm/fetchers/turso.js +2 -1
- package/dist/esm/fetchers/turso.js.map +1 -1
- package/dist/esm/search-relevance.d.ts +40 -0
- package/dist/esm/search-relevance.d.ts.map +1 -0
- package/dist/esm/search-relevance.js +40 -0
- package/dist/esm/search-relevance.js.map +1 -0
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/src/fetchers/csv-file.ts +16 -5
- package/src/fetchers/firestore.ts +2 -2
- package/src/fetchers/google-sheets.ts +14 -5
- package/src/fetchers/javascript.ts +12 -5
- package/src/fetchers/mariadb.ts +21 -26
- package/src/fetchers/mysql.ts +21 -26
- package/src/fetchers/postgresql.ts +24 -24
- package/src/fetchers/redshift.ts +18 -17
- package/src/fetchers/rest-api.ts +12 -5
- package/src/fetchers/supabase.ts +27 -20
- package/src/fetchers/teleport.ts +24 -24
- package/src/fetchers/turso.ts +20 -19
- package/src/search-relevance.ts +96 -0
package/src/fetchers/mariadb.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
generateFilterTreeHelpersCode,
|
|
6
6
|
generateSearchEscapeHelpersCode,
|
|
7
7
|
} from '../utils'
|
|
8
|
+
import { generateSearchSqlHelpersCode, MARIADB_SEARCH_MATCH_SQL } from '../search-relevance'
|
|
8
9
|
|
|
9
10
|
interface MariaDBConfig {
|
|
10
11
|
host?: string
|
|
@@ -31,6 +32,7 @@ ${generateSafeJSONParseCode()}
|
|
|
31
32
|
${generateFilterTreeHelpersCode()}
|
|
32
33
|
|
|
33
34
|
${generateSearchEscapeHelpersCode()}
|
|
35
|
+
${generateSearchSqlHelpersCode(MARIADB_SEARCH_MATCH_SQL)}
|
|
34
36
|
|
|
35
37
|
// Helper function to process filters and build conditions
|
|
36
38
|
const processFilters = (filters, conditions, queryParams) => {
|
|
@@ -110,6 +112,7 @@ export default async function handler(req, res) {
|
|
|
110
112
|
|
|
111
113
|
const conditions = []
|
|
112
114
|
const queryParams = []
|
|
115
|
+
let search = null
|
|
113
116
|
|
|
114
117
|
if (query) {
|
|
115
118
|
let columns = []
|
|
@@ -135,14 +138,10 @@ export default async function handler(req, res) {
|
|
|
135
138
|
}
|
|
136
139
|
}
|
|
137
140
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
"LOWER(CAST(\`" + sanitizeSearchIdentifier(col) + "\` AS CHAR)) LIKE LOWER(?) ESCAPE '|'"
|
|
143
|
-
)
|
|
144
|
-
columns.forEach(() => queryParams.push(pattern))
|
|
145
|
-
conditions.push("(" + searchConditions.join(" OR ") + ")")
|
|
141
|
+
search = buildSearchSql(query, columns)
|
|
142
|
+
if (search) {
|
|
143
|
+
conditions.push(search.where)
|
|
144
|
+
queryParams.push(...search.params)
|
|
146
145
|
}
|
|
147
146
|
}
|
|
148
147
|
|
|
@@ -155,22 +154,24 @@ export default async function handler(req, res) {
|
|
|
155
154
|
sql += \` WHERE \${conditions.join(' AND ')}\`
|
|
156
155
|
}
|
|
157
156
|
|
|
158
|
-
// Handle sorts - new array format
|
|
157
|
+
// Handle sorts - new array format. A search orders its matches by where
|
|
158
|
+
// the words are first; the sort orders the rows within each of those groups.
|
|
159
|
+
const orderClauses = search && search.rank ? [search.rank] : []
|
|
159
160
|
if (sorts) {
|
|
160
161
|
const parsedSorts = safeJSONParse(sorts)
|
|
161
162
|
if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
|
|
162
|
-
|
|
163
|
+
orderClauses.push(...parsedSorts.map((sort) => {
|
|
163
164
|
if (!sort.field) return null
|
|
164
165
|
const order = (sort.order || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'
|
|
165
166
|
return \`\\\`\${sort.field}\\\` \${order}\`
|
|
166
|
-
}).filter(Boolean)
|
|
167
|
-
|
|
168
|
-
if (orderClauses.length > 0) {
|
|
169
|
-
sql += \` ORDER BY \${orderClauses.join(', ')}\`
|
|
170
|
-
}
|
|
167
|
+
}).filter(Boolean))
|
|
171
168
|
}
|
|
172
169
|
} else if (sortBy) {
|
|
173
|
-
|
|
170
|
+
orderClauses.push(\`\\\`\${sortBy}\\\` \${(sortOrder || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'}\`)
|
|
171
|
+
}
|
|
172
|
+
if (orderClauses.length > 0) {
|
|
173
|
+
sql += \` ORDER BY \${orderClauses.join(', ')}\`
|
|
174
|
+
if (search && search.rank) queryParams.push(...search.rankParams)
|
|
174
175
|
}
|
|
175
176
|
|
|
176
177
|
const limitValue = limit || perPage
|
|
@@ -284,16 +285,10 @@ async function getCount(req, res) {
|
|
|
284
285
|
}
|
|
285
286
|
}
|
|
286
287
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
(col) =>
|
|
292
|
-
"LOWER(CAST(\`" + sanitizeSearchIdentifier(col) + "\` AS CHAR)) LIKE LOWER(?) ESCAPE '|'"
|
|
293
|
-
)
|
|
294
|
-
.join(" OR ")
|
|
295
|
-
conditions.push("(" + searchConditions + ")")
|
|
296
|
-
columns.forEach(() => queryParams.push(pattern))
|
|
288
|
+
const search = buildSearchSql(query, columns)
|
|
289
|
+
if (search) {
|
|
290
|
+
conditions.push(search.where)
|
|
291
|
+
queryParams.push(...search.params)
|
|
297
292
|
}
|
|
298
293
|
}
|
|
299
294
|
|
package/src/fetchers/mysql.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
generateFilterTreeHelpersCode,
|
|
6
6
|
generateSearchEscapeHelpersCode,
|
|
7
7
|
} from '../utils'
|
|
8
|
+
import { generateSearchSqlHelpersCode, MYSQL_SEARCH_MATCH_SQL } from '../search-relevance'
|
|
8
9
|
|
|
9
10
|
interface MySQLConfig {
|
|
10
11
|
host?: string
|
|
@@ -68,6 +69,7 @@ ${generateSafeJSONParseCode()}
|
|
|
68
69
|
${generateFilterTreeHelpersCode()}
|
|
69
70
|
|
|
70
71
|
${generateSearchEscapeHelpersCode()}
|
|
72
|
+
${generateSearchSqlHelpersCode(MYSQL_SEARCH_MATCH_SQL)}
|
|
71
73
|
|
|
72
74
|
// Helper function to process filters and build conditions
|
|
73
75
|
const processFilters = (filters, conditions, queryParams) => {
|
|
@@ -119,6 +121,7 @@ export default async function handler(req, res) {
|
|
|
119
121
|
|
|
120
122
|
const conditions = []
|
|
121
123
|
const queryParams = []
|
|
124
|
+
let search = null
|
|
122
125
|
|
|
123
126
|
if (query) {
|
|
124
127
|
let columns = []
|
|
@@ -144,14 +147,10 @@ export default async function handler(req, res) {
|
|
|
144
147
|
}
|
|
145
148
|
}
|
|
146
149
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
"LOWER(CAST(" + mysql.escapeId(sanitizeSearchIdentifier(col)) + " AS CHAR)) LIKE LOWER(?) ESCAPE '|'"
|
|
152
|
-
)
|
|
153
|
-
columns.forEach(() => queryParams.push(pattern))
|
|
154
|
-
conditions.push("(" + searchConditions.join(" OR ") + ")")
|
|
150
|
+
search = buildSearchSql(query, columns)
|
|
151
|
+
if (search) {
|
|
152
|
+
conditions.push(search.where)
|
|
153
|
+
queryParams.push(...search.params)
|
|
155
154
|
}
|
|
156
155
|
}
|
|
157
156
|
|
|
@@ -164,22 +163,24 @@ export default async function handler(req, res) {
|
|
|
164
163
|
sql += \` WHERE \${conditions.join(' AND ')}\`
|
|
165
164
|
}
|
|
166
165
|
|
|
167
|
-
// Handle sorts - new array format
|
|
166
|
+
// Handle sorts - new array format. A search orders its matches by where
|
|
167
|
+
// the words are first; the sort orders the rows within each of those groups.
|
|
168
|
+
const orderClauses = search && search.rank ? [search.rank] : []
|
|
168
169
|
if (sorts) {
|
|
169
170
|
const parsedSorts = safeJSONParse(sorts)
|
|
170
171
|
if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
|
|
171
|
-
|
|
172
|
+
orderClauses.push(...parsedSorts.map((sort) => {
|
|
172
173
|
if (!sort.field) return null
|
|
173
174
|
const order = (sort.order || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'
|
|
174
175
|
return \`\${mysql.escapeId(sort.field)} \${order}\`
|
|
175
|
-
}).filter(Boolean)
|
|
176
|
-
|
|
177
|
-
if (orderClauses.length > 0) {
|
|
178
|
-
sql += \` ORDER BY \${orderClauses.join(', ')}\`
|
|
179
|
-
}
|
|
176
|
+
}).filter(Boolean))
|
|
180
177
|
}
|
|
181
178
|
} else if (sortBy) {
|
|
182
|
-
|
|
179
|
+
orderClauses.push(\`\${mysql.escapeId(sortBy)} \${(sortOrder || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'}\`)
|
|
180
|
+
}
|
|
181
|
+
if (orderClauses.length > 0) {
|
|
182
|
+
sql += \` ORDER BY \${orderClauses.join(', ')}\`
|
|
183
|
+
if (search && search.rank) queryParams.push(...search.rankParams)
|
|
183
184
|
}
|
|
184
185
|
|
|
185
186
|
const limitValue = limit || perPage
|
|
@@ -263,16 +264,10 @@ async function getCount(req, res) {
|
|
|
263
264
|
}
|
|
264
265
|
}
|
|
265
266
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
(col) =>
|
|
271
|
-
"LOWER(CAST(" + mysql.escapeId(sanitizeSearchIdentifier(col)) + " AS CHAR)) LIKE LOWER(?) ESCAPE '|'"
|
|
272
|
-
)
|
|
273
|
-
.join(" OR ")
|
|
274
|
-
conditions.push("(" + searchConditions + ")")
|
|
275
|
-
columns.forEach(() => queryParams.push(pattern))
|
|
267
|
+
const search = buildSearchSql(query, columns)
|
|
268
|
+
if (search) {
|
|
269
|
+
conditions.push(search.where)
|
|
270
|
+
queryParams.push(...search.params)
|
|
276
271
|
}
|
|
277
272
|
}
|
|
278
273
|
|
|
@@ -10,6 +10,7 @@ import { SORT_NULLS_LAST_SQL } from '../list-sort-order'
|
|
|
10
10
|
import { generateRowOrderTiebreakHelper, rowIdentityColumn } from '../row-order'
|
|
11
11
|
import { generateProductFilterClauseHelper } from '../product-filter-fields'
|
|
12
12
|
import { generateReadGuardCall, generateTableAccessPreamble } from './utils/table-access-guard'
|
|
13
|
+
import { generateSearchSqlHelpersCode, PG_SEARCH_MATCH_SQL } from '../search-relevance'
|
|
13
14
|
import { generateRequestSqlGuardsCode } from './utils/request-sql-guards'
|
|
14
15
|
import { generateArrayOverlapSqlCode } from '../array-overlap-sql'
|
|
15
16
|
import { PG_CLIENT_IMPORT } from './pg-client-import'
|
|
@@ -72,6 +73,7 @@ ${generateSafeJSONParseCode()}
|
|
|
72
73
|
${generateFilterTreeHelpersCode()}
|
|
73
74
|
|
|
74
75
|
${generateSearchEscapeHelpersCode()}
|
|
76
|
+
${generateSearchSqlHelpersCode(PG_SEARCH_MATCH_SQL)}
|
|
75
77
|
${generateArrayOverlapSqlCode()}
|
|
76
78
|
|
|
77
79
|
// Builds one SQL clause for the whole filter tree, so an OR group nested in
|
|
@@ -173,6 +175,7 @@ ${generateReadGuardCall(tableName, null)}
|
|
|
173
175
|
const conditions = []
|
|
174
176
|
const queryParams = []
|
|
175
177
|
let paramIndex = 1
|
|
178
|
+
let searchRank = ''
|
|
176
179
|
|
|
177
180
|
if (query) {
|
|
178
181
|
let columns = []
|
|
@@ -205,15 +208,12 @@ ${generateReadGuardCall(tableName, null)}
|
|
|
205
208
|
}
|
|
206
209
|
if (__rowPolicy) columns = __brpVisibleColumns(__rowPolicy, columns)
|
|
207
210
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
(col) => '"' + sanitizeSearchIdentifier(col) + '"::text ILIKE ' + placeholder + " ESCAPE '|'"
|
|
215
|
-
)
|
|
216
|
-
conditions.push('(' + searchConditions.join(' OR ') + ')')
|
|
211
|
+
const search = buildSearchSql(query, columns, paramIndex)
|
|
212
|
+
if (search) {
|
|
213
|
+
conditions.push(search.where)
|
|
214
|
+
queryParams.push(...search.params)
|
|
215
|
+
paramIndex = search.nextIndex
|
|
216
|
+
searchRank = search.rank
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
219
|
|
|
@@ -230,8 +230,11 @@ ${generateReadGuardCall(tableName, null)}
|
|
|
230
230
|
// Handle sorts - new array format. Two ORDER BY clauses are built: the one
|
|
231
231
|
// actually used (the products table's price rewrites, rows without a value
|
|
232
232
|
// last), and a plain-column twin kept only as the fallback below.
|
|
233
|
+
// A search orders its matches by where the words are first; the sort
|
|
234
|
+
// orders the rows within each of those groups.
|
|
233
235
|
let orderBySql = ''
|
|
234
236
|
let plainOrderBySql = ''
|
|
237
|
+
const searchOrder = searchRank ? searchRank + ', ' : ''
|
|
235
238
|
if (sorts) {
|
|
236
239
|
const parsedSorts = safeJSONParse(sorts)
|
|
237
240
|
if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
|
|
@@ -245,16 +248,19 @@ ${generateReadGuardCall(tableName, null)}
|
|
|
245
248
|
// The row's own key last, so equal rows can't repeat across pages
|
|
246
249
|
// or vanish between them.
|
|
247
250
|
const tiebreak = rowOrderTiebreak(valid.map((sort) => sort.field), orderOf(valid[valid.length - 1]))
|
|
248
|
-
orderBySql = \` ORDER BY \${orderClauses.join(', ')}\${tiebreak}\`
|
|
249
|
-
plainOrderBySql = \` ORDER BY \${plainClauses.join(', ')}\${tiebreak}\`
|
|
251
|
+
orderBySql = \` ORDER BY \${searchOrder}\${orderClauses.join(', ')}\${tiebreak}\`
|
|
252
|
+
plainOrderBySql = \` ORDER BY \${searchOrder}\${plainClauses.join(', ')}\${tiebreak}\`
|
|
250
253
|
}
|
|
251
254
|
}
|
|
252
255
|
} else if (sortBy) {
|
|
253
256
|
assertRequestColumn(sortBy, 'sort field')
|
|
254
257
|
const sortByDirection = (sortOrder || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'
|
|
255
258
|
const tiebreak = rowOrderTiebreak([sortBy], sortByDirection)
|
|
256
|
-
orderBySql = \` ORDER BY \${sortBy} \${sortByDirection}${SORT_NULLS_LAST_SQL}\${tiebreak}\`
|
|
257
|
-
plainOrderBySql = \` ORDER BY \${sortBy} \${sortByDirection}\${tiebreak}\`
|
|
259
|
+
orderBySql = \` ORDER BY \${searchOrder}\${sortBy} \${sortByDirection}${SORT_NULLS_LAST_SQL}\${tiebreak}\`
|
|
260
|
+
plainOrderBySql = \` ORDER BY \${searchOrder}\${sortBy} \${sortByDirection}\${tiebreak}\`
|
|
261
|
+
}
|
|
262
|
+
if (!orderBySql && searchRank) {
|
|
263
|
+
orderBySql = plainOrderBySql = ' ORDER BY ' + searchRank + rowOrderTiebreak([], 'ASC')
|
|
258
264
|
}
|
|
259
265
|
const usedRewrittenSort = orderBySql !== plainOrderBySql
|
|
260
266
|
|
|
@@ -379,17 +385,11 @@ ${generateReadGuardCall(tableName, null)}
|
|
|
379
385
|
}
|
|
380
386
|
if (__rowPolicy) columns = __brpVisibleColumns(__rowPolicy, columns)
|
|
381
387
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const searchConditions = columns
|
|
388
|
-
.map(
|
|
389
|
-
(col) => '"' + sanitizeSearchIdentifier(col) + '"::text ILIKE ' + placeholder + " ESCAPE '|'"
|
|
390
|
-
)
|
|
391
|
-
.join(' OR ')
|
|
392
|
-
conditions.push('(' + searchConditions + ')')
|
|
388
|
+
const search = buildSearchSql(query, columns, paramIndex)
|
|
389
|
+
if (search) {
|
|
390
|
+
conditions.push(search.where)
|
|
391
|
+
queryParams.push(...search.params)
|
|
392
|
+
paramIndex = search.nextIndex
|
|
393
393
|
}
|
|
394
394
|
}
|
|
395
395
|
|
package/src/fetchers/redshift.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
generateFilterTreeHelpersCode,
|
|
6
6
|
generateSearchEscapeHelpersCode,
|
|
7
7
|
} from '../utils'
|
|
8
|
+
import { generateSearchSqlHelpersCode, PG_SEARCH_MATCH_SQL } from '../search-relevance'
|
|
8
9
|
|
|
9
10
|
interface RedshiftConfig {
|
|
10
11
|
host?: string
|
|
@@ -60,6 +61,7 @@ ${generateSafeJSONParseCode()}
|
|
|
60
61
|
${generateFilterTreeHelpersCode()}
|
|
61
62
|
|
|
62
63
|
${generateSearchEscapeHelpersCode()}
|
|
64
|
+
${generateSearchSqlHelpersCode(PG_SEARCH_MATCH_SQL)}
|
|
63
65
|
|
|
64
66
|
${generateDateFormatterCode()}
|
|
65
67
|
|
|
@@ -75,6 +77,7 @@ export default async function handler(req, res) {
|
|
|
75
77
|
const conditions = []
|
|
76
78
|
const queryParams = []
|
|
77
79
|
let paramIndex = 1
|
|
80
|
+
let search = null
|
|
78
81
|
|
|
79
82
|
if (query) {
|
|
80
83
|
let columns = []
|
|
@@ -100,15 +103,11 @@ export default async function handler(req, res) {
|
|
|
100
103
|
}
|
|
101
104
|
}
|
|
102
105
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const searchConditions = columns.map(
|
|
109
|
-
(col) => '"' + sanitizeSearchIdentifier(col) + '"::text ILIKE ' + placeholder + " ESCAPE '|'"
|
|
110
|
-
)
|
|
111
|
-
conditions.push('(' + searchConditions.join(' OR ') + ')')
|
|
106
|
+
search = buildSearchSql(query, columns, paramIndex)
|
|
107
|
+
if (search) {
|
|
108
|
+
conditions.push(search.where)
|
|
109
|
+
queryParams.push(...search.params)
|
|
110
|
+
paramIndex = search.nextIndex
|
|
112
111
|
}
|
|
113
112
|
}
|
|
114
113
|
|
|
@@ -166,22 +165,24 @@ export default async function handler(req, res) {
|
|
|
166
165
|
sql += \` WHERE \${conditions.join(' AND ')}\`
|
|
167
166
|
}
|
|
168
167
|
|
|
169
|
-
// Handle sorts - new array format
|
|
168
|
+
// Handle sorts - new array format. A search orders its matches by where
|
|
169
|
+
// the words are first; the sort orders the rows within each of those groups.
|
|
170
|
+
const orderClauses = search && search.rank ? [search.rank] : []
|
|
170
171
|
if (sorts) {
|
|
171
172
|
const parsedSorts = safeJSONParse(sorts)
|
|
172
173
|
if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
|
|
173
|
-
|
|
174
|
+
orderClauses.push(...parsedSorts.map((sort) => {
|
|
174
175
|
if (!sort.field) return null
|
|
175
176
|
const order = (sort.order || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'
|
|
176
177
|
return \`\${sanitizeIdentifier(sort.field)} \${order}\`
|
|
177
|
-
}).filter(Boolean)
|
|
178
|
-
|
|
179
|
-
if (orderClauses.length > 0) {
|
|
180
|
-
sql += \` ORDER BY \${orderClauses.join(', ')}\`
|
|
181
|
-
}
|
|
178
|
+
}).filter(Boolean))
|
|
182
179
|
}
|
|
183
180
|
} else if (sortBy) {
|
|
184
|
-
|
|
181
|
+
orderClauses.push(\`\${sanitizeIdentifier(sortBy)} \${(sortOrder || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'}\`)
|
|
182
|
+
}
|
|
183
|
+
if (orderClauses.length > 0) {
|
|
184
|
+
sql += \` ORDER BY \${orderClauses.join(', ')}\`
|
|
185
|
+
if (search && search.rank) queryParams.push(...search.rankParams)
|
|
185
186
|
}
|
|
186
187
|
|
|
187
188
|
const limitValue = limit || perPage
|
package/src/fetchers/rest-api.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
generateSafeJSONParseCode,
|
|
6
6
|
generateFilterTreeHelpersCode,
|
|
7
7
|
} from '../utils'
|
|
8
|
+
import { generateSearchRankHelperCode } from '../search-relevance'
|
|
8
9
|
import {
|
|
9
10
|
RESTAPICacheConfig,
|
|
10
11
|
generateRESTAPIResponseCacheCode,
|
|
@@ -124,6 +125,8 @@ export const generateRESTAPIFetcher = (config: Record<string, unknown>): string
|
|
|
124
125
|
|
|
125
126
|
${generateSafeJSONParseCode()}
|
|
126
127
|
|
|
128
|
+
${generateSearchRankHelperCode()}
|
|
129
|
+
|
|
127
130
|
${generateDateFormatterCode()}
|
|
128
131
|
|
|
129
132
|
${generateSortFilterHelperCode()}
|
|
@@ -194,18 +197,18 @@ export default async function handler(req, res) {
|
|
|
194
197
|
|
|
195
198
|
if (Array.isArray(data)) {
|
|
196
199
|
if (query && query.trim()) {
|
|
197
|
-
const
|
|
200
|
+
const searchTerms = query.toLowerCase().split(' ').filter(Boolean)
|
|
198
201
|
|
|
199
202
|
if (queryColumns) {
|
|
200
203
|
try {
|
|
201
204
|
const parsed = safeJSONParse(queryColumns)
|
|
202
205
|
const columns = Array.isArray(parsed) ? parsed : [parsed]
|
|
203
206
|
data = data.filter((item) => {
|
|
204
|
-
return columns.some((col) => {
|
|
207
|
+
return searchTerms.every((term) => columns.some((col) => {
|
|
205
208
|
const value = getNestedValue(item, col)
|
|
206
209
|
if (value === null || value === undefined) return false
|
|
207
|
-
return String(value).toLowerCase().includes(
|
|
208
|
-
})
|
|
210
|
+
return String(value).toLowerCase().includes(term)
|
|
211
|
+
}))
|
|
209
212
|
})
|
|
210
213
|
} catch (err) {
|
|
211
214
|
console.error('Error parsing queryColumns:', err)
|
|
@@ -214,7 +217,7 @@ export default async function handler(req, res) {
|
|
|
214
217
|
data = data.filter((item) => {
|
|
215
218
|
try {
|
|
216
219
|
const stringified = JSON.stringify(item).toLowerCase()
|
|
217
|
-
return stringified.includes(
|
|
220
|
+
return searchTerms.every((term) => stringified.includes(term))
|
|
218
221
|
} catch {
|
|
219
222
|
return false
|
|
220
223
|
}
|
|
@@ -310,6 +313,10 @@ export default async function handler(req, res) {
|
|
|
310
313
|
})
|
|
311
314
|
}
|
|
312
315
|
|
|
316
|
+
if (query && queryColumns) {
|
|
317
|
+
data = rankBySearch(data, query, safeJSONParse(queryColumns), getNestedValue)
|
|
318
|
+
}
|
|
319
|
+
|
|
313
320
|
const limitValue = limit || perPage
|
|
314
321
|
const pageValue = page ? Math.max(1, parseInt(page)) : undefined
|
|
315
322
|
const offsetValue = offset !== undefined ? Math.max(0, parseInt(offset)) : (pageValue && perPage ? (pageValue - 1) * Math.max(1, parseInt(perPage)) : 0)
|
package/src/fetchers/supabase.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
generateSearchEscapeHelpersCode,
|
|
6
6
|
generateFilterTreeHelpersCode,
|
|
7
7
|
} from '../utils'
|
|
8
|
+
import { generateSearchWordsCode } from '../search-relevance'
|
|
8
9
|
|
|
9
10
|
export const validateSupabaseConfig = (
|
|
10
11
|
config: Record<string, unknown>
|
|
@@ -131,6 +132,7 @@ ${generateSafeJSONParseCode()}
|
|
|
131
132
|
${generateFilterTreeHelpersCode()}
|
|
132
133
|
|
|
133
134
|
${generateSearchEscapeHelpersCode()}
|
|
135
|
+
${generateSearchWordsCode()}
|
|
134
136
|
|
|
135
137
|
// Helper function to process filter values
|
|
136
138
|
const processFilterValue = (value) => {
|
|
@@ -354,16 +356,19 @@ export default async function handler(req, res) {
|
|
|
354
356
|
// shared identifier sanitizer to reject injection. Note:
|
|
355
357
|
// PostgREST .ilike. does not expose a LIKE ESCAPE clause, so
|
|
356
358
|
// raw % / _ in the user input still act as wildcards in this
|
|
357
|
-
// backend; the canvas preview path escapes them explicitly.
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
359
|
+
// backend; the canvas preview path escapes them explicitly. Each
|
|
360
|
+
// typed word gets its own .or(), and PostgREST ANDs them.
|
|
361
|
+
searchWords(query).forEach((word) => {
|
|
362
|
+
const rawPattern = "%" + word + "%"
|
|
363
|
+
const escapedForPostgrest = rawPattern
|
|
364
|
+
.replace(/\\\\/g, "\\\\\\\\")
|
|
365
|
+
.replace(/"/g, '\\\\"')
|
|
366
|
+
const searchPattern = '"' + escapedForPostgrest + '"'
|
|
367
|
+
const orConditions = columns
|
|
368
|
+
.map((col) => sanitizeSearchIdentifier(col) + ".ilike." + searchPattern)
|
|
369
|
+
.join(",")
|
|
370
|
+
queryRef = queryRef.or(orConditions)
|
|
371
|
+
})
|
|
367
372
|
}
|
|
368
373
|
}
|
|
369
374
|
|
|
@@ -478,16 +483,18 @@ async function getCount(req, res) {
|
|
|
478
483
|
if (columns.length > 0) {
|
|
479
484
|
// Mirror the fetch handler: sanitize identifiers, wrap the
|
|
480
485
|
// pattern in double quotes for PostgREST .or() DSL, and escape
|
|
481
|
-
// backslashes / quotes in the user's search term.
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
486
|
+
// backslashes / quotes in the user's search term, one .or() per word.
|
|
487
|
+
searchWords(query).forEach((word) => {
|
|
488
|
+
const rawPattern = "%" + word + "%"
|
|
489
|
+
const escapedForPostgrest = rawPattern
|
|
490
|
+
.replace(/\\\\/g, "\\\\\\\\")
|
|
491
|
+
.replace(/"/g, '\\\\"')
|
|
492
|
+
const searchPattern = '"' + escapedForPostgrest + '"'
|
|
493
|
+
const orConditions = columns
|
|
494
|
+
.map((col) => sanitizeSearchIdentifier(col) + ".ilike." + searchPattern)
|
|
495
|
+
.join(",")
|
|
496
|
+
countQuery = countQuery.or(orConditions)
|
|
497
|
+
})
|
|
491
498
|
}
|
|
492
499
|
}
|
|
493
500
|
|
package/src/fetchers/teleport.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { REQUEST_LOCALE_PARAM } from '../request-locale'
|
|
|
20
20
|
import { generateReadGuardCall, generateTableAccessPreamble } from './utils/table-access-guard'
|
|
21
21
|
import { generateRequestSqlGuardsCode } from './utils/request-sql-guards'
|
|
22
22
|
import { generateArrayOverlapSqlCode } from '../array-overlap-sql'
|
|
23
|
+
import { generateSearchSqlHelpersCode, PG_SEARCH_MATCH_SQL } from '../search-relevance'
|
|
23
24
|
import { PG_CLIENT_IMPORT } from './pg-client-import'
|
|
24
25
|
|
|
25
26
|
interface TeleportDBConfig {
|
|
@@ -162,6 +163,7 @@ ${generateSafeJSONParseCode()}
|
|
|
162
163
|
${generateFilterTreeHelpersCode()}
|
|
163
164
|
|
|
164
165
|
${generateSearchEscapeHelpersCode()}
|
|
166
|
+
${generateSearchSqlHelpersCode(PG_SEARCH_MATCH_SQL)}
|
|
165
167
|
${generateArrayOverlapSqlCode()}
|
|
166
168
|
${getTransformationCode(tableName, transformOptions)}
|
|
167
169
|
${getTransformWrapperCode(tableName, transformOptions)}
|
|
@@ -346,6 +348,7 @@ ${generateReadGuardCall(tableName, 'req.query && req.query.rawQuery')}
|
|
|
346
348
|
const conditions = []
|
|
347
349
|
const queryParams = []
|
|
348
350
|
let paramIndex = 1
|
|
351
|
+
let searchRank = ''
|
|
349
352
|
|
|
350
353
|
if (query) {
|
|
351
354
|
let columns = []
|
|
@@ -376,15 +379,12 @@ ${generateReadGuardCall(tableName, 'req.query && req.query.rawQuery')}
|
|
|
376
379
|
}
|
|
377
380
|
if (__rowPolicy) columns = __brpVisibleColumns(__rowPolicy, columns)
|
|
378
381
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
(col) => '"' + sanitizeSearchIdentifier(col) + '"::text ILIKE ' + placeholder + " ESCAPE '|'"
|
|
386
|
-
)
|
|
387
|
-
conditions.push('(' + searchConditions.join(' OR ') + ')')
|
|
382
|
+
const search = buildSearchSql(query, columns, paramIndex)
|
|
383
|
+
if (search) {
|
|
384
|
+
conditions.push(search.where)
|
|
385
|
+
queryParams.push(...search.params)
|
|
386
|
+
paramIndex = search.nextIndex
|
|
387
|
+
searchRank = search.rank
|
|
388
388
|
}
|
|
389
389
|
}
|
|
390
390
|
|
|
@@ -401,8 +401,11 @@ ${generateReadGuardCall(tableName, 'req.query && req.query.rawQuery')}
|
|
|
401
401
|
// actually used (a translatable column in the request's language, the
|
|
402
402
|
// products table's price rewrites, rows without a value last), and a
|
|
403
403
|
// plain-column twin kept only as the fallback below.
|
|
404
|
+
// A search orders its matches by where the words are first; the sort
|
|
405
|
+
// orders the rows within each of those groups.
|
|
404
406
|
let orderBySql = ''
|
|
405
407
|
let plainOrderBySql = ''
|
|
408
|
+
const searchOrder = searchRank ? searchRank + ', ' : ''
|
|
406
409
|
if (sorts) {
|
|
407
410
|
const parsedSorts = safeJSONParse(sorts)
|
|
408
411
|
if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
|
|
@@ -416,16 +419,19 @@ ${generateReadGuardCall(tableName, 'req.query && req.query.rawQuery')}
|
|
|
416
419
|
// The row's own key last, so equal rows can't repeat across pages
|
|
417
420
|
// or vanish between them.
|
|
418
421
|
const tiebreak = rowOrderTiebreak(valid.map((sort) => sort.field), orderOf(valid[valid.length - 1]))
|
|
419
|
-
orderBySql = \` ORDER BY \${orderClauses.join(', ')}\${tiebreak}\`
|
|
420
|
-
plainOrderBySql = \` ORDER BY \${plainClauses.join(', ')}\${tiebreak}\`
|
|
422
|
+
orderBySql = \` ORDER BY \${searchOrder}\${orderClauses.join(', ')}\${tiebreak}\`
|
|
423
|
+
plainOrderBySql = \` ORDER BY \${searchOrder}\${plainClauses.join(', ')}\${tiebreak}\`
|
|
421
424
|
}
|
|
422
425
|
}
|
|
423
426
|
} else if (sortBy) {
|
|
424
427
|
assertRequestColumn(sortBy, 'sort field')
|
|
425
428
|
const sortByDirection = (sortOrder || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'
|
|
426
429
|
const tiebreak = rowOrderTiebreak([sortBy], sortByDirection)
|
|
427
|
-
orderBySql = \` ORDER BY \${localizedSortFieldSql(sortBy, requestLocale) || sortBy} \${sortByDirection}${SORT_NULLS_LAST_SQL}\${tiebreak}\`
|
|
428
|
-
plainOrderBySql = \` ORDER BY \${sortBy} \${sortByDirection}\${tiebreak}\`
|
|
430
|
+
orderBySql = \` ORDER BY \${searchOrder}\${localizedSortFieldSql(sortBy, requestLocale) || sortBy} \${sortByDirection}${SORT_NULLS_LAST_SQL}\${tiebreak}\`
|
|
431
|
+
plainOrderBySql = \` ORDER BY \${searchOrder}\${sortBy} \${sortByDirection}\${tiebreak}\`
|
|
432
|
+
}
|
|
433
|
+
if (!orderBySql && searchRank) {
|
|
434
|
+
orderBySql = plainOrderBySql = ' ORDER BY ' + searchRank + rowOrderTiebreak([], 'ASC')
|
|
429
435
|
}
|
|
430
436
|
const usedRewrittenSort = orderBySql !== plainOrderBySql
|
|
431
437
|
|
|
@@ -566,17 +572,11 @@ ${generateReadGuardCall(tableName, null)}
|
|
|
566
572
|
}
|
|
567
573
|
if (__rowPolicy) columns = __brpVisibleColumns(__rowPolicy, columns)
|
|
568
574
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
const searchConditions = columns
|
|
575
|
-
.map(
|
|
576
|
-
(col) => '"' + sanitizeSearchIdentifier(col) + '"::text ILIKE ' + placeholder + " ESCAPE '|'"
|
|
577
|
-
)
|
|
578
|
-
.join(' OR ')
|
|
579
|
-
conditions.push('(' + searchConditions + ')')
|
|
575
|
+
const search = buildSearchSql(query, columns, paramIndex)
|
|
576
|
+
if (search) {
|
|
577
|
+
conditions.push(search.where)
|
|
578
|
+
queryParams.push(...search.params)
|
|
579
|
+
paramIndex = search.nextIndex
|
|
580
580
|
}
|
|
581
581
|
}
|
|
582
582
|
|