@teleporthq/teleport-plugin-next-data-source 0.42.34 → 0.43.0

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 (239) hide show
  1. package/__tests__/ecommerce-product-out-of-stock.test.ts +112 -0
  2. package/__tests__/fetchers.test.ts +0 -42
  3. package/__tests__/filter-utils.test.ts +149 -0
  4. package/__tests__/mocks.ts +0 -12
  5. package/__tests__/utils.test.ts +0 -2
  6. package/dist/cjs/array-mapper-registry.d.ts +2 -0
  7. package/dist/cjs/array-mapper-registry.d.ts.map +1 -1
  8. package/dist/cjs/array-mapper-registry.js +9 -1
  9. package/dist/cjs/array-mapper-registry.js.map +1 -1
  10. package/dist/cjs/count-fetchers.d.ts +2 -2
  11. package/dist/cjs/count-fetchers.d.ts.map +1 -1
  12. package/dist/cjs/count-fetchers.js +5 -5
  13. package/dist/cjs/count-fetchers.js.map +1 -1
  14. package/dist/cjs/data-source-fetchers.d.ts +2 -1
  15. package/dist/cjs/data-source-fetchers.d.ts.map +1 -1
  16. package/dist/cjs/data-source-fetchers.js +11 -9
  17. package/dist/cjs/data-source-fetchers.js.map +1 -1
  18. package/dist/cjs/fetchers/airtable.d.ts.map +1 -1
  19. package/dist/cjs/fetchers/airtable.js +1 -1
  20. package/dist/cjs/fetchers/airtable.js.map +1 -1
  21. package/dist/cjs/fetchers/clickhouse.d.ts.map +1 -1
  22. package/dist/cjs/fetchers/clickhouse.js +1 -1
  23. package/dist/cjs/fetchers/clickhouse.js.map +1 -1
  24. package/dist/cjs/fetchers/csv-file.js +1 -1
  25. package/dist/cjs/fetchers/csv-file.js.map +1 -1
  26. package/dist/cjs/fetchers/firestore.js +1 -1
  27. package/dist/cjs/fetchers/firestore.js.map +1 -1
  28. package/dist/cjs/fetchers/google-sheets.js +1 -1
  29. package/dist/cjs/fetchers/google-sheets.js.map +1 -1
  30. package/dist/cjs/fetchers/index.d.ts +2 -1
  31. package/dist/cjs/fetchers/index.d.ts.map +1 -1
  32. package/dist/cjs/fetchers/index.js +8 -5
  33. package/dist/cjs/fetchers/index.js.map +1 -1
  34. package/dist/cjs/fetchers/javascript.js +1 -1
  35. package/dist/cjs/fetchers/javascript.js.map +1 -1
  36. package/dist/cjs/fetchers/mariadb.d.ts.map +1 -1
  37. package/dist/cjs/fetchers/mariadb.js +3 -3
  38. package/dist/cjs/fetchers/mariadb.js.map +1 -1
  39. package/dist/cjs/fetchers/mongodb.js +1 -1
  40. package/dist/cjs/fetchers/mongodb.js.map +1 -1
  41. package/dist/cjs/fetchers/mysql.d.ts.map +1 -1
  42. package/dist/cjs/fetchers/mysql.js +2 -2
  43. package/dist/cjs/fetchers/mysql.js.map +1 -1
  44. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  45. package/dist/cjs/fetchers/postgresql.js +2 -2
  46. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  47. package/dist/cjs/fetchers/raw-query.d.ts +18 -0
  48. package/dist/cjs/fetchers/raw-query.d.ts.map +1 -0
  49. package/dist/cjs/fetchers/raw-query.js +70 -0
  50. package/dist/cjs/fetchers/raw-query.js.map +1 -0
  51. package/dist/cjs/fetchers/redis.js +1 -1
  52. package/dist/cjs/fetchers/redis.js.map +1 -1
  53. package/dist/cjs/fetchers/redshift.d.ts.map +1 -1
  54. package/dist/cjs/fetchers/redshift.js +2 -2
  55. package/dist/cjs/fetchers/redshift.js.map +1 -1
  56. package/dist/cjs/fetchers/rest-api.js +1 -1
  57. package/dist/cjs/fetchers/rest-api.js.map +1 -1
  58. package/dist/cjs/fetchers/supabase.d.ts.map +1 -1
  59. package/dist/cjs/fetchers/supabase.js +62 -2
  60. package/dist/cjs/fetchers/supabase.js.map +1 -1
  61. package/dist/cjs/fetchers/teleport.d.ts +7 -0
  62. package/dist/cjs/fetchers/teleport.d.ts.map +1 -0
  63. package/dist/cjs/fetchers/teleport.js +63 -0
  64. package/dist/cjs/fetchers/teleport.js.map +1 -0
  65. package/dist/cjs/fetchers/turso.d.ts.map +1 -1
  66. package/dist/cjs/fetchers/turso.js +1 -1
  67. package/dist/cjs/fetchers/turso.js.map +1 -1
  68. package/dist/cjs/filter-utils.d.ts +13 -0
  69. package/dist/cjs/filter-utils.d.ts.map +1 -0
  70. package/dist/cjs/filter-utils.js +95 -0
  71. package/dist/cjs/filter-utils.js.map +1 -0
  72. package/dist/cjs/index.d.ts.map +1 -1
  73. package/dist/cjs/index.js +112 -9
  74. package/dist/cjs/index.js.map +1 -1
  75. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  76. package/dist/cjs/pagination-plugin.js +389 -128
  77. package/dist/cjs/pagination-plugin.js.map +1 -1
  78. package/dist/cjs/sort-utils.d.ts +10 -0
  79. package/dist/cjs/sort-utils.d.ts.map +1 -0
  80. package/dist/cjs/sort-utils.js +141 -0
  81. package/dist/cjs/sort-utils.js.map +1 -0
  82. package/dist/cjs/transformations/blog-post.d.ts +7 -0
  83. package/dist/cjs/transformations/blog-post.d.ts.map +1 -0
  84. package/dist/cjs/transformations/blog-post.js +13 -0
  85. package/dist/cjs/transformations/blog-post.js.map +1 -0
  86. package/dist/cjs/transformations/ecommerce-product.d.ts +7 -0
  87. package/dist/cjs/transformations/ecommerce-product.d.ts.map +1 -0
  88. package/dist/cjs/transformations/ecommerce-product.js +13 -0
  89. package/dist/cjs/transformations/ecommerce-product.js.map +1 -0
  90. package/dist/cjs/transformations/index.d.ts +26 -0
  91. package/dist/cjs/transformations/index.d.ts.map +1 -0
  92. package/dist/cjs/transformations/index.js +81 -0
  93. package/dist/cjs/transformations/index.js.map +1 -0
  94. package/dist/cjs/transformations/shared-utils.d.ts +7 -0
  95. package/dist/cjs/transformations/shared-utils.d.ts.map +1 -0
  96. package/dist/cjs/transformations/shared-utils.js +13 -0
  97. package/dist/cjs/transformations/shared-utils.js.map +1 -0
  98. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  99. package/dist/cjs/utils.d.ts +30 -1
  100. package/dist/cjs/utils.d.ts.map +1 -1
  101. package/dist/cjs/utils.js +173 -10
  102. package/dist/cjs/utils.js.map +1 -1
  103. package/dist/esm/array-mapper-registry.d.ts +2 -0
  104. package/dist/esm/array-mapper-registry.d.ts.map +1 -1
  105. package/dist/esm/array-mapper-registry.js +9 -1
  106. package/dist/esm/array-mapper-registry.js.map +1 -1
  107. package/dist/esm/count-fetchers.d.ts +2 -2
  108. package/dist/esm/count-fetchers.d.ts.map +1 -1
  109. package/dist/esm/count-fetchers.js +4 -4
  110. package/dist/esm/count-fetchers.js.map +1 -1
  111. package/dist/esm/data-source-fetchers.d.ts +2 -1
  112. package/dist/esm/data-source-fetchers.d.ts.map +1 -1
  113. package/dist/esm/data-source-fetchers.js +10 -9
  114. package/dist/esm/data-source-fetchers.js.map +1 -1
  115. package/dist/esm/fetchers/airtable.d.ts.map +1 -1
  116. package/dist/esm/fetchers/airtable.js +1 -1
  117. package/dist/esm/fetchers/airtable.js.map +1 -1
  118. package/dist/esm/fetchers/clickhouse.d.ts.map +1 -1
  119. package/dist/esm/fetchers/clickhouse.js +1 -1
  120. package/dist/esm/fetchers/clickhouse.js.map +1 -1
  121. package/dist/esm/fetchers/csv-file.js +1 -1
  122. package/dist/esm/fetchers/csv-file.js.map +1 -1
  123. package/dist/esm/fetchers/firestore.js +1 -1
  124. package/dist/esm/fetchers/firestore.js.map +1 -1
  125. package/dist/esm/fetchers/google-sheets.js +1 -1
  126. package/dist/esm/fetchers/google-sheets.js.map +1 -1
  127. package/dist/esm/fetchers/index.d.ts +2 -1
  128. package/dist/esm/fetchers/index.d.ts.map +1 -1
  129. package/dist/esm/fetchers/index.js +2 -1
  130. package/dist/esm/fetchers/index.js.map +1 -1
  131. package/dist/esm/fetchers/javascript.js +1 -1
  132. package/dist/esm/fetchers/javascript.js.map +1 -1
  133. package/dist/esm/fetchers/mariadb.d.ts.map +1 -1
  134. package/dist/esm/fetchers/mariadb.js +4 -4
  135. package/dist/esm/fetchers/mariadb.js.map +1 -1
  136. package/dist/esm/fetchers/mongodb.js +1 -1
  137. package/dist/esm/fetchers/mongodb.js.map +1 -1
  138. package/dist/esm/fetchers/mysql.d.ts.map +1 -1
  139. package/dist/esm/fetchers/mysql.js +3 -3
  140. package/dist/esm/fetchers/mysql.js.map +1 -1
  141. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  142. package/dist/esm/fetchers/postgresql.js +3 -3
  143. package/dist/esm/fetchers/postgresql.js.map +1 -1
  144. package/dist/esm/fetchers/raw-query.d.ts +18 -0
  145. package/dist/esm/fetchers/raw-query.d.ts.map +1 -0
  146. package/dist/esm/fetchers/raw-query.js +65 -0
  147. package/dist/esm/fetchers/raw-query.js.map +1 -0
  148. package/dist/esm/fetchers/redis.js +1 -1
  149. package/dist/esm/fetchers/redis.js.map +1 -1
  150. package/dist/esm/fetchers/redshift.d.ts.map +1 -1
  151. package/dist/esm/fetchers/redshift.js +3 -3
  152. package/dist/esm/fetchers/redshift.js.map +1 -1
  153. package/dist/esm/fetchers/rest-api.js +1 -1
  154. package/dist/esm/fetchers/rest-api.js.map +1 -1
  155. package/dist/esm/fetchers/supabase.d.ts.map +1 -1
  156. package/dist/esm/fetchers/supabase.js +63 -3
  157. package/dist/esm/fetchers/supabase.js.map +1 -1
  158. package/dist/esm/fetchers/teleport.d.ts +7 -0
  159. package/dist/esm/fetchers/teleport.d.ts.map +1 -0
  160. package/dist/esm/fetchers/teleport.js +57 -0
  161. package/dist/esm/fetchers/teleport.js.map +1 -0
  162. package/dist/esm/fetchers/turso.d.ts.map +1 -1
  163. package/dist/esm/fetchers/turso.js +2 -2
  164. package/dist/esm/fetchers/turso.js.map +1 -1
  165. package/dist/esm/filter-utils.d.ts +13 -0
  166. package/dist/esm/filter-utils.d.ts.map +1 -0
  167. package/dist/esm/filter-utils.js +66 -0
  168. package/dist/esm/filter-utils.js.map +1 -0
  169. package/dist/esm/index.d.ts.map +1 -1
  170. package/dist/esm/index.js +113 -10
  171. package/dist/esm/index.js.map +1 -1
  172. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  173. package/dist/esm/pagination-plugin.js +389 -128
  174. package/dist/esm/pagination-plugin.js.map +1 -1
  175. package/dist/esm/sort-utils.d.ts +10 -0
  176. package/dist/esm/sort-utils.d.ts.map +1 -0
  177. package/dist/esm/sort-utils.js +113 -0
  178. package/dist/esm/sort-utils.js.map +1 -0
  179. package/dist/esm/transformations/blog-post.d.ts +7 -0
  180. package/dist/esm/transformations/blog-post.d.ts.map +1 -0
  181. package/dist/esm/transformations/blog-post.js +9 -0
  182. package/dist/esm/transformations/blog-post.js.map +1 -0
  183. package/dist/esm/transformations/ecommerce-product.d.ts +7 -0
  184. package/dist/esm/transformations/ecommerce-product.d.ts.map +1 -0
  185. package/dist/esm/transformations/ecommerce-product.js +9 -0
  186. package/dist/esm/transformations/ecommerce-product.js.map +1 -0
  187. package/dist/esm/transformations/index.d.ts +26 -0
  188. package/dist/esm/transformations/index.d.ts.map +1 -0
  189. package/dist/esm/transformations/index.js +74 -0
  190. package/dist/esm/transformations/index.js.map +1 -0
  191. package/dist/esm/transformations/shared-utils.d.ts +7 -0
  192. package/dist/esm/transformations/shared-utils.d.ts.map +1 -0
  193. package/dist/esm/transformations/shared-utils.js +9 -0
  194. package/dist/esm/transformations/shared-utils.js.map +1 -0
  195. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  196. package/dist/esm/utils.d.ts +30 -1
  197. package/dist/esm/utils.d.ts.map +1 -1
  198. package/dist/esm/utils.js +170 -9
  199. package/dist/esm/utils.js.map +1 -1
  200. package/package.json +6 -5
  201. package/src/array-mapper-registry.ts +13 -0
  202. package/src/count-fetchers.ts +5 -5
  203. package/src/data-source-fetchers.ts +15 -11
  204. package/src/fetchers/airtable.ts +54 -8
  205. package/src/fetchers/clickhouse.ts +25 -19
  206. package/src/fetchers/csv-file.ts +2 -2
  207. package/src/fetchers/firestore.ts +2 -2
  208. package/src/fetchers/google-sheets.ts +2 -2
  209. package/src/fetchers/index.ts +6 -5
  210. package/src/fetchers/javascript.ts +2 -2
  211. package/src/fetchers/mariadb.ts +27 -12
  212. package/src/fetchers/mongodb.ts +2 -2
  213. package/src/fetchers/mysql.ts +27 -12
  214. package/src/fetchers/postgresql.ts +31 -18
  215. package/src/fetchers/raw-query.ts +178 -0
  216. package/src/fetchers/redis.ts +2 -2
  217. package/src/fetchers/redshift.ts +14 -10
  218. package/src/fetchers/rest-api.ts +2 -2
  219. package/src/fetchers/supabase.ts +97 -14
  220. package/src/fetchers/teleport.ts +485 -0
  221. package/src/fetchers/turso.ts +15 -7
  222. package/src/filter-utils.ts +111 -0
  223. package/src/index.ts +146 -6
  224. package/src/pagination-plugin.ts +547 -308
  225. package/src/sort-utils.ts +150 -0
  226. package/src/transformations/blog-post.ts +128 -0
  227. package/src/transformations/ecommerce-product.ts +173 -0
  228. package/src/transformations/index.ts +97 -0
  229. package/src/transformations/shared-utils.ts +271 -0
  230. package/src/utils.ts +227 -11
  231. package/dist/cjs/fetchers/static-collection.d.ts +0 -7
  232. package/dist/cjs/fetchers/static-collection.d.ts.map +0 -1
  233. package/dist/cjs/fetchers/static-collection.js +0 -25
  234. package/dist/cjs/fetchers/static-collection.js.map +0 -1
  235. package/dist/esm/fetchers/static-collection.d.ts +0 -7
  236. package/dist/esm/fetchers/static-collection.d.ts.map +0 -1
  237. package/dist/esm/fetchers/static-collection.js +0 -19
  238. package/dist/esm/fetchers/static-collection.js.map +0 -1
  239. package/src/fetchers/static-collection.ts +0 -231
