@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/turso.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
generateFilterTreeHelpersCode,
|
|
6
6
|
generateSearchEscapeHelpersCode,
|
|
7
7
|
} from '../utils'
|
|
8
|
+
import { generateSearchSqlHelpersCode, SQLITE_SEARCH_MATCH_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_MATCH_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
|
|
@@ -185,11 +186,11 @@ export default async function handler(req, res) {
|
|
|
185
186
|
|
|
186
187
|
// Apply post-filtering for search without queryColumns
|
|
187
188
|
if (searchQueryColumns) {
|
|
188
|
-
const
|
|
189
|
+
const searchTerms = searchQueryColumns.toLowerCase().split(' ').filter(Boolean)
|
|
189
190
|
data = data.filter((item) => {
|
|
190
191
|
try {
|
|
191
192
|
const stringified = JSON.stringify(item).toLowerCase()
|
|
192
|
-
return stringified.includes(
|
|
193
|
+
return searchTerms.every((term) => stringified.includes(term))
|
|
193
194
|
} catch {
|
|
194
195
|
return false
|
|
195
196
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a list's search box narrows and orders the rows of a generated route.
|
|
3
|
+
*
|
|
4
|
+
* Every typed word must appear in one of the searched columns, so "create
|
|
5
|
+
* form" finds "How to create a form". The matches are then ordered by where
|
|
6
|
+
* the words are: a row ranks by the first searched column, in the order the
|
|
7
|
+
* list searches them, that holds every word (a title match before a summary
|
|
8
|
+
* match before a body match), and a row whose words are spread over several
|
|
9
|
+
* columns comes after all of those. The list's own sort orders the rows
|
|
10
|
+
* within each of those groups.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** `searchWords(query)`: the typed words, lowercased, each once. */
|
|
14
|
+
export const generateSearchWordsCode = (): string => `const searchWords = (query) => {
|
|
15
|
+
const words = []
|
|
16
|
+
String(query == null ? '' : query).toLowerCase().split(/\\s+/).forEach((word) => {
|
|
17
|
+
if (word && words.indexOf(word) === -1) words.push(word)
|
|
18
|
+
})
|
|
19
|
+
return words
|
|
20
|
+
}`
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* `buildSearchSql(query, columns, firstIndex)` for the SQL routes:
|
|
24
|
+
* `{ where, params, rank, rankParams, nextIndex }`, or null when there is
|
|
25
|
+
* nothing to search. `matchSql` is the dialect's case-insensitive substring
|
|
26
|
+
* test, `(column, placeholder) => sql`. With `firstIndex` the placeholders are
|
|
27
|
+
* numbered (`$n`) and each word's pattern is bound once, the rank reusing it;
|
|
28
|
+
* without it they are positional (`?`), every occurrence binds its own copy,
|
|
29
|
+
* and `rankParams` go after the parameters of the whole WHERE, as the ORDER BY
|
|
30
|
+
* comes after it. `rank` is '' with fewer than two columns.
|
|
31
|
+
*
|
|
32
|
+
* Uses `escapeLikePattern` and `sanitizeSearchIdentifier`
|
|
33
|
+
* (`generateSearchEscapeHelpersCode`).
|
|
34
|
+
*/
|
|
35
|
+
export const generateSearchSqlHelpersCode = (
|
|
36
|
+
matchSql: string
|
|
37
|
+
): string => `${generateSearchWordsCode()}
|
|
38
|
+
const searchMatchSql = ${matchSql}
|
|
39
|
+
const buildSearchSql = (query, columns, firstIndex) => {
|
|
40
|
+
const words = searchWords(query)
|
|
41
|
+
const searched = []
|
|
42
|
+
;(Array.isArray(columns) ? columns : []).forEach((column) => {
|
|
43
|
+
const name = sanitizeSearchIdentifier(column)
|
|
44
|
+
if (searched.indexOf(name) === -1) searched.push(name)
|
|
45
|
+
})
|
|
46
|
+
if (words.length === 0 || searched.length === 0) return null
|
|
47
|
+
const patterns = words.map((word) => '%' + escapeLikePattern(word) + '%')
|
|
48
|
+
const numbered = typeof firstIndex === 'number'
|
|
49
|
+
const params = numbered ? patterns.slice() : []
|
|
50
|
+
const rankParams = []
|
|
51
|
+
const placeholder = (wordIndex, bound) => {
|
|
52
|
+
if (numbered) return '$' + (firstIndex + wordIndex)
|
|
53
|
+
bound.push(patterns[wordIndex])
|
|
54
|
+
return '?'
|
|
55
|
+
}
|
|
56
|
+
const where = '(' + patterns.map((pattern, wordIndex) =>
|
|
57
|
+
'(' + searched.map((column) => searchMatchSql(column, placeholder(wordIndex, params))).join(' OR ') + ')'
|
|
58
|
+
).join(' AND ') + ')'
|
|
59
|
+
const rank = searched.length < 2 ? '' : 'CASE ' + searched.map((column, position) =>
|
|
60
|
+
'WHEN ' + patterns.map((pattern, wordIndex) => searchMatchSql(column, placeholder(wordIndex, rankParams))).join(' AND ') + ' THEN ' + position
|
|
61
|
+
).join(' ') + ' ELSE ' + searched.length + ' END'
|
|
62
|
+
return { where, params, rank, rankParams, nextIndex: numbered ? firstIndex + patterns.length : firstIndex }
|
|
63
|
+
}`
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* `rankBySearch(rows, query, columns, valueOf)` for the routes that search
|
|
67
|
+
* rows in memory: the same order as the SQL routes' rank, applied after the
|
|
68
|
+
* list's sort (a stable sort keeps it within each group) and before the page
|
|
69
|
+
* is cut.
|
|
70
|
+
*/
|
|
71
|
+
export const generateSearchRankHelperCode = (): string => `${generateSearchWordsCode()}
|
|
72
|
+
const rankBySearch = (rows, query, columns, valueOf) => {
|
|
73
|
+
const words = searchWords(query)
|
|
74
|
+
if (!Array.isArray(columns) || columns.length < 2 || words.length === 0) return rows
|
|
75
|
+
const tierOf = (row) => {
|
|
76
|
+
for (let position = 0; position < columns.length; position++) {
|
|
77
|
+
const value = valueOf(row, columns[position])
|
|
78
|
+
const text = value === null || value === undefined ? '' : String(value).toLowerCase()
|
|
79
|
+
if (words.every((word) => text.includes(word))) return position
|
|
80
|
+
}
|
|
81
|
+
return columns.length
|
|
82
|
+
}
|
|
83
|
+
return rows
|
|
84
|
+
.map((row, index) => ({ row, index, tier: tierOf(row) }))
|
|
85
|
+
.sort((a, b) => a.tier - b.tier || a.index - b.index)
|
|
86
|
+
.map((entry) => entry.row)
|
|
87
|
+
}`
|
|
88
|
+
|
|
89
|
+
/** Each dialect's case-insensitive substring match, for `generateSearchSqlHelpersCode`. */
|
|
90
|
+
export const PG_SEARCH_MATCH_SQL = `(column, placeholder) => '"' + column + '"::text ILIKE ' + placeholder + " ESCAPE '|'"`
|
|
91
|
+
|
|
92
|
+
export const MYSQL_SEARCH_MATCH_SQL = `(column, placeholder) => 'LOWER(CAST(' + mysql.escapeId(column) + ' AS CHAR)) LIKE LOWER(' + placeholder + ") ESCAPE '|'"`
|
|
93
|
+
|
|
94
|
+
export const MARIADB_SEARCH_MATCH_SQL = `(column, placeholder) => 'LOWER(CAST(\`' + column + '\` AS CHAR)) LIKE LOWER(' + placeholder + ") ESCAPE '|'"`
|
|
95
|
+
|
|
96
|
+
export const SQLITE_SEARCH_MATCH_SQL = `(column, placeholder) => 'LOWER(CAST("' + column + '" AS TEXT)) LIKE LOWER(' + placeholder + ") ESCAPE '|'"`
|