@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.
Files changed (96) hide show
  1. package/__tests__/items-path.test.ts +3 -2
  2. package/__tests__/search-relevance.test.ts +260 -0
  3. package/dist/cjs/fetchers/csv-file.d.ts.map +1 -1
  4. package/dist/cjs/fetchers/csv-file.js +4 -3
  5. package/dist/cjs/fetchers/csv-file.js.map +1 -1
  6. package/dist/cjs/fetchers/firestore.js +1 -1
  7. package/dist/cjs/fetchers/firestore.js.map +1 -1
  8. package/dist/cjs/fetchers/google-sheets.d.ts.map +1 -1
  9. package/dist/cjs/fetchers/google-sheets.js +2 -1
  10. package/dist/cjs/fetchers/google-sheets.js.map +1 -1
  11. package/dist/cjs/fetchers/javascript.d.ts.map +1 -1
  12. package/dist/cjs/fetchers/javascript.js +2 -1
  13. package/dist/cjs/fetchers/javascript.js.map +1 -1
  14. package/dist/cjs/fetchers/mariadb.d.ts.map +1 -1
  15. package/dist/cjs/fetchers/mariadb.js +4 -3
  16. package/dist/cjs/fetchers/mariadb.js.map +1 -1
  17. package/dist/cjs/fetchers/mysql.d.ts.map +1 -1
  18. package/dist/cjs/fetchers/mysql.js +3 -2
  19. package/dist/cjs/fetchers/mysql.js.map +1 -1
  20. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  21. package/dist/cjs/fetchers/postgresql.js +3 -2
  22. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  23. package/dist/cjs/fetchers/redshift.d.ts.map +1 -1
  24. package/dist/cjs/fetchers/redshift.js +3 -2
  25. package/dist/cjs/fetchers/redshift.js.map +1 -1
  26. package/dist/cjs/fetchers/rest-api.d.ts.map +1 -1
  27. package/dist/cjs/fetchers/rest-api.js +3 -2
  28. package/dist/cjs/fetchers/rest-api.js.map +1 -1
  29. package/dist/cjs/fetchers/supabase.d.ts.map +1 -1
  30. package/dist/cjs/fetchers/supabase.js +3 -2
  31. package/dist/cjs/fetchers/supabase.js.map +1 -1
  32. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  33. package/dist/cjs/fetchers/teleport.js +4 -3
  34. package/dist/cjs/fetchers/teleport.js.map +1 -1
  35. package/dist/cjs/fetchers/turso.d.ts.map +1 -1
  36. package/dist/cjs/fetchers/turso.js +2 -1
  37. package/dist/cjs/fetchers/turso.js.map +1 -1
  38. package/dist/cjs/search-relevance.d.ts +40 -0
  39. package/dist/cjs/search-relevance.d.ts.map +1 -0
  40. package/dist/cjs/search-relevance.js +46 -0
  41. package/dist/cjs/search-relevance.js.map +1 -0
  42. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  43. package/dist/esm/fetchers/csv-file.d.ts.map +1 -1
  44. package/dist/esm/fetchers/csv-file.js +4 -3
  45. package/dist/esm/fetchers/csv-file.js.map +1 -1
  46. package/dist/esm/fetchers/firestore.js +1 -1
  47. package/dist/esm/fetchers/firestore.js.map +1 -1
  48. package/dist/esm/fetchers/google-sheets.d.ts.map +1 -1
  49. package/dist/esm/fetchers/google-sheets.js +2 -1
  50. package/dist/esm/fetchers/google-sheets.js.map +1 -1
  51. package/dist/esm/fetchers/javascript.d.ts.map +1 -1
  52. package/dist/esm/fetchers/javascript.js +2 -1
  53. package/dist/esm/fetchers/javascript.js.map +1 -1
  54. package/dist/esm/fetchers/mariadb.d.ts.map +1 -1
  55. package/dist/esm/fetchers/mariadb.js +4 -3
  56. package/dist/esm/fetchers/mariadb.js.map +1 -1
  57. package/dist/esm/fetchers/mysql.d.ts.map +1 -1
  58. package/dist/esm/fetchers/mysql.js +3 -2
  59. package/dist/esm/fetchers/mysql.js.map +1 -1
  60. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  61. package/dist/esm/fetchers/postgresql.js +3 -2
  62. package/dist/esm/fetchers/postgresql.js.map +1 -1
  63. package/dist/esm/fetchers/redshift.d.ts.map +1 -1
  64. package/dist/esm/fetchers/redshift.js +3 -2
  65. package/dist/esm/fetchers/redshift.js.map +1 -1
  66. package/dist/esm/fetchers/rest-api.d.ts.map +1 -1
  67. package/dist/esm/fetchers/rest-api.js +3 -2
  68. package/dist/esm/fetchers/rest-api.js.map +1 -1
  69. package/dist/esm/fetchers/supabase.d.ts.map +1 -1
  70. package/dist/esm/fetchers/supabase.js +3 -2
  71. package/dist/esm/fetchers/supabase.js.map +1 -1
  72. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  73. package/dist/esm/fetchers/teleport.js +4 -3
  74. package/dist/esm/fetchers/teleport.js.map +1 -1
  75. package/dist/esm/fetchers/turso.d.ts.map +1 -1
  76. package/dist/esm/fetchers/turso.js +2 -1
  77. package/dist/esm/fetchers/turso.js.map +1 -1
  78. package/dist/esm/search-relevance.d.ts +40 -0
  79. package/dist/esm/search-relevance.d.ts.map +1 -0
  80. package/dist/esm/search-relevance.js +40 -0
  81. package/dist/esm/search-relevance.js.map +1 -0
  82. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  83. package/package.json +5 -5
  84. package/src/fetchers/csv-file.ts +16 -5
  85. package/src/fetchers/firestore.ts +2 -2
  86. package/src/fetchers/google-sheets.ts +14 -5
  87. package/src/fetchers/javascript.ts +12 -5
  88. package/src/fetchers/mariadb.ts +21 -26
  89. package/src/fetchers/mysql.ts +21 -26
  90. package/src/fetchers/postgresql.ts +24 -24
  91. package/src/fetchers/redshift.ts +18 -17
  92. package/src/fetchers/rest-api.ts +12 -5
  93. package/src/fetchers/supabase.ts +27 -20
  94. package/src/fetchers/teleport.ts +24 -24
  95. package/src/fetchers/turso.ts +20 -19
  96. package/src/search-relevance.ts +96 -0
@@ -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
- const pattern = "%" + escapeLikePattern(query) + "%"
74
- const searchConditions = columns.map(
75
- (col) =>
76
- 'LOWER(CAST("' + sanitizeSearchIdentifier(col) + '" AS TEXT)) LIKE LOWER(?) ESCAPE ' + "'|'"
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
- const orderClauses = parsedSorts.map((sort) => {
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
- sql += \` ORDER BY \${sanitizeIdentifier(sortBy)} \${sortOrderValue}\`
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 searchQuery = searchQueryColumns.toLowerCase()
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(searchQuery)
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 '|'"`