@@ -0,0 +1,485 @@
1
+ import {
2
+ replaceSecretReference,
3
+ generateDateFormatterCode,
4
+ generateSafeJSONParseCode,
5
+ generateSearchEscapeHelpersCode,
6
+ } from '../utils'
7
+ import {
8
+ getTransformationCode,
9
+ getTransformExpression,
10
+ getTransformWrapperCode,
11
+ } from '../transformations'
12
+
13
+ interface TeleportDBConfig {
14
+ host?: string
15
+ port?: number | string
16
+ user?: string
17
+ username?: string
18
+ password?: string
19
+ database?: string
20
+ ssl?: boolean | { ca?: string; cert?: string; key?: string; rejectUnauthorized?: boolean }
21
+ sslConfig?: { ca?: string; cert?: string; key?: string; rejectUnauthorized?: boolean }
22
+ options?: { schema?: string }
23
+ }
24
+
25
+ const DEFAULT_ENV_KEYS = {
26
+ host: 'TELEPORT_DB_HOST',
27
+ port: 'TELEPORT_DB_PORT',
28
+ user: 'TELEPORT_DB_USER',
29
+ password: 'TELEPORT_DB_PASSWORD',
30
+ database: 'TELEPORT_DB_NAME',
31
+ ssl: 'TELEPORT_DB_SSL',
32
+ }
33
+
34
+ const resolveEnvReference = (value: unknown, defaultEnvKey: string): string => {
35
+ if (typeof value === 'string' && value.startsWith('teleporthq.secrets.')) {
36
+ return replaceSecretReference(value)
37
+ }
38
+ return `process.env.${defaultEnvKey}`
39
+ }
40
+
41
+ export const validateTeleportConfig = (
42
+ config: Record<string, unknown>
43
+ ): { isValid: boolean; error?: string } => {
44
+ if (!config || typeof config !== 'object') {
45
+ return { isValid: false, error: 'Config must be a valid object' }
46
+ }
47
+
48
+ return { isValid: true }
49
+ }
50
+
51
+ export const generateTeleportFetcher = (
52
+ config: Record<string, unknown>,
53
+ tableName: string
54
+ ): string => {
55
+ const dbConfig = config as TeleportDBConfig
56
+ const schema = dbConfig.options?.schema
57
+
58
+ const hostRef = resolveEnvReference(dbConfig.host, DEFAULT_ENV_KEYS.host)
59
+ const portRef = resolveEnvReference(dbConfig.port, DEFAULT_ENV_KEYS.port)
60
+ const userRef = resolveEnvReference(dbConfig.user || dbConfig.username, DEFAULT_ENV_KEYS.user)
61
+ const passwordRef = resolveEnvReference(dbConfig.password, DEFAULT_ENV_KEYS.password)
62
+ const databaseRef = resolveEnvReference(dbConfig.database, DEFAULT_ENV_KEYS.database)
63
+
64
+ const sslCode =
65
+ dbConfig.ssl === false
66
+ ? 'false'
67
+ : dbConfig.sslConfig
68
+ ? `{
69
+ ${
70
+ dbConfig.sslConfig.ca
71
+ ? `ca: ${resolveEnvReference(dbConfig.sslConfig.ca, 'TELEPORT_DB_SSL_CA')},`
72
+ : ''
73
+ }
74
+ ${
75
+ dbConfig.sslConfig.cert
76
+ ? `cert: ${resolveEnvReference(dbConfig.sslConfig.cert, 'TELEPORT_DB_SSL_CERT')},`
77
+ : ''
78
+ }
79
+ ${
80
+ dbConfig.sslConfig.key
81
+ ? `key: ${resolveEnvReference(dbConfig.sslConfig.key, 'TELEPORT_DB_SSL_KEY')},`
82
+ : ''
83
+ }
84
+ rejectUnauthorized: false
85
+ }`
86
+ : `(process.env.${DEFAULT_ENV_KEYS.ssl} === 'false' ? false : process.env.${DEFAULT_ENV_KEYS.ssl} === 'true' ? { rejectUnauthorized: false } : undefined)`
87
+
88
+ return `import { Client } from 'pg'
89
+
90
+ function normalizePostgresConnectionString(connectionString) {
91
+ if (!connectionString || typeof connectionString !== 'string') return connectionString;
92
+ if (/^postgresql:\\/(?!\\/)/i.test(connectionString)) {
93
+ return connectionString.replace(/^postgresql:\\//i, 'postgresql://');
94
+ }
95
+ return connectionString;
96
+ }
97
+
98
+ function stripSslQueryParamsFromConnectionString(connectionString) {
99
+ if (!connectionString || typeof connectionString !== 'string') return connectionString;
100
+ try {
101
+ var u = new URL(connectionString.replace(/^postgresql:/i, 'postgres:'));
102
+ u.searchParams.delete('sslmode');
103
+ u.searchParams.delete('ssl');
104
+ u.searchParams.delete('sslrootcert');
105
+ u.searchParams.delete('sslcert');
106
+ u.searchParams.delete('sslkey');
107
+ return u.toString().replace(/^postgres:/i, 'postgresql:');
108
+ } catch (e) {
109
+ return connectionString;
110
+ }
111
+ }
112
+
113
+ const getClient = () => {
114
+ var ssl = ${sslCode};
115
+ var connStr = process.env.TELEPORT_DB_CONNECTION_STRING;
116
+ if (connStr) {
117
+ connStr = normalizePostgresConnectionString(connStr);
118
+ }
119
+ if (ssl === false && connStr) {
120
+ connStr = stripSslQueryParamsFromConnectionString(connStr);
121
+ }
122
+ if (connStr) {
123
+ return new Client(Object.assign(
124
+ { connectionString: connStr },
125
+ ssl !== undefined ? { ssl: ssl } : {}
126
+ ));
127
+ }
128
+ return new Client(Object.assign(
129
+ {
130
+ host: ${hostRef},
131
+ port: parseInt(${portRef} || '5432', 10),
132
+ user: ${userRef},
133
+ password: ${passwordRef},
134
+ database: ${databaseRef},
135
+ },
136
+ ssl !== undefined ? { ssl: ssl } : {}
137
+ ));
138
+ }
139
+
140
+ ${generateSafeJSONParseCode()}
141
+
142
+ ${generateSearchEscapeHelpersCode()}
143
+ ${getTransformationCode(tableName)}
144
+ ${getTransformWrapperCode(tableName)}
145
+ const processFilters = (filters, conditions, queryParams, paramIndex) => {
146
+ if (!filters) return paramIndex
147
+
148
+ const parsedFilters = safeJSONParse(filters)
149
+
150
+ if (Array.isArray(parsedFilters)) {
151
+ parsedFilters.forEach((filter) => {
152
+ if (!filter.source || filter.destination === undefined) return
153
+
154
+ const field = filter.source
155
+ const value = filter.destination
156
+ const operand = filter.operand || '='
157
+
158
+ if (Array.isArray(value)) {
159
+ if (value.length === 0) return
160
+ const placeholders = value.map(() => \`$\${paramIndex++}\`)
161
+ queryParams.push(...value)
162
+ if (operand === '!=') {
163
+ conditions.push(\`\${field} NOT IN (\${placeholders.join(', ')})\`)
164
+ } else {
165
+ conditions.push(\`\${field} IN (\${placeholders.join(', ')})\`)
166
+ }
167
+ } else {
168
+ if (value === null) {
169
+ if (operand === '=') {
170
+ conditions.push(\`\${field} IS NULL\`)
171
+ } else if (operand === '!=') {
172
+ conditions.push(\`\${field} IS NOT NULL\`)
173
+ }
174
+ } else {
175
+ const validOps = ['=', '!=', '>', '<', '>=', '<=']
176
+ const sqlOperator = validOps.includes(operand) ? operand : '='
177
+ conditions.push(\`\${field} \${sqlOperator} $\${paramIndex}\`)
178
+ queryParams.push(value)
179
+ paramIndex++
180
+ }
181
+ }
182
+ })
183
+ } else {
184
+ Object.entries(parsedFilters).forEach(([key, value]) => {
185
+ if (Array.isArray(value)) {
186
+ const placeholders = value.map(() => \`$\${paramIndex++}\`)
187
+ queryParams.push(...value)
188
+ conditions.push(\`\${key} IN (\${placeholders.join(', ')})\`)
189
+ } else {
190
+ conditions.push(\`\${key} = $\${paramIndex}\`)
191
+ queryParams.push(value)
192
+ paramIndex++
193
+ }
194
+ })
195
+ }
196
+
197
+ return paramIndex
198
+ }
199
+
200
+ ${generateDateFormatterCode()}
201
+
202
+ // Matches DDL / dangerous statements the raw-query branch should refuse.
203
+ // Keep this list conservative — anything destructive or schema-changing is
204
+ // out of scope for a client-triggered fetch. SELECT and CTEs (WITH ...) are
205
+ // the only shapes consumers legitimately need.
206
+ const BLOCKED_RAW_QUERY_PATTERNS = [
207
+ /\\bcreate\\s+(?:temporary\\s+|temp\\s+|unlogged\\s+|global\\s+|local\\s+)?table\\b/i,
208
+ /\\bcreate\\s+(?:unique\\s+)?index\\b/i,
209
+ /\\bcreate\\s+(?:or\\s+replace\\s+)?(?:materialized\\s+)?view\\b/i,
210
+ /\\bcreate\\s+(?:or\\s+replace\\s+)?trigger\\b/i,
211
+ /\\bcreate\\s+(?:or\\s+replace\\s+)?(?:aggregate\\s+)?function\\b/i,
212
+ /\\bcreate\\s+(?:or\\s+replace\\s+)?procedure\\b/i,
213
+ /\\bcreate\\s+(?:database|schema|sequence|extension|type|role|user)\\b/i,
214
+ /\\bdrop\\s+(?:table|view|index|schema|database|sequence|trigger|function|procedure|role|user|extension|type|materialized)\\b/i,
215
+ /\\balter\\s+(?:table|view|index|schema|database|sequence|role|user|system)\\b/i,
216
+ /\\btruncate\\b/i,
217
+ /\\bgrant\\b/i,
218
+ /\\brevoke\\b/i,
219
+ /\\binsert\\b/i,
220
+ /\\bupdate\\b/i,
221
+ /\\bdelete\\b/i,
222
+ /\\bcopy\\b/i,
223
+ /\\bvacuum\\b/i,
224
+ /\\breindex\\b/i,
225
+ /\\bcluster\\b/i,
226
+ ]
227
+
228
+ function assertRawQuerySafe(rawQuery) {
229
+ if (typeof rawQuery !== 'string' || rawQuery.length === 0) {
230
+ throw new Error('rawQuery must be a non-empty string')
231
+ }
232
+ // Reject multi-statement payloads — only single SELECT / WITH statements.
233
+ // A trailing semicolon is tolerated but any content after it fails.
234
+ var trimmed = rawQuery.trim().replace(/;\\s*$/, '')
235
+ if (trimmed.indexOf(';') !== -1) {
236
+ throw new Error('rawQuery must contain exactly one statement')
237
+ }
238
+ for (var i = 0; i < BLOCKED_RAW_QUERY_PATTERNS.length; i++) {
239
+ if (BLOCKED_RAW_QUERY_PATTERNS[i].test(trimmed)) {
240
+ throw new Error('rawQuery contains a blocked statement')
241
+ }
242
+ }
243
+ }
244
+
245
+ export default async function handler(req, res) {
246
+ const client = getClient()
247
+
248
+ try {
249
+ await client.connect()
250
+ ${schema ? `await client.query('SET search_path TO ${schema}')` : ''}
251
+
252
+ // If the caller supplied a rawQuery, run it verbatim (after a safety
253
+ // guard). The schema-driven branch below builds \`SELECT * FROM ${tableName}\`
254
+ // with optional filters — it's the default read path and ignores
255
+ // rawQuery. Page-load workflows and other consumers that need a JOIN
256
+ // or a user-scoped filter pass their fully-rendered SQL here via
257
+ // \`fetchData({ rawQuery })\` and expect it to execute verbatim.
258
+ if (req.query && typeof req.query.rawQuery === 'string' && req.query.rawQuery.length > 0) {
259
+ assertRawQuerySafe(req.query.rawQuery)
260
+ const rawResult = await client.query(req.query.rawQuery)
261
+ const rawRows = Array.isArray(rawResult?.rows) ? rawResult.rows : []
262
+ const rawPlain = rawRows.map((row) =>
263
+ row && typeof row.toJSON === 'function' ? row.toJSON() : row
264
+ )
265
+ const rawSafe = JSON.parse(JSON.stringify(rawPlain, dateReplacer))
266
+ return res.status(200).json({
267
+ success: true,
268
+ data: rawSafe,
269
+ timestamp: Date.now()
270
+ })
271
+ }
272
+
273
+ const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset } = req.query
274
+
275
+ const conditions = []
276
+ const queryParams = []
277
+ let paramIndex = 1
278
+
279
+ if (query) {
280
+ let columns = []
281
+
282
+ if (queryColumns) {
283
+ const parsed = safeJSONParse(queryColumns)
284
+ columns = Array.isArray(parsed) ? parsed : (parsed ? [parsed] : [])
285
+ } else {
286
+ try {
287
+ const schemaQuery = \`
288
+ SELECT column_name
289
+ FROM information_schema.columns
290
+ WHERE table_name = $1
291
+ ${schema ? `AND table_schema = $2` : ''}
292
+ ORDER BY ordinal_position
293
+ \`
294
+ const schemaParams = ${
295
+ schema
296
+ ? `[${JSON.stringify(tableName)}, ${JSON.stringify(schema)}]`
297
+ : `[${JSON.stringify(tableName)}]`
298
+ }
299
+
300
+ const schemaResult = await client.query(schemaQuery, schemaParams)
301
+ columns = schemaResult.rows.map(row => row.column_name)
302
+ } catch (schemaError) {
303
+ console.warn('Failed to fetch column names from information_schema:', schemaError.message)
304
+ }
305
+ }
306
+
307
+ if (columns.length > 0) {
308
+ const pattern = '%' + escapeLikePattern(query) + '%'
309
+ const placeholder = '$' + paramIndex
310
+ paramIndex++
311
+ queryParams.push(pattern)
312
+ const searchConditions = columns.map(
313
+ (col) => '"' + sanitizeSearchIdentifier(col) + '"::text ILIKE ' + placeholder + " ESCAPE '|'"
314
+ )
315
+ conditions.push('(' + searchConditions.join(' OR ') + ')')
316
+ }
317
+ }
318
+
319
+ paramIndex = processFilters(filters, conditions, queryParams, paramIndex)
320
+
321
+ let sql = \`SELECT * FROM ${tableName}\`
322
+
323
+ if (conditions.length > 0) {
324
+ sql += \` WHERE \${conditions.join(' AND ')}\`
325
+ }
326
+
327
+ if (sorts) {
328
+ const parsedSorts = safeJSONParse(sorts)
329
+ if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
330
+ const orderClauses = parsedSorts.map((sort) => {
331
+ if (!sort.field) return null
332
+ const order = (sort.order || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'
333
+ return \`\${sort.field} \${order}\`
334
+ }).filter(Boolean)
335
+
336
+ if (orderClauses.length > 0) {
337
+ sql += \` ORDER BY \${orderClauses.join(', ')}\`
338
+ }
339
+ }
340
+ } else if (sortBy) {
341
+ sql += \` ORDER BY \${sortBy} \${(sortOrder || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'}\`
342
+ }
343
+
344
+ const limitValue = limit || perPage
345
+ const offsetValue = offset !== undefined ? parseInt(offset) : (page && perPage ? (parseInt(page) - 1) * parseInt(perPage) : undefined)
346
+
347
+ if (limitValue) {
348
+ sql += \` LIMIT \${limitValue}\`
349
+ }
350
+
351
+ if (offsetValue !== undefined) {
352
+ sql += \` OFFSET \${offsetValue}\`
353
+ }
354
+
355
+ const result = await client.query(sql, queryParams)
356
+ const rows = Array.isArray(result?.rows) ? result.rows : []
357
+ const plainRows = rows.map((row) =>
358
+ row && typeof row.toJSON === 'function' ? row.toJSON() : row
359
+ )
360
+ const safeData = JSON.parse(JSON.stringify(plainRows, dateReplacer))
361
+ ${
362
+ getTransformExpression(tableName)
363
+ ? `const transformedData = ${getTransformExpression(tableName)}`
364
+ : ''
365
+ }
366
+
367
+ return res.status(200).json({
368
+ success: true,
369
+ data: ${getTransformExpression(tableName) ? 'transformedData' : 'safeData'},
370
+ timestamp: Date.now()
371
+ })
372
+ } catch (error) {
373
+ console.error('Teleport DB fetch error:', error)
374
+ return res.status(500).json({
375
+ success: false,
376
+ error: error.message || 'Failed to fetch data',
377
+ timestamp: Date.now()
378
+ })
379
+ } finally {
380
+ if (client) {
381
+ try {
382
+ await client.end()
383
+ } catch (error) {
384
+ console.error('Error closing database client:', error)
385
+ }
386
+ }
387
+ }
388
+ }
389
+ `
390
+ }
391
+
392
+ export const generateTeleportCountFetcher = (
393
+ config: Record<string, unknown>,
394
+ tableName: string
395
+ ): string => {
396
+ const dbConfig = config as TeleportDBConfig
397
+ const hasSchema = !!dbConfig.options?.schema
398
+
399
+ return `
400
+ async function getCount(req, res) {
401
+ const client = getClient()
402
+
403
+ try {
404
+ await client.connect()
405
+ const { query, queryColumns, filters } = req.query
406
+ const conditions = []
407
+ const queryParams = []
408
+ let paramIndex = 1
409
+
410
+ if (query) {
411
+ let columns = []
412
+
413
+ if (queryColumns) {
414
+ const parsed = safeJSONParse(queryColumns)
415
+ columns = Array.isArray(parsed) ? parsed : [parsed]
416
+ } else {
417
+ try {
418
+ const schemaQuery = \`
419
+ SELECT column_name
420
+ FROM information_schema.columns
421
+ WHERE table_name = $1
422
+ ${hasSchema ? `AND table_schema = $2` : ''}
423
+ ORDER BY ordinal_position
424
+ \`
425
+ const schemaParams = ${
426
+ hasSchema
427
+ ? `[${JSON.stringify(tableName)}, ${JSON.stringify(dbConfig.options!.schema)}]`
428
+ : `[${JSON.stringify(tableName)}]`
429
+ }
430
+
431
+ const schemaResult = await client.query(schemaQuery, schemaParams)
432
+ columns = schemaResult.rows.map(row => row.column_name)
433
+ } catch (schemaError) {
434
+ console.warn('Failed to fetch column names from information_schema:', schemaError.message)
435
+ }
436
+ }
437
+
438
+ if (columns.length > 0) {
439
+ const pattern = '%' + escapeLikePattern(query) + '%'
440
+ const placeholder = '$' + paramIndex
441
+ paramIndex++
442
+ queryParams.push(pattern)
443
+ const searchConditions = columns
444
+ .map(
445
+ (col) => '"' + sanitizeSearchIdentifier(col) + '"::text ILIKE ' + placeholder + " ESCAPE '|'"
446
+ )
447
+ .join(' OR ')
448
+ conditions.push('(' + searchConditions + ')')
449
+ }
450
+ }
451
+
452
+ paramIndex = processFilters(filters, conditions, queryParams, paramIndex)
453
+
454
+ let countSql = \`SELECT COUNT(*) FROM ${tableName}\`
455
+ if (conditions.length > 0) {
456
+ countSql += \` WHERE \${conditions.join(' AND ')}\`
457
+ }
458
+
459
+ const result = await client.query(countSql, queryParams)
460
+ const count = parseInt(result.rows[0].count, 10)
461
+
462
+ return res.status(200).json({
463
+ success: true,
464
+ count: count,
465
+ timestamp: Date.now()
466
+ })
467
+ } catch (error) {
468
+ console.error('Error getting count:', error)
469
+ return res.status(500).json({
470
+ success: false,
471
+ error: error.message || 'Failed to get count',
472
+ timestamp: Date.now()
473
+ })
474
+ } finally {
475
+ if (client) {
476
+ try {
477
+ await client.end()
478
+ } catch (error) {
479
+ console.error('Error closing database client:', error)
480
+ }
481
+ }
482
+ }
483
+ }
484
+ `
485
+ }
@@ -2,6 +2,7 @@ import {
2
2
  replaceSecretReference,
3
3
  generateDateFormatterCode,
4
4
  generateSafeJSONParseCode,
5
+ generateSearchEscapeHelpersCode,
5
6
  } from '../utils'
