@teleporthq/teleport-plugin-next-data-source 0.42.35 → 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,178 @@
1
+ import { replaceSecretReference } from '../utils'
2
+
3
+ interface PostgreSQLConfig {
4
+ host?: string
5
+ port?: number
6
+ user?: string
7
+ username?: string
8
+ password?: string
9
+ database?: string
10
+ ssl?: boolean | { ca?: string; cert?: string; key?: string; rejectUnauthorized?: boolean }
11
+ sslConfig?: { ca?: string; cert?: string; key?: string; rejectUnauthorized?: boolean }
12
+ }
13
+
14
+ const FORBIDDEN_KEYWORDS = ['CREATE', 'ALTER', 'DROP', 'TRUNCATE', 'RENAME', 'GRANT', 'REVOKE']
15
+
16
+ /**
17
+ * Generates an API route handler that executes a parameterized raw SQL query.
18
+ * Supports {{Current User.*}} substitution via query params.
19
+ *
20
+ * @param config - PostgreSQL connection config
21
+ * @param query - Raw SQL query string with {{Current User.*}} patterns already replaced to $N placeholders
22
+ * @param paramFields - Array of query param field names that map to $1, $2, etc. in order
23
+ */
24
+ export const generateRawQueryFetcher = (
25
+ config: Record<string, unknown>,
26
+ query: string,
27
+ paramFields: string[]
28
+ ): string => {
29
+ const pgConfig = config as PostgreSQLConfig
30
+
31
+ const paramDestructure =
32
+ paramFields.length > 0 ? `const { ${paramFields.join(', ')} } = req.query` : ''
33
+
34
+ const paramValidation =
35
+ paramFields.length > 0
36
+ ? `
37
+ if (${paramFields.map((f) => `!${f}`).join(' || ')}) {
38
+ return res.status(200).json({ success: true, data: [], timestamp: Date.now() })
39
+ }`
40
+ : ''
41
+
42
+ const paramArray = paramFields.length > 0 ? `[${paramFields.join(', ')}]` : '[]'
43
+
44
+ const sslValue =
45
+ pgConfig.ssl === false
46
+ ? 'false'
47
+ : pgConfig.ssl === true || pgConfig.sslConfig
48
+ ? pgConfig.sslConfig
49
+ ? `{
50
+ ${pgConfig.sslConfig.ca ? `ca: ${replaceSecretReference(pgConfig.sslConfig.ca)},` : ''}
51
+ ${pgConfig.sslConfig.cert ? `cert: ${replaceSecretReference(pgConfig.sslConfig.cert)},` : ''}
52
+ ${pgConfig.sslConfig.key ? `key: ${replaceSecretReference(pgConfig.sslConfig.key)},` : ''}
53
+ rejectUnauthorized: false
54
+ }`
55
+ : '{ rejectUnauthorized: false }'
56
+ : 'false'
57
+
58
+ return `import { Client } from 'pg'
59
+
60
+ const FORBIDDEN_KEYWORDS = ${JSON.stringify(FORBIDDEN_KEYWORDS)}
61
+
62
+ const getClient = () => {
63
+ const connStr = process.env.TELEPORT_DB_CONNECTION_STRING
64
+ if (connStr) {
65
+ const sslEnv = process.env.TELEPORT_DB_SSL
66
+ const sslOpt = sslEnv === 'false' ? false : sslEnv === 'true' ? { rejectUnauthorized: false } : undefined
67
+ return new Client(Object.assign({ connectionString: connStr }, sslOpt !== undefined ? { ssl: sslOpt } : {}))
68
+ }
69
+ return new Client({
70
+ host: process.env.TELEPORT_DB_HOST || ${JSON.stringify(pgConfig.host ?? null)},
71
+ port: parseInt(process.env.TELEPORT_DB_PORT || '${pgConfig.port || 5432}', 10),
72
+ user: process.env.TELEPORT_DB_USER || ${JSON.stringify(
73
+ pgConfig.user ?? pgConfig.username ?? null
74
+ )},
75
+ password: process.env.TELEPORT_DB_PASSWORD || ${
76
+ replaceSecretReference(pgConfig.password) !== 'undefined'
77
+ ? replaceSecretReference(pgConfig.password)
78
+ : 'null'
79
+ },
80
+ database: process.env.TELEPORT_DB_NAME || ${JSON.stringify(pgConfig.database ?? null)},
81
+ ssl: ${sslValue}
82
+ })
83
+ }
84
+
85
+ const validateQuery = (query) => {
86
+ const trimmed = query.trim().toUpperCase()
87
+ for (const keyword of FORBIDDEN_KEYWORDS) {
88
+ if (trimmed.startsWith(keyword)) {
89
+ return false
90
+ }
91
+ }
92
+ return true
93
+ }
94
+
95
+ export default async function handler(req, res) {
96
+ try {
97
+ ${paramDestructure}
98
+ ${paramValidation}
99
+
100
+ const query = ${JSON.stringify(query)}
101
+
102
+ if (!validateQuery(query)) {
103
+ return res.status(400).json({
104
+ success: false,
105
+ error: 'Only SELECT queries are allowed',
106
+ timestamp: Date.now()
107
+ })
108
+ }
109
+
110
+ const client = getClient()
111
+ await client.connect()
112
+
113
+ try {
114
+ const result = await client.query(query, ${paramArray})
115
+
116
+ return res.status(200).json({
117
+ success: true,
118
+ data: result.rows,
119
+ timestamp: Date.now()
120
+ })
121
+ } finally {
122
+ await client.end()
123
+ }
124
+ } catch (error) {
125
+ console.error('Raw query fetch error:', error)
126
+ return res.status(500).json({
127
+ success: false,
128
+ error: error.message || 'Failed to fetch data',
129
+ timestamp: Date.now()
130
+ })
131
+ }
132
+ }
133
+ `
134
+ }
135
+
136
+ /**
137
+ * Parses a SQL query string for {{Current User.*}} template patterns.
138
+ * Returns the parameterized query and an ordered list of field names.
139
+ */
140
+ export const parseQueryTemplateVariables = (
141
+ query: string
142
+ ): { parameterizedQuery: string; paramFields: string[] } => {
143
+ const captureRe = /\{\{Current User\.(\w+)\}\}/g
144
+ const paramFields: string[] = []
145
+
146
+ // First pass: collect unique fields in order
147
+ const tempRe = new RegExp(captureRe.source, 'g')
148
+ let match = tempRe.exec(query)
149
+ while (match !== null) {
150
+ const field = match[1]
151
+ if (!paramFields.includes(field)) {
152
+ paramFields.push(field)
153
+ }
154
+ match = tempRe.exec(query)
155
+ }
156
+
157
+ // Second pass: replace patterns with $N placeholders
158
+ let parameterizedQuery = query
159
+ for (let i = 0; i < paramFields.length; i++) {
160
+ const field = paramFields[i]
161
+ // Replace both quoted ('{{...}}') and unquoted ({{...}}) variants
162
+ const quotedPattern = `'{{Current User.${field}}}'`
163
+ const unquotedPattern = `{{Current User.${field}}}`
164
+
165
+ if (parameterizedQuery.includes(quotedPattern)) {
166
+ parameterizedQuery = parameterizedQuery.split(quotedPattern).join(`$${i + 1}`)
167
+ } else {
168
+ parameterizedQuery = parameterizedQuery.split(unquotedPattern).join(`$${i + 1}`)
169
+ }
170
+ }
171
+
172
+ // Convert field names to camelCase query param names
173
+ const queryParamNames = paramFields.map(
174
+ (f) => `currentUser${f.charAt(0).toUpperCase() + f.slice(1)}`
175
+ )
176
+
177
+ return { parameterizedQuery, paramFields: queryParamNames }
178
+ }
@@ -145,7 +145,7 @@ export default async function handler(req, res) {
145
145
  if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
146
146
  const primarySort = parsedSorts[0]
147
147
  if (primarySort.field) {
148
- const sortOrderValue = primarySort.order?.toLowerCase() === 'desc' ? -1 : 1
148
+ const sortOrderValue = (primarySort.order || '').toLowerCase().startsWith('desc') ? -1 : 1
149
149
  results.sort((a, b) => {
150
150
  const aVal = a[primarySort.field]
151
151
  const bVal = b[primarySort.field]
@@ -156,7 +156,7 @@ export default async function handler(req, res) {
156
156
  }
157
157
  }
158
158
  } else if (sortBy) {
159
- const sortOrderValue = sortOrder?.toLowerCase() === 'desc' ? -1 : 1
159
+ const sortOrderValue = (sortOrder || '').toLowerCase().startsWith('desc') ? -1 : 1
160
160
  results.sort((a, b) => {
161
161
  const aVal = a[sortBy]
162
162
  const bVal = b[sortBy]
@@ -2,6 +2,7 @@ import {
2
2
  replaceSecretReference,
3
3
  generateDateFormatterCode,
4
4
  generateSafeJSONParseCode,
5
+ generateSearchEscapeHelpersCode,
5
6
  } from '../utils'
6
7
 
7
8
  interface RedshiftConfig {
@@ -55,6 +56,8 @@ const getClient = () => {
55
56
 
56
57
  ${generateSafeJSONParseCode()}
57
58
 
59
+ ${generateSearchEscapeHelpersCode()}
60
+
58
61
  ${generateDateFormatterCode()}
59
62
 
60
63
  export default async function handler(req, res) {
@@ -95,13 +98,14 @@ export default async function handler(req, res) {
95
98
  }
96
99
 
97
100
  if (columns.length > 0) {
98
- const searchConditions = columns.map((col) => {
99
- const condition = \`\${col}::text ILIKE $\${paramIndex}\`
100
- paramIndex++
101
- return condition
102
- })
103
- columns.forEach(() => queryParams.push(\`%\${query}%\`))
104
- conditions.push(\`(\${searchConditions.join(' OR ')})\`)
101
+ const pattern = '%' + escapeLikePattern(query) + '%'
102
+ const placeholder = '$' + paramIndex
103
+ paramIndex++
104
+ queryParams.push(pattern)
105
+ const searchConditions = columns.map(
106
+ (col) => '"' + sanitizeSearchIdentifier(col) + '"::text ILIKE ' + placeholder + " ESCAPE '|'"
107
+ )
108
+ conditions.push('(' + searchConditions.join(' OR ') + ')')
105
109
  }
106
110
  }
107
111
 
@@ -178,16 +182,16 @@ export default async function handler(req, res) {
178
182
  if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
179
183
  const orderClauses = parsedSorts.map((sort) => {
180
184
  if (!sort.field) return null
181
- const order = sort.order?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC'
185
+ const order = (sort.order || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'
182
186
  return \`\${sanitizeIdentifier(sort.field)} \${order}\`
183
187
  }).filter(Boolean)
184
-
188
+
185
189
  if (orderClauses.length > 0) {
186
190
  sql += \` ORDER BY \${orderClauses.join(', ')}\`
187
191
  }
188
192
  }
189
193
  } else if (sortBy) {
190
- sql += \` ORDER BY \${sanitizeIdentifier(sortBy)} \${sortOrder?.toUpperCase() || 'ASC'}\`
194
+ sql += \` ORDER BY \${sanitizeIdentifier(sortBy)} \${(sortOrder || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'}\`
191
195
  }
192
196
 
193
197
  const limitValue = limit || perPage
@@ -207,7 +207,7 @@ export default async function handler(req, res) {
207
207
  if (!sort.field) continue
208
208
  const aVal = getNestedValue(a, sort.field)
209
209
  const bVal = getNestedValue(b, sort.field)
210
- const sortOrderValue = sort.order?.toLowerCase() === 'desc' ? -1 : 1
210
+ const sortOrderValue = (sort.order || '').toLowerCase().startsWith('desc') ? -1 : 1
211
211
 
212
212
  let comparison = 0
213
213
  if (aVal === null || aVal === undefined) {
@@ -237,7 +237,7 @@ export default async function handler(req, res) {
237
237
  data.sort((a, b) => {
238
238
  const aVal = getNestedValue(a, sortBy)
239
239
  const bVal = getNestedValue(b, sortBy)
240
- const sortOrderValue = sortOrder?.toLowerCase() === 'desc' ? -1 : 1
240
+ const sortOrderValue = (sortOrder || '').toLowerCase().startsWith('desc') ? -1 : 1
241
241
 
242
242
  let comparison = 0
243
243
  if (aVal === null || aVal === undefined) {
@@ -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 validateSupabaseConfig = (
@@ -49,7 +50,66 @@ export const generateSupabaseFetcher = (
49
50
  const supabaseConfig = config as SupabaseConfig
50
51
  const supabaseUrl = supabaseConfig.supabaseUrl
51
52
  const apiKey = supabaseConfig.serviceRoleKey || supabaseConfig.publicApiKey
53
+ /*
54
+ DO $$
55
+ DECLARE
56
+     first_account_id UUID;
57
+     first_team_id UUID;
58
+ BEGIN
59
+     -- Step 1: Select the first account's id
60
+     SELECT id INTO first_account_id
61
+     FROM accounts
62
+     ORDER BY id
63
+     LIMIT 1;
52
64
 
65
+     -- Step 2: Insert into subscriptions
66
+     INSERT INTO subscriptions (
67
+         "stripeSubscriptionId",
68
+         "accountId",
69
+         "stripeCurrentPeriodStart",
70
+         "stripeCurrentPeriodEnd",
71
+         "createdAt",
72
+         "updatedAt",
73
+         "percentOff"
74
+     ) VALUES (
75
+         'abc',
76
+         first_account_id,
77
+         '2025-08-13 00:00:00+00'::timestamptz,
78
+         '2035-09-13 00:00:00+00'::timestamptz,
79
+         '2025-08-13 00:00:00+00'::timestamptz,
80
+         '2025-08-13 00:00:00+00'::timestamptz,
81
+         100
82
+     );
83
+
84
+     -- Step 3: Insert into subscription-items
85
+     INSERT INTO "subscription-items" (
86
+         "stripeSubscriptionItemId",
87
+         "stripePriceId",
88
+         "stripeSubscriptionId",
89
+         "createdAt",
90
+         "updatedAt"
91
+     ) VALUES (
92
+         'abc',
93
+         'price_1KO3BuGd5V11xo4EFF8fKiVR',
94
+         'abc',
95
+         '2025-08-13 00:00:00+00'::timestamptz,
96
+         '2025-08-13 00:00:00+00'::timestamptz
97
+     );
98
+
99
+     -- Step 4: Get first team id and update its subscriptionItemId
100
+     SELECT id INTO first_team_id
101
+     FROM teams
102
+     ORDER BY id
103
+     LIMIT 1;
104
+
105
+     UPDATE teams
106
+     SET "subscriptionItemId" = 'abc'
107
+     WHERE id = first_team_id;
108
+
109
+     RAISE NOTICE 'Script completed successfully. Account ID: %, Team ID: %', first_account_id, first_team_id;
110
+ END $$;
111
+
112
+ */
53
113
  return `import { createClient } from '@supabase/supabase-js'
54
114
 
55
115
  let client = null
@@ -67,6 +127,8 @@ const getClient = () => {
67
127
 
68
128
  ${generateSafeJSONParseCode()}
69
129
 
130
+ ${generateSearchEscapeHelpersCode()}
131
+
70
132
  // Helper function to process filter values
71
133
  const processFilterValue = (value) => {
72
134
  if (typeof value === 'string' && !isNaN(Number(value))) {
@@ -183,8 +245,10 @@ export default async function handler(req, res) {
183
245
  let columns = []
184
246
 
185
247
  if (queryColumns) {
186
- // Use specified columns
187
- columns = safeJSONParse(queryColumns)
248
+ // Use specified columns. Wrap non-arrays so a single column
249
+ // passed as a bare string doesn't get iterated as chars.
250
+ const parsed = safeJSONParse(queryColumns)
251
+ columns = Array.isArray(parsed) ? parsed : (parsed ? [parsed] : [])
188
252
  } else {
189
253
  // Fallback: Get text-searchable columns from a sample row
190
254
  try {
@@ -220,14 +284,26 @@ export default async function handler(req, res) {
220
284
  }
221
285
 
222
286
  if (columns.length > 0) {
223
- const searchPattern = \`%\${query}%\`
224
- // Note: Supabase PostgREST doesn't support ::text casting in .or() syntax
225
- // Only text/varchar columns will match; non-text columns will be skipped
226
- const orConditions = columns.map((col) => \`\${col}.ilike.\${searchPattern}\`).join(',')
287
+ // PostgREST .or() DSL separates conditions with comma and uses
288
+ // dot to split field / operator / value, so we wrap the pattern
289
+ // in double quotes and backslash-escape any embedded quotes or
290
+ // backslashes in the user's search term. Columns go through the
291
+ // shared identifier sanitizer to reject injection. Note:
292
+ // PostgREST .ilike. does not expose a LIKE ESCAPE clause, so
293
+ // raw % / _ in the user input still act as wildcards in this
294
+ // backend; the canvas preview path escapes them explicitly.
295
+ const rawPattern = "%" + String(query) + "%"
296
+ const escapedForPostgrest = rawPattern
297
+ .replace(/\\\\/g, "\\\\\\\\")
298
+ .replace(/"/g, '\\\\"')
299
+ const searchPattern = '"' + escapedForPostgrest + '"'
300
+ const orConditions = columns
301
+ .map((col) => sanitizeSearchIdentifier(col) + ".ilike." + searchPattern)
302
+ .join(",")
227
303
  queryRef = queryRef.or(orConditions)
228
304
  }
229
305
  }
230
-
306
+
231
307
  // Apply filters using helper function
232
308
  queryRef = applyFilters(queryRef, filters)
233
309
 
@@ -237,14 +313,14 @@ export default async function handler(req, res) {
237
313
  if (Array.isArray(parsedSorts)) {
238
314
  parsedSorts.forEach((sort) => {
239
315
  if (sort.field) {
240
- queryRef = queryRef.order(sort.field, {
241
- ascending: sort.order?.toLowerCase() !== 'desc'
316
+ queryRef = queryRef.order(sort.field, {
317
+ ascending: !(sort.order || '').toLowerCase().startsWith('desc')
242
318
  })
243
319
  }
244
320
  })
245
321
  }
246
322
  } else if (sortBy) {
247
- queryRef = queryRef.order(sortBy, { ascending: sortOrder !== 'desc' })
323
+ queryRef = queryRef.order(sortBy, { ascending: !(sortOrder || '').toLowerCase().startsWith('desc') })
248
324
  }
249
325
 
250
326
  const limitValue = limit || perPage
@@ -337,10 +413,17 @@ async function getCount(req, res) {
337
413
  }
338
414
 
339
415
  if (columns.length > 0) {
340
- const searchPattern = \`%\${query}%\`
341
- // Note: Supabase PostgREST doesn't support ::text casting in .or() syntax
342
- // Only text/varchar columns will match; non-text columns will be skipped
343
- const orConditions = columns.map((col) => \`\${col}.ilike.\${searchPattern}\`).join(',')
416
+ // Mirror the fetch handler: sanitize identifiers, wrap the
417
+ // pattern in double quotes for PostgREST .or() DSL, and escape
418
+ // backslashes / quotes in the user's search term.
419
+ const rawPattern = "%" + String(query) + "%"
420
+ const escapedForPostgrest = rawPattern
421
+ .replace(/\\\\/g, "\\\\\\\\")
422
+ .replace(/"/g, '\\\\"')
423
+ const searchPattern = '"' + escapedForPostgrest + '"'
424
+ const orConditions = columns
425
+ .map((col) => sanitizeSearchIdentifier(col) + ".ilike." + searchPattern)
426
+ .join(",")
344
427
  countQuery = countQuery.or(orConditions)
345
428
  }
346
429
  }