@teleporthq/teleport-plugin-next-data-source 0.44.11 → 0.44.13
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__/browser-row-policy.test.ts +6 -6
- package/__tests__/fetchers.test.ts +1 -1
- package/__tests__/items-path.test.ts +3 -2
- package/__tests__/search-relevance.test.ts +407 -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/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 +4 -3
- 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 +5 -4
- 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 +81 -0
- package/dist/cjs/search-relevance.d.ts.map +1 -0
- package/dist/cjs/search-relevance.js +119 -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/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 +4 -3
- 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 +5 -4
- 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 +81 -0
- package/dist/esm/search-relevance.d.ts.map +1 -0
- package/dist/esm/search-relevance.js +113 -0
- package/dist/esm/search-relevance.js.map +1 -0
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/fetchers/csv-file.ts +17 -11
- package/src/fetchers/google-sheets.ts +15 -12
- package/src/fetchers/javascript.ts +12 -12
- package/src/fetchers/mariadb.ts +21 -26
- package/src/fetchers/mysql.ts +21 -26
- package/src/fetchers/postgresql.ts +31 -26
- package/src/fetchers/redshift.ts +18 -17
- package/src/fetchers/rest-api.ts +11 -11
- package/src/fetchers/supabase.ts +27 -20
- package/src/fetchers/teleport.ts +31 -26
- package/src/fetchers/turso.ts +18 -17
- package/src/search-relevance.ts +230 -0
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_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_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_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_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,8 @@ ${generateReadGuardCall(tableName, null)}
|
|
|
173
175
|
const conditions = []
|
|
174
176
|
const queryParams = []
|
|
175
177
|
let paramIndex = 1
|
|
178
|
+
let searchRank = ''
|
|
179
|
+
let searchFrom = ''
|
|
176
180
|
|
|
177
181
|
if (query) {
|
|
178
182
|
let columns = []
|
|
@@ -205,15 +209,13 @@ ${generateReadGuardCall(tableName, null)}
|
|
|
205
209
|
}
|
|
206
210
|
if (__rowPolicy) columns = __brpVisibleColumns(__rowPolicy, columns)
|
|
207
211
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
)
|
|
216
|
-
conditions.push('(' + searchConditions.join(' OR ') + ')')
|
|
212
|
+
const search = buildSearchSql(query, columns, paramIndex)
|
|
213
|
+
if (search) {
|
|
214
|
+
conditions.push(search.where)
|
|
215
|
+
queryParams.push(...search.params)
|
|
216
|
+
paramIndex = search.nextIndex
|
|
217
|
+
searchRank = search.rank
|
|
218
|
+
searchFrom = search.from
|
|
217
219
|
}
|
|
218
220
|
}
|
|
219
221
|
|
|
@@ -221,7 +223,8 @@ ${generateReadGuardCall(tableName, null)}
|
|
|
221
223
|
paramIndex = processFilters(filters, conditions, queryParams, paramIndex)
|
|
222
224
|
if (__rowPolicy) conditions.push('(' + __rowPolicy.predicate + ')')
|
|
223
225
|
|
|
224
|
-
|
|
226
|
+
// A search's worked-out texts stay out of the rows: only the table's own columns.
|
|
227
|
+
let sql = \`SELECT \${searchFrom ? '"${tableName}".*' : '*'} FROM "${tableName}"\${searchFrom}\`
|
|
225
228
|
|
|
226
229
|
if (conditions.length > 0) {
|
|
227
230
|
sql += \` WHERE \${conditions.join(' AND ')}\`
|
|
@@ -230,8 +233,11 @@ ${generateReadGuardCall(tableName, null)}
|
|
|
230
233
|
// Handle sorts - new array format. Two ORDER BY clauses are built: the one
|
|
231
234
|
// actually used (the products table's price rewrites, rows without a value
|
|
232
235
|
// last), and a plain-column twin kept only as the fallback below.
|
|
236
|
+
// A search orders its matches by where the words are first; the sort
|
|
237
|
+
// orders the rows within each of those groups.
|
|
233
238
|
let orderBySql = ''
|
|
234
239
|
let plainOrderBySql = ''
|
|
240
|
+
const searchOrder = searchRank ? searchRank + ', ' : ''
|
|
235
241
|
if (sorts) {
|
|
236
242
|
const parsedSorts = safeJSONParse(sorts)
|
|
237
243
|
if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
|
|
@@ -245,16 +251,19 @@ ${generateReadGuardCall(tableName, null)}
|
|
|
245
251
|
// The row's own key last, so equal rows can't repeat across pages
|
|
246
252
|
// or vanish between them.
|
|
247
253
|
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}\`
|
|
254
|
+
orderBySql = \` ORDER BY \${searchOrder}\${orderClauses.join(', ')}\${tiebreak}\`
|
|
255
|
+
plainOrderBySql = \` ORDER BY \${searchOrder}\${plainClauses.join(', ')}\${tiebreak}\`
|
|
250
256
|
}
|
|
251
257
|
}
|
|
252
258
|
} else if (sortBy) {
|
|
253
259
|
assertRequestColumn(sortBy, 'sort field')
|
|
254
260
|
const sortByDirection = (sortOrder || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'
|
|
255
261
|
const tiebreak = rowOrderTiebreak([sortBy], sortByDirection)
|
|
256
|
-
orderBySql = \` ORDER BY \${sortBy} \${sortByDirection}${SORT_NULLS_LAST_SQL}\${tiebreak}\`
|
|
257
|
-
plainOrderBySql = \` ORDER BY \${sortBy} \${sortByDirection}\${tiebreak}\`
|
|
262
|
+
orderBySql = \` ORDER BY \${searchOrder}\${sortBy} \${sortByDirection}${SORT_NULLS_LAST_SQL}\${tiebreak}\`
|
|
263
|
+
plainOrderBySql = \` ORDER BY \${searchOrder}\${sortBy} \${sortByDirection}\${tiebreak}\`
|
|
264
|
+
}
|
|
265
|
+
if (!orderBySql && searchRank) {
|
|
266
|
+
orderBySql = plainOrderBySql = ' ORDER BY ' + searchRank + rowOrderTiebreak([], 'ASC')
|
|
258
267
|
}
|
|
259
268
|
const usedRewrittenSort = orderBySql !== plainOrderBySql
|
|
260
269
|
|
|
@@ -346,6 +355,7 @@ ${generateReadGuardCall(tableName, null)}
|
|
|
346
355
|
const conditions = []
|
|
347
356
|
const queryParams = []
|
|
348
357
|
let paramIndex = 1
|
|
358
|
+
let searchFrom = ''
|
|
349
359
|
|
|
350
360
|
if (query) {
|
|
351
361
|
let columns = []
|
|
@@ -379,17 +389,12 @@ ${generateReadGuardCall(tableName, null)}
|
|
|
379
389
|
}
|
|
380
390
|
if (__rowPolicy) columns = __brpVisibleColumns(__rowPolicy, columns)
|
|
381
391
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
.map(
|
|
389
|
-
(col) => '"' + sanitizeSearchIdentifier(col) + '"::text ILIKE ' + placeholder + " ESCAPE '|'"
|
|
390
|
-
)
|
|
391
|
-
.join(' OR ')
|
|
392
|
-
conditions.push('(' + searchConditions + ')')
|
|
392
|
+
const search = buildSearchSql(query, columns, paramIndex)
|
|
393
|
+
if (search) {
|
|
394
|
+
conditions.push(search.where)
|
|
395
|
+
queryParams.push(...search.params)
|
|
396
|
+
paramIndex = search.nextIndex
|
|
397
|
+
searchFrom = search.from
|
|
393
398
|
}
|
|
394
399
|
}
|
|
395
400
|
|
|
@@ -397,7 +402,7 @@ ${generateReadGuardCall(tableName, null)}
|
|
|
397
402
|
paramIndex = processFilters(filters, conditions, queryParams, paramIndex)
|
|
398
403
|
if (__rowPolicy) conditions.push('(' + __rowPolicy.predicate + ')')
|
|
399
404
|
|
|
400
|
-
let countSql = \`SELECT COUNT(*) FROM "${tableName}"\`
|
|
405
|
+
let countSql = \`SELECT COUNT(*) FROM "${tableName}"\${searchFrom}\`
|
|
401
406
|
if (conditions.length > 0) {
|
|
402
407
|
countSql += \` WHERE \${conditions.join(' AND ')}\`
|
|
403
408
|
}
|
package/src/fetchers/redshift.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
generateFilterTreeHelpersCode,
|
|
6
6
|
generateSearchEscapeHelpersCode,
|
|
7
7
|
} from '../utils'
|
|
8
|
+
import { generateSearchSqlHelpersCode, REDSHIFT_SEARCH_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(REDSHIFT_SEARCH_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,27 +197,20 @@ export default async function handler(req, res) {
|
|
|
194
197
|
|
|
195
198
|
if (Array.isArray(data)) {
|
|
196
199
|
if (query && query.trim()) {
|
|
197
|
-
const searchTerms = query.toLowerCase().split(' ').filter(Boolean)
|
|
198
|
-
|
|
199
200
|
if (queryColumns) {
|
|
200
201
|
try {
|
|
201
202
|
const parsed = safeJSONParse(queryColumns)
|
|
202
203
|
const columns = Array.isArray(parsed) ? parsed : [parsed]
|
|
203
|
-
data = data.filter((item) =>
|
|
204
|
-
return searchTerms.every((term) => columns.some((col) => {
|
|
205
|
-
const value = getNestedValue(item, col)
|
|
206
|
-
if (value === null || value === undefined) return false
|
|
207
|
-
return String(value).toLowerCase().includes(term)
|
|
208
|
-
}))
|
|
209
|
-
})
|
|
204
|
+
data = data.filter((item) => matchesSearch(item, query, columns, getNestedValue))
|
|
210
205
|
} catch (err) {
|
|
211
206
|
console.error('Error parsing queryColumns:', err)
|
|
212
207
|
}
|
|
213
208
|
} else {
|
|
209
|
+
const terms = searchTerms(query)
|
|
214
210
|
data = data.filter((item) => {
|
|
215
211
|
try {
|
|
216
|
-
const
|
|
217
|
-
return
|
|
212
|
+
const text = searchText(item)
|
|
213
|
+
return terms.every((term) => text.indexOf(term) !== -1)
|
|
218
214
|
} catch {
|
|
219
215
|
return false
|
|
220
216
|
}
|
|
@@ -310,6 +306,10 @@ export default async function handler(req, res) {
|
|
|
310
306
|
})
|
|
311
307
|
}
|
|
312
308
|
|
|
309
|
+
if (query && queryColumns) {
|
|
310
|
+
data = rankBySearch(data, query, safeJSONParse(queryColumns), getNestedValue)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
313
|
const limitValue = limit || perPage
|
|
314
314
|
const pageValue = page ? Math.max(1, parseInt(page)) : undefined
|
|
315
315
|
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 { generateSearchTermsCode } 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
|
+
${generateSearchTermsCode(false)}
|
|
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
|
+
searchTerms(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
|
+
searchTerms(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_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_SQL)}
|
|
165
167
|
${generateArrayOverlapSqlCode()}
|
|
166
168
|
${getTransformationCode(tableName, transformOptions)}
|
|
167
169
|
${getTransformWrapperCode(tableName, transformOptions)}
|
|
@@ -346,6 +348,8 @@ ${generateReadGuardCall(tableName, 'req.query && req.query.rawQuery')}
|
|
|
346
348
|
const conditions = []
|
|
347
349
|
const queryParams = []
|
|
348
350
|
let paramIndex = 1
|
|
351
|
+
let searchRank = ''
|
|
352
|
+
let searchFrom = ''
|
|
349
353
|
|
|
350
354
|
if (query) {
|
|
351
355
|
let columns = []
|
|
@@ -376,22 +380,21 @@ ${generateReadGuardCall(tableName, 'req.query && req.query.rawQuery')}
|
|
|
376
380
|
}
|
|
377
381
|
if (__rowPolicy) columns = __brpVisibleColumns(__rowPolicy, columns)
|
|
378
382
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
)
|
|
387
|
-
conditions.push('(' + searchConditions.join(' OR ') + ')')
|
|
383
|
+
const search = buildSearchSql(query, columns, paramIndex)
|
|
384
|
+
if (search) {
|
|
385
|
+
conditions.push(search.where)
|
|
386
|
+
queryParams.push(...search.params)
|
|
387
|
+
paramIndex = search.nextIndex
|
|
388
|
+
searchRank = search.rank
|
|
389
|
+
searchFrom = search.from
|
|
388
390
|
}
|
|
389
391
|
}
|
|
390
392
|
|
|
391
393
|
paramIndex = processFilters(filters, conditions, queryParams, paramIndex)
|
|
392
394
|
if (__rowPolicy) conditions.push('(' + __rowPolicy.predicate + ')')
|
|
393
395
|
|
|
394
|
-
|
|
396
|
+
// A search's worked-out texts stay out of the rows: only the table's own columns.
|
|
397
|
+
let sql = \`SELECT \${searchFrom ? '${tableName}.*' : '*'} FROM ${tableName}\${searchFrom}\`
|
|
395
398
|
|
|
396
399
|
if (conditions.length > 0) {
|
|
397
400
|
sql += \` WHERE \${conditions.join(' AND ')}\`
|
|
@@ -401,8 +404,11 @@ ${generateReadGuardCall(tableName, 'req.query && req.query.rawQuery')}
|
|
|
401
404
|
// actually used (a translatable column in the request's language, the
|
|
402
405
|
// products table's price rewrites, rows without a value last), and a
|
|
403
406
|
// plain-column twin kept only as the fallback below.
|
|
407
|
+
// A search orders its matches by where the words are first; the sort
|
|
408
|
+
// orders the rows within each of those groups.
|
|
404
409
|
let orderBySql = ''
|
|
405
410
|
let plainOrderBySql = ''
|
|
411
|
+
const searchOrder = searchRank ? searchRank + ', ' : ''
|
|
406
412
|
if (sorts) {
|
|
407
413
|
const parsedSorts = safeJSONParse(sorts)
|
|
408
414
|
if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
|
|
@@ -416,16 +422,19 @@ ${generateReadGuardCall(tableName, 'req.query && req.query.rawQuery')}
|
|
|
416
422
|
// The row's own key last, so equal rows can't repeat across pages
|
|
417
423
|
// or vanish between them.
|
|
418
424
|
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}\`
|
|
425
|
+
orderBySql = \` ORDER BY \${searchOrder}\${orderClauses.join(', ')}\${tiebreak}\`
|
|
426
|
+
plainOrderBySql = \` ORDER BY \${searchOrder}\${plainClauses.join(', ')}\${tiebreak}\`
|
|
421
427
|
}
|
|
422
428
|
}
|
|
423
429
|
} else if (sortBy) {
|
|
424
430
|
assertRequestColumn(sortBy, 'sort field')
|
|
425
431
|
const sortByDirection = (sortOrder || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'
|
|
426
432
|
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}\`
|
|
433
|
+
orderBySql = \` ORDER BY \${searchOrder}\${localizedSortFieldSql(sortBy, requestLocale) || sortBy} \${sortByDirection}${SORT_NULLS_LAST_SQL}\${tiebreak}\`
|
|
434
|
+
plainOrderBySql = \` ORDER BY \${searchOrder}\${sortBy} \${sortByDirection}\${tiebreak}\`
|
|
435
|
+
}
|
|
436
|
+
if (!orderBySql && searchRank) {
|
|
437
|
+
orderBySql = plainOrderBySql = ' ORDER BY ' + searchRank + rowOrderTiebreak([], 'ASC')
|
|
429
438
|
}
|
|
430
439
|
const usedRewrittenSort = orderBySql !== plainOrderBySql
|
|
431
440
|
|
|
@@ -536,6 +545,7 @@ ${generateReadGuardCall(tableName, null)}
|
|
|
536
545
|
const conditions = []
|
|
537
546
|
const queryParams = []
|
|
538
547
|
let paramIndex = 1
|
|
548
|
+
let searchFrom = ''
|
|
539
549
|
|
|
540
550
|
if (query) {
|
|
541
551
|
let columns = []
|
|
@@ -566,24 +576,19 @@ ${generateReadGuardCall(tableName, null)}
|
|
|
566
576
|
}
|
|
567
577
|
if (__rowPolicy) columns = __brpVisibleColumns(__rowPolicy, columns)
|
|
568
578
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
.map(
|
|
576
|
-
(col) => '"' + sanitizeSearchIdentifier(col) + '"::text ILIKE ' + placeholder + " ESCAPE '|'"
|
|
577
|
-
)
|
|
578
|
-
.join(' OR ')
|
|
579
|
-
conditions.push('(' + searchConditions + ')')
|
|
579
|
+
const search = buildSearchSql(query, columns, paramIndex)
|
|
580
|
+
if (search) {
|
|
581
|
+
conditions.push(search.where)
|
|
582
|
+
queryParams.push(...search.params)
|
|
583
|
+
paramIndex = search.nextIndex
|
|
584
|
+
searchFrom = search.from
|
|
580
585
|
}
|
|
581
586
|
}
|
|
582
587
|
|
|
583
588
|
paramIndex = processFilters(filters, conditions, queryParams, paramIndex)
|
|
584
589
|
if (__rowPolicy) conditions.push('(' + __rowPolicy.predicate + ')')
|
|
585
590
|
|
|
586
|
-
let countSql = \`SELECT COUNT(*) FROM ${tableName}\`
|
|
591
|
+
let countSql = \`SELECT COUNT(*) FROM ${tableName}\${searchFrom}\`
|
|
587
592
|
if (conditions.length > 0) {
|
|
588
593
|
countSql += \` WHERE \${conditions.join(' AND ')}\`
|
|
589
594
|
}
|
package/src/fetchers/turso.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
generateFilterTreeHelpersCode,
|
|
6
6
|
generateSearchEscapeHelpersCode,
|
|
7
7
|
} from '../utils'
|
|
8
|
+
import { generateSearchSqlHelpersCode, SQLITE_SEARCH_SQL } from '../search-relevance'
|
|
8
9
|
|
|
9
10
|
export const validateTursoConfig = (
|
|
10
11
|
config: Record<string, unknown>
|
|
@@ -46,6 +47,7 @@ ${generateSafeJSONParseCode()}
|
|
|
46
47
|
${generateFilterTreeHelpersCode()}
|
|
47
48
|
|
|
48
49
|
${generateSearchEscapeHelpersCode()}
|
|
50
|
+
${generateSearchSqlHelpersCode(SQLITE_SEARCH_SQL)}
|
|
49
51
|
|
|
50
52
|
${generateDateFormatterCode()}
|
|
51
53
|
|
|
@@ -63,6 +65,7 @@ export default async function handler(req, res) {
|
|
|
63
65
|
const whereClauses = []
|
|
64
66
|
const queryParams = []
|
|
65
67
|
let searchQueryColumns = null
|
|
68
|
+
let search = null
|
|
66
69
|
|
|
67
70
|
if (query) {
|
|
68
71
|
if (queryColumns) {
|
|
@@ -70,15 +73,11 @@ export default async function handler(req, res) {
|
|
|
70
73
|
const columns = Array.isArray(parsed) ? parsed : [parsed]
|
|
71
74
|
// Cast columns to TEXT and LOWER both sides so the match is
|
|
72
75
|
// case-insensitive regardless of SQLite collation.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
whereClauses.push("(" + searchConditions.join(" OR ") + ")")
|
|
79
|
-
columns.forEach(() => {
|
|
80
|
-
queryParams.push(pattern)
|
|
81
|
-
})
|
|
76
|
+
search = buildSearchSql(query, columns)
|
|
77
|
+
if (search) {
|
|
78
|
+
whereClauses.push(search.where)
|
|
79
|
+
queryParams.push(...search.params)
|
|
80
|
+
}
|
|
82
81
|
} else {
|
|
83
82
|
// Store query for post-filtering if columns not specified
|
|
84
83
|
searchQueryColumns = query
|
|
@@ -135,23 +134,25 @@ export default async function handler(req, res) {
|
|
|
135
134
|
sql += \` WHERE \${whereClauses.join(' AND ')}\`
|
|
136
135
|
}
|
|
137
136
|
|
|
138
|
-
// Handle sorts - new array format
|
|
137
|
+
// Handle sorts - new array format. A search orders its matches by where
|
|
138
|
+
// the words are first; the sort orders the rows within each of those groups.
|
|
139
|
+
const orderClauses = search && search.rank ? [search.rank] : []
|
|
139
140
|
if (sorts) {
|
|
140
141
|
const parsedSorts = safeJSONParse(sorts)
|
|
141
142
|
if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
|
|
142
|
-
|
|
143
|
+
orderClauses.push(...parsedSorts.map((sort) => {
|
|
143
144
|
if (!sort.field) return null
|
|
144
145
|
const order = (sort.order || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'
|
|
145
146
|
return \`\${sanitizeIdentifier(sort.field)} \${order}\`
|
|
146
|
-
}).filter(Boolean)
|
|
147
|
-
|
|
148
|
-
if (orderClauses.length > 0) {
|
|
149
|
-
sql += \` ORDER BY \${orderClauses.join(', ')}\`
|
|
150
|
-
}
|
|
147
|
+
}).filter(Boolean))
|
|
151
148
|
}
|
|
152
149
|
} else if (sortBy) {
|
|
153
150
|
const sortOrderValue = (sortOrder || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'
|
|
154
|
-
|
|
151
|
+
orderClauses.push(\`\${sanitizeIdentifier(sortBy)} \${sortOrderValue}\`)
|
|
152
|
+
}
|
|
153
|
+
if (orderClauses.length > 0) {
|
|
154
|
+
sql += \` ORDER BY \${orderClauses.join(', ')}\`
|
|
155
|
+
if (search && search.rank) queryParams.push(...search.rankParams)
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
const limitValue = limit || perPage
|