6
7
 
7
8
  export const validateTursoConfig = (
@@ -41,6 +42,8 @@ export const generateTursoFetcher = (
41
42
 
42
43
  ${generateSafeJSONParseCode()}
43
44
 
45
+ ${generateSearchEscapeHelpersCode()}
46
+
44
47
  ${generateDateFormatterCode()}
45
48
 
46
49
  export default async function handler(req, res) {
@@ -62,11 +65,16 @@ export default async function handler(req, res) {
62
65
  if (queryColumns) {
63
66
  const parsed = safeJSONParse(queryColumns)
64
67
  const columns = Array.isArray(parsed) ? parsed : [parsed]
65
- // Cast columns to TEXT to support searching on non-text columns (dates, numbers, etc.)
66
- const searchConditions = columns.map((col) => \`CAST(\${col} AS TEXT) LIKE ?\`)
67
- whereClauses.push(\`(\${searchConditions.join(' OR ')})\`)
68
+ // Cast columns to TEXT and LOWER both sides so the match is
69
+ // case-insensitive regardless of SQLite collation.
70
+ const pattern = "%" + escapeLikePattern(query) + "%"
71
+ const searchConditions = columns.map(
72
+ (col) =>
73
+ 'LOWER(CAST("' + sanitizeSearchIdentifier(col) + '" AS TEXT)) LIKE LOWER(?) ESCAPE ' + "'|'"
74
+ )
75
+ whereClauses.push("(" + searchConditions.join(" OR ") + ")")
68
76
  columns.forEach(() => {
69
- queryParams.push(\`%\${query}%\`)
77
+ queryParams.push(pattern)
70
78
  })
71
79
  } else {
72
80
  // Store query for post-filtering if columns not specified
@@ -143,16 +151,16 @@ export default async function handler(req, res) {
143
151
  if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
144
152
  const orderClauses = parsedSorts.map((sort) => {
145
153
  if (!sort.field) return null
146
- const order = sort.order?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC'
154
+ const order = (sort.order || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'
147
155
  return \`\${sanitizeIdentifier(sort.field)} \${order}\`
148
156
  }).filter(Boolean)
149
-
157
+
150
158
  if (orderClauses.length > 0) {
151
159
  sql += \` ORDER BY \${orderClauses.join(', ')}\`
152
160
  }
153
161
  }
154
162
  } else if (sortBy) {
155
- const sortOrderValue = sortOrder?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC'
163
+ const sortOrderValue = (sortOrder || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'
156
164
  sql += \` ORDER BY \${sanitizeIdentifier(sortBy)} \${sortOrderValue}\`
157
165
  }
158
166
 
@@ -0,0 +1,111 @@
1
+ import * as types from '@babel/types'
2
+
3
+ // Build a single filter entry: { source: '<src>', destination: <destExpr>, operand: '<op>' }
4
+ const buildFilterEntry = (
5
+ source: string,
6
+ destinationExpr: types.Expression,
7
+ operand: string
8
+ ): types.ObjectExpression =>
9
+ types.objectExpression([
10
+ types.objectProperty(types.identifier('source'), types.stringLiteral(source)),
11
+ types.objectProperty(types.identifier('destination'), destinationExpr),
12
+ types.objectProperty(types.identifier('operand'), types.stringLiteral(operand)),
13
+ ])
14
+
15
+ // Predicate `(__f) => __f.destination !== '' && __f.destination !== null && __f.destination !== undefined`
16
+ // Used at runtime to drop filter conditions whose destination resolves to an
17
+ // empty value. Keeps the page generic: state-bound filters (e.g.
18
+ // `selectedCategory === ''` after picking "All Categories") and url-bound
19
+ // filters (e.g. no `?categoryFilter=` in URL → `router?.query?.categoryFilter`
20
+ // is `undefined`) both fall away cleanly instead of being sent to the API as
21
+ // `destination: ''`, which the SQL backend interprets as a literal empty
22
+ // string and returns zero rows.
23
+ const buildNonEmptyDestinationPredicate = (): types.ArrowFunctionExpression => {
24
+ const f = types.identifier('__f')
25
+ const dest = types.memberExpression(f, types.identifier('destination'))
26
+ return types.arrowFunctionExpression(
27
+ [f],
28
+ types.logicalExpression(
29
+ '&&',
30
+ types.logicalExpression(
31
+ '&&',
32
+ types.binaryExpression('!==', dest, types.stringLiteral('')),
33
+ types.binaryExpression('!==', dest, types.nullLiteral())
34
+ ),
35
+ types.binaryExpression('!==', dest, types.identifier('undefined'))
36
+ )
37
+ )
38
+ }
39
+
40
+ // Build the AST for:
41
+ // JSON.stringify(
42
+ // [ {source, destination, operand}, ... ]
43
+ // .filter(__f => __f.destination !== '' && __f.destination !== null && __f.destination !== undefined)
44
+ // )
45
+ //
46
+ // The `.filter(...)` step is unconditional even when every entry is a static
47
+ // (always-truthy) destination — the cost is negligible at runtime and it
48
+ // keeps the generated code uniform, so future regressions where a new filter
49
+ // type is added but its empty-value behavior is forgotten don't silently
50
+ // re-introduce empty-string filters.
51
+ export const buildFiltersStringifyCall = (
52
+ filters: Array<{ source?: string; operand?: string; destination: unknown }>,
53
+ buildDestinationExpression: (destination: unknown) => types.Expression
54
+ ): types.CallExpression => {
55
+ const entries = filters.map((filter) =>
56
+ buildFilterEntry(
57
+ filter.source || '',
58
+ buildDestinationExpression(filter.destination),
59
+ filter.operand || ''
60
+ )
61
+ )
62
+
63
+ const filteredArray = types.callExpression(
64
+ types.memberExpression(types.arrayExpression(entries), types.identifier('filter')),
65
+ [buildNonEmptyDestinationPredicate()]
66
+ )
67
+
68
+ return types.callExpression(
69
+ types.memberExpression(types.identifier('JSON'), types.identifier('stringify')),
70
+ [filteredArray]
71
+ )
72
+ }
73
+
74
+ // Convenience: push `filters: <call>` onto a paramsProps array.
75
+ export const appendFiltersParam = (
76
+ paramsProps: types.ObjectProperty[],
77
+ filters: Array<{ source?: string; operand?: string; destination: unknown }> | undefined,
78
+ buildDestinationExpression: (destination: unknown) => types.Expression
79
+ ): void => {
80
+ if (!filters || filters.length === 0) {
81
+ return
82
+ }
83
+ paramsProps.push(
84
+ types.objectProperty(
85
+ types.identifier('filters'),
86
+ buildFiltersStringifyCall(filters, buildDestinationExpression)
87
+ )
88
+ )
89
+ }
90
+
91
+ // Appends each id in `stateIds` as a bare `Identifier` onto `deps`, skipping
92
+ // any id already tracked in `seen`. Mutates both `deps` and `seen`.
93
+ //
94
+ // Used by every count-fetch and params-`useMemo` builder in the pagination
95
+ // plugin to wire state-bound filter destinations into React's deps array.
96
+ // Centralising the loop here means a new dep-source (sort, future filter
97
+ // shapes) can hook into the same dedup-by-name set without re-implementing
98
+ // it at four call sites.
99
+ export const pushStateIdsAsDeps = (
100
+ deps: types.Expression[],
101
+ seen: Set<string>,
102
+ stateIds: string[]
103
+ ): void => {
104
+ for (const id of stateIds) {
105
+ if (seen.has(id)) {
106
+ continue
107
+ }
108
+ seen.add(id)
109
+ deps.push(types.identifier(id))
110
+ }
111
+ }