@teleporthq/teleport-plugin-next-data-source 0.43.62 → 0.43.64

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 (200) hide show
  1. package/__tests__/blog-post-categories.test.ts +131 -0
  2. package/__tests__/custom-page-transform.test.ts +119 -0
  3. package/__tests__/ecommerce-product-ratings.test.ts +155 -0
  4. package/__tests__/ecommerce-product-storefront-contract.test.ts +147 -0
  5. package/__tests__/filter-groups.test.ts +174 -0
  6. package/__tests__/product-reviews-structured-data.test.ts +196 -0
  7. package/dist/cjs/data-source-fetchers.d.ts +3 -3
  8. package/dist/cjs/data-source-fetchers.d.ts.map +1 -1
  9. package/dist/cjs/data-source-fetchers.js.map +1 -1
  10. package/dist/cjs/fetchers/airtable.d.ts.map +1 -1
  11. package/dist/cjs/fetchers/airtable.js +1 -1
  12. package/dist/cjs/fetchers/airtable.js.map +1 -1
  13. package/dist/cjs/fetchers/clickhouse.d.ts.map +1 -1
  14. package/dist/cjs/fetchers/clickhouse.js +1 -1
  15. package/dist/cjs/fetchers/clickhouse.js.map +1 -1
  16. package/dist/cjs/fetchers/csv-file.d.ts.map +1 -1
  17. package/dist/cjs/fetchers/csv-file.js +3 -3
  18. package/dist/cjs/fetchers/csv-file.js.map +1 -1
  19. package/dist/cjs/fetchers/firestore.d.ts.map +1 -1
  20. package/dist/cjs/fetchers/firestore.js +1 -1
  21. package/dist/cjs/fetchers/firestore.js.map +1 -1
  22. package/dist/cjs/fetchers/google-sheets.d.ts.map +1 -1
  23. package/dist/cjs/fetchers/google-sheets.js +1 -1
  24. package/dist/cjs/fetchers/google-sheets.js.map +1 -1
  25. package/dist/cjs/fetchers/javascript.d.ts.map +1 -1
  26. package/dist/cjs/fetchers/javascript.js +1 -1
  27. package/dist/cjs/fetchers/javascript.js.map +1 -1
  28. package/dist/cjs/fetchers/mariadb.d.ts.map +1 -1
  29. package/dist/cjs/fetchers/mariadb.js +1 -1
  30. package/dist/cjs/fetchers/mariadb.js.map +1 -1
  31. package/dist/cjs/fetchers/mongodb.d.ts.map +1 -1
  32. package/dist/cjs/fetchers/mongodb.js +1 -1
  33. package/dist/cjs/fetchers/mongodb.js.map +1 -1
  34. package/dist/cjs/fetchers/mysql.d.ts.map +1 -1
  35. package/dist/cjs/fetchers/mysql.js +1 -1
  36. package/dist/cjs/fetchers/mysql.js.map +1 -1
  37. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  38. package/dist/cjs/fetchers/postgresql.js +1 -1
  39. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  40. package/dist/cjs/fetchers/redis.d.ts.map +1 -1
  41. package/dist/cjs/fetchers/redis.js +1 -1
  42. package/dist/cjs/fetchers/redis.js.map +1 -1
  43. package/dist/cjs/fetchers/redshift.d.ts.map +1 -1
  44. package/dist/cjs/fetchers/redshift.js +2 -2
  45. package/dist/cjs/fetchers/redshift.js.map +1 -1
  46. package/dist/cjs/fetchers/rest-api.d.ts.map +1 -1
  47. package/dist/cjs/fetchers/rest-api.js +2 -2
  48. package/dist/cjs/fetchers/rest-api.js.map +1 -1
  49. package/dist/cjs/fetchers/supabase.d.ts.map +1 -1
  50. package/dist/cjs/fetchers/supabase.js +1 -1
  51. package/dist/cjs/fetchers/supabase.js.map +1 -1
  52. package/dist/cjs/fetchers/teleport.d.ts +2 -2
  53. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  54. package/dist/cjs/fetchers/teleport.js +1 -1
  55. package/dist/cjs/fetchers/teleport.js.map +1 -1
  56. package/dist/cjs/fetchers/turso.d.ts.map +1 -1
  57. package/dist/cjs/fetchers/turso.js +1 -1
  58. package/dist/cjs/fetchers/turso.js.map +1 -1
  59. package/dist/cjs/index.d.ts +1 -1
  60. package/dist/cjs/index.d.ts.map +1 -1
  61. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  62. package/dist/cjs/pagination-plugin.js.map +1 -1
  63. package/dist/cjs/transformations/blog-post.d.ts +7 -1
  64. package/dist/cjs/transformations/blog-post.d.ts.map +1 -1
  65. package/dist/cjs/transformations/blog-post.js +4 -2
  66. package/dist/cjs/transformations/blog-post.js.map +1 -1
  67. package/dist/cjs/transformations/category-taxonomy.d.ts +33 -0
  68. package/dist/cjs/transformations/category-taxonomy.d.ts.map +1 -0
  69. package/dist/cjs/transformations/category-taxonomy.js +42 -0
  70. package/dist/cjs/transformations/category-taxonomy.js.map +1 -0
  71. package/dist/cjs/transformations/custom-page.d.ts +18 -0
  72. package/dist/cjs/transformations/custom-page.d.ts.map +1 -0
  73. package/dist/cjs/transformations/custom-page.js +24 -0
  74. package/dist/cjs/transformations/custom-page.js.map +1 -0
  75. package/dist/cjs/transformations/ecommerce-product.d.ts.map +1 -1
  76. package/dist/cjs/transformations/ecommerce-product.js +2 -35
  77. package/dist/cjs/transformations/ecommerce-product.js.map +1 -1
  78. package/dist/cjs/transformations/index.d.ts +23 -4
  79. package/dist/cjs/transformations/index.d.ts.map +1 -1
  80. package/dist/cjs/transformations/index.js +38 -8
  81. package/dist/cjs/transformations/index.js.map +1 -1
  82. package/dist/cjs/transformations/shared-utils.d.ts.map +1 -1
  83. package/dist/cjs/transformations/shared-utils.js +1 -1
  84. package/dist/cjs/transformations/shared-utils.js.map +1 -1
  85. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  86. package/dist/cjs/utils.d.ts +35 -3
  87. package/dist/cjs/utils.d.ts.map +1 -1
  88. package/dist/cjs/utils.js +116 -18
  89. package/dist/cjs/utils.js.map +1 -1
  90. package/dist/esm/data-source-fetchers.d.ts +3 -3
  91. package/dist/esm/data-source-fetchers.d.ts.map +1 -1
  92. package/dist/esm/data-source-fetchers.js.map +1 -1
  93. package/dist/esm/fetchers/airtable.d.ts.map +1 -1
  94. package/dist/esm/fetchers/airtable.js +2 -2
  95. package/dist/esm/fetchers/airtable.js.map +1 -1
  96. package/dist/esm/fetchers/clickhouse.d.ts.map +1 -1
  97. package/dist/esm/fetchers/clickhouse.js +2 -2
  98. package/dist/esm/fetchers/clickhouse.js.map +1 -1
  99. package/dist/esm/fetchers/csv-file.d.ts.map +1 -1
  100. package/dist/esm/fetchers/csv-file.js +4 -4
  101. package/dist/esm/fetchers/csv-file.js.map +1 -1
  102. package/dist/esm/fetchers/firestore.d.ts.map +1 -1
  103. package/dist/esm/fetchers/firestore.js +2 -2
  104. package/dist/esm/fetchers/firestore.js.map +1 -1
  105. package/dist/esm/fetchers/google-sheets.d.ts.map +1 -1
  106. package/dist/esm/fetchers/google-sheets.js +2 -2
  107. package/dist/esm/fetchers/google-sheets.js.map +1 -1
  108. package/dist/esm/fetchers/javascript.d.ts.map +1 -1
  109. package/dist/esm/fetchers/javascript.js +2 -2
  110. package/dist/esm/fetchers/javascript.js.map +1 -1
  111. package/dist/esm/fetchers/mariadb.d.ts.map +1 -1
  112. package/dist/esm/fetchers/mariadb.js +2 -2
  113. package/dist/esm/fetchers/mariadb.js.map +1 -1
  114. package/dist/esm/fetchers/mongodb.d.ts.map +1 -1
  115. package/dist/esm/fetchers/mongodb.js +2 -2
  116. package/dist/esm/fetchers/mongodb.js.map +1 -1
  117. package/dist/esm/fetchers/mysql.d.ts.map +1 -1
  118. package/dist/esm/fetchers/mysql.js +2 -2
  119. package/dist/esm/fetchers/mysql.js.map +1 -1
  120. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  121. package/dist/esm/fetchers/postgresql.js +2 -2
  122. package/dist/esm/fetchers/postgresql.js.map +1 -1
  123. package/dist/esm/fetchers/redis.d.ts.map +1 -1
  124. package/dist/esm/fetchers/redis.js +2 -2
  125. package/dist/esm/fetchers/redis.js.map +1 -1
  126. package/dist/esm/fetchers/redshift.d.ts.map +1 -1
  127. package/dist/esm/fetchers/redshift.js +3 -3
  128. package/dist/esm/fetchers/redshift.js.map +1 -1
  129. package/dist/esm/fetchers/rest-api.d.ts.map +1 -1
  130. package/dist/esm/fetchers/rest-api.js +3 -3
  131. package/dist/esm/fetchers/rest-api.js.map +1 -1
  132. package/dist/esm/fetchers/supabase.d.ts.map +1 -1
  133. package/dist/esm/fetchers/supabase.js +2 -2
  134. package/dist/esm/fetchers/supabase.js.map +1 -1
  135. package/dist/esm/fetchers/teleport.d.ts +2 -2
  136. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  137. package/dist/esm/fetchers/teleport.js +2 -2
  138. package/dist/esm/fetchers/teleport.js.map +1 -1
  139. package/dist/esm/fetchers/turso.d.ts.map +1 -1
  140. package/dist/esm/fetchers/turso.js +2 -2
  141. package/dist/esm/fetchers/turso.js.map +1 -1
  142. package/dist/esm/index.d.ts +1 -1
  143. package/dist/esm/index.d.ts.map +1 -1
  144. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  145. package/dist/esm/pagination-plugin.js +1 -1
  146. package/dist/esm/pagination-plugin.js.map +1 -1
  147. package/dist/esm/transformations/blog-post.d.ts +7 -1
  148. package/dist/esm/transformations/blog-post.d.ts.map +1 -1
  149. package/dist/esm/transformations/blog-post.js +4 -2
  150. package/dist/esm/transformations/blog-post.js.map +1 -1
  151. package/dist/esm/transformations/category-taxonomy.d.ts +33 -0
  152. package/dist/esm/transformations/category-taxonomy.d.ts.map +1 -0
  153. package/dist/esm/transformations/category-taxonomy.js +37 -0
  154. package/dist/esm/transformations/category-taxonomy.js.map +1 -0
  155. package/dist/esm/transformations/custom-page.d.ts +18 -0
  156. package/dist/esm/transformations/custom-page.d.ts.map +1 -0
  157. package/dist/esm/transformations/custom-page.js +20 -0
  158. package/dist/esm/transformations/custom-page.js.map +1 -0
  159. package/dist/esm/transformations/ecommerce-product.d.ts.map +1 -1
  160. package/dist/esm/transformations/ecommerce-product.js +3 -36
  161. package/dist/esm/transformations/ecommerce-product.js.map +1 -1
  162. package/dist/esm/transformations/index.d.ts +23 -4
  163. package/dist/esm/transformations/index.d.ts.map +1 -1
  164. package/dist/esm/transformations/index.js +37 -7
  165. package/dist/esm/transformations/index.js.map +1 -1
  166. package/dist/esm/transformations/shared-utils.d.ts.map +1 -1
  167. package/dist/esm/transformations/shared-utils.js +1 -1
  168. package/dist/esm/transformations/shared-utils.js.map +1 -1
  169. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  170. package/dist/esm/utils.d.ts +35 -3
  171. package/dist/esm/utils.d.ts.map +1 -1
  172. package/dist/esm/utils.js +112 -17
  173. package/dist/esm/utils.js.map +1 -1
  174. package/package.json +5 -5
  175. package/src/data-source-fetchers.ts +3 -3
  176. package/src/fetchers/airtable.ts +35 -36
  177. package/src/fetchers/clickhouse.ts +26 -36
  178. package/src/fetchers/csv-file.ts +13 -21
  179. package/src/fetchers/firestore.ts +41 -35
  180. package/src/fetchers/google-sheets.ts +15 -23
  181. package/src/fetchers/javascript.ts +11 -21
  182. package/src/fetchers/mariadb.ts +33 -44
  183. package/src/fetchers/mongodb.ts +66 -81
  184. package/src/fetchers/mysql.ts +33 -45
  185. package/src/fetchers/postgresql.ts +52 -62
  186. package/src/fetchers/redis.ts +6 -5
  187. package/src/fetchers/redshift.ts +29 -41
  188. package/src/fetchers/rest-api.ts +11 -21
  189. package/src/fetchers/supabase.ts +104 -41
  190. package/src/fetchers/teleport.ts +55 -66
  191. package/src/fetchers/turso.ts +27 -39
  192. package/src/index.ts +1 -1
  193. package/src/pagination-plugin.ts +3 -6
  194. package/src/transformations/blog-post.ts +28 -9
  195. package/src/transformations/category-taxonomy.ts +107 -0
  196. package/src/transformations/custom-page.ts +83 -0
  197. package/src/transformations/ecommerce-product.ts +177 -78
  198. package/src/transformations/index.ts +85 -11
  199. package/src/transformations/shared-utils.ts +9 -0
  200. package/src/utils.ts +191 -34
@@ -1,7 +1,8 @@
1
- import type { GeneratorOptions } from '@teleporthq/teleport-types'
1
+ import type { GeneratorOptions, UIDLEcommerceCategory } from '@teleporthq/teleport-types'
2
2
  import { StorefrontTax } from '@teleporthq/teleport-shared'
3
3
  import { generateSharedTransformationCode } from './shared-utils'
4
4
  import { generateBlogPostTransformationCode } from './blog-post'
5
+ import { generateCustomPageTransformationCode } from './custom-page'
5
6
  import {
6
7
  generateEcommerceProductTransformationCode,
7
8
  type EcommerceProductTransformOptions,
@@ -9,6 +10,17 @@ import {
9
10
 
10
11
  export type { EcommerceProductTransformOptions }
11
12
 
13
+ /**
14
+ * Everything a generated fetcher bakes in, for EITHER entity. One options object
15
+ * because `getTransformationCode` is called with a table name and has to be able
16
+ * to serve whichever transform that resolves to — the product fields are ignored
17
+ * for a blog table and vice versa.
18
+ */
19
+ export interface EntityTransformOptions extends EcommerceProductTransformOptions {
20
+ /** Blog post-category taxonomy — see `blogSettings.categories`. */
21
+ blogCategories?: UIDLEcommerceCategory[]
22
+ }
23
+
12
24
  /**
13
25
  * Stock never gates purchasability when the merchant disabled stock management
14
26
  * entirely or opted into backorders. Mirrors the GUI rule in
@@ -35,14 +47,15 @@ const resolveAllowBackorders = (
35
47
  * "stock never blocks a purchase" flag — see `resolveAllowBackorders`.
36
48
  */
37
49
  export const buildProductTransformOptions = (
38
- options: Pick<GeneratorOptions, 'ecommerceSettings' | 'invoiceSettings'>
39
- ): EcommerceProductTransformOptions => ({
50
+ options: Pick<GeneratorOptions, 'ecommerceSettings' | 'invoiceSettings' | 'blogSettings'>
51
+ ): EntityTransformOptions => ({
40
52
  categories: options.ecommerceSettings?.categories,
53
+ blogCategories: options.blogSettings?.categories,
41
54
  storefrontTaxRate: StorefrontTax.resolveStorefrontTaxRate(options.invoiceSettings),
42
55
  allowBackorders: resolveAllowBackorders(options.ecommerceSettings),
43
56
  })
44
57
 
45
- export type TransformationType = 'blog-post' | 'ecommerce-product' | null
58
+ export type TransformationType = 'blog-post' | 'ecommerce-product' | 'custom-page' | null
46
59
 
47
60
  /**
48
61
  * Strips a leading `schema.` qualifier from a table name so that only the
@@ -80,6 +93,9 @@ export const detectTransformationType = (tableName: string): TransformationType
80
93
  if (bare === 'teleport_products') {
81
94
  return 'ecommerce-product'
82
95
  }
96
+ if (bare === 'teleport_pages') {
97
+ return 'custom-page'
98
+ }
83
99
  return null
84
100
  }
85
101
 
@@ -90,7 +106,7 @@ export const detectTransformationType = (tableName: string): TransformationType
90
106
  */
91
107
  export const getTransformationCode = (
92
108
  tableName: string,
93
- options: EcommerceProductTransformOptions = {}
109
+ options: EntityTransformOptions = {}
94
110
  ): string => {
95
111
  const type = detectTransformationType(tableName)
96
112
  if (!type) {
@@ -101,9 +117,11 @@ export const getTransformationCode = (
101
117
 
102
118
  switch (type) {
103
119
  case 'blog-post':
104
- return shared + generateBlogPostTransformationCode()
120
+ return shared + generateBlogPostTransformationCode({ categories: options.blogCategories })
105
121
  case 'ecommerce-product':
106
122
  return shared + generateEcommerceProductTransformationCode(options)
123
+ case 'custom-page':
124
+ return shared + generateCustomPageTransformationCode()
107
125
  default:
108
126
  return ''
109
127
  }
@@ -126,11 +144,23 @@ export const getTransformExpression = (tableName: string): string | null => {
126
144
  return 'await transformRecords(safeData, getClient, req.query)'
127
145
  case 'ecommerce-product':
128
146
  return 'await transformRecords(safeData, getClient, req.query)'
147
+ case 'custom-page':
148
+ return 'await transformRecords(safeData, getClient, req.query)'
129
149
  default:
130
150
  return null
131
151
  }
132
152
  }
133
153
 
154
+ /**
155
+ * How many reviews a product page carries into its structured data.
156
+ *
157
+ * They are inlined TWICE — once into the page's props, once into the JSON-LD
158
+ * script rendered from them — so this number is paid for in bytes on every
159
+ * request. Five is what a review snippet shows; the rest are on the page itself,
160
+ * fetched by the reviews section's own paginated query.
161
+ */
162
+ export const REVIEWS_PER_PRODUCT = 5
163
+
134
164
  /**
135
165
  * Returns the transform wrapper function code that handles asset map loading
136
166
  * and calls the appropriate transformer.
@@ -142,7 +172,12 @@ export const getTransformWrapperCode = (tableName: string): string => {
142
172
  return ''
143
173
  }
144
174
 
145
- const transformFn = type === 'blog-post' ? 'transformBlogPosts' : 'transformEcommerceProducts'
175
+ const transformFn =
176
+ type === 'blog-post'
177
+ ? 'transformBlogPosts'
178
+ : type === 'custom-page'
179
+ ? 'transformCustomPages'
180
+ : 'transformEcommerceProducts'
146
181
 
147
182
  // Products additionally get their purchasable variant combinations attached in
148
183
  // ONE batched query (keyed by product id). Blog posts have no such enrichment.
@@ -160,8 +195,11 @@ export const getTransformWrapperCode = (tableName: string): string => {
160
195
  type === 'ecommerce-product'
161
196
  ? `
162
197
  var variantsByProductId = null
198
+ // Declared OUTSIDE the try because the ratings lookup below reuses it. \`var\`
199
+ // would hoist it anyway, but relying on that would make this block break
200
+ // silently the day someone modernises it to \`let\`.
201
+ var __variantPids = []
163
202
  try {
164
- var __variantPids = []
165
203
  for (var __i = 0; __i < records.length; __i++) {
166
204
  if (records[__i] && records[__i].id != null) __variantPids.push(records[__i].id)
167
205
  }
@@ -178,11 +216,41 @@ export const getTransformWrapperCode = (tableName: string): string => {
178
216
  variantsByProductId = await getVariantsMap(getClientFn, __variantPids)
179
217
  } catch (e) {
180
218
  // Leaves the map null — "unknown", never "none".
219
+ }
220
+ // The aggregate star rating for the same id set, in ONE more query. Reuses
221
+ // __variantPids so a details page's related cards get their ratings from the
222
+ // same round trip rather than one query per card.
223
+ //
224
+ // Unlike the variants map this stays {} on failure: "no rating to show" is
225
+ // both the empty answer and the safe answer, so there is no unknown state for
226
+ // a caller to reason about.
227
+ var ratingsByProductId = {}
228
+ try {
229
+ ratingsByProductId = await getRatingsMap(getClientFn, __variantPids)
230
+ } catch (e) {
231
+ // Leaves the map empty — the star rows simply stay hidden.
232
+ }
233
+ // The individual reviews behind the stars, for the product page's JSON-LD.
234
+ //
235
+ // Gated on the SINGLE-RECORD heuristic, unlike the two lookups above. Those
236
+ // feed something every card on the page draws; this feeds structured data,
237
+ // which only a details page emits — and a listing of 24 products would
238
+ // otherwise fetch and inline 120 review rows that nothing on it renders or
239
+ // markup-references.
240
+ var reviewsByProductId = {}
241
+ if (Array.isArray(records) && records.length === 1) {
242
+ try {
243
+ reviewsByProductId = await getProductReviewsMap(getClientFn, __variantPids, ${REVIEWS_PER_PRODUCT})
244
+ } catch (e) {
245
+ // Best-effort; the product simply ships without review snippets.
246
+ }
181
247
  }`
182
248
  : ''
183
249
 
184
250
  const variantOption =
185
- type === 'ecommerce-product' ? ', variantsByProductId: variantsByProductId' : ''
251
+ type === 'ecommerce-product'
252
+ ? ', variantsByProductId: variantsByProductId, ratingsByProductId: ratingsByProductId, reviewsByProductId: reviewsByProductId'
253
+ : ''
186
254
 
187
255
  // Related items are resolved for a SINGLE-record fetch only — which is the
188
256
  // details page, the one surface that renders them (it looks the row up by
@@ -195,9 +263,13 @@ export const getTransformWrapperCode = (tableName: string): string => {
195
263
  // A one-product store's listing does pay for one extra query. That is the
196
264
  // whole cost of the heuristic, and it beats plumbing a page-role flag through
197
265
  // every fetcher.
266
+ // Custom pages have no related items and no variants: their transform only
267
+ // normalises the row's SEO fields.
268
+ const hasRelatedItems = type === 'blog-post' || type === 'ecommerce-product'
198
269
  const relatedMapVar = type === 'blog-post' ? 'relatedPostsById' : 'relatedProductsById'
199
270
  const relatedMapFn = type === 'blog-post' ? 'getRelatedPostsMap' : 'getRelatedProductsMap'
200
- const relatedEnrichment = `
271
+ const relatedEnrichment = hasRelatedItems
272
+ ? `
201
273
  var ${relatedMapVar} = null
202
274
  if (Array.isArray(records) && records.length === 1) {
203
275
  try {
@@ -206,6 +278,8 @@ export const getTransformWrapperCode = (tableName: string): string => {
206
278
  // Best-effort; the related rail stays hidden behind its empty gate.
207
279
  }
208
280
  }`
281
+ : ''
282
+ const relatedOption = hasRelatedItems ? `, ${relatedMapVar}: ${relatedMapVar}` : ''
209
283
 
210
284
  return `
211
285
  async function transformRecords(records, getClientFn, reqQuery) {
@@ -217,7 +291,7 @@ async function transformRecords(records, getClientFn, reqQuery) {
217
291
  }${relatedEnrichment}${variantEnrichment}
218
292
  var currentLanguage = (reqQuery && reqQuery.lang) || null
219
293
  var mainLanguage = (reqQuery && reqQuery.mainLang) || null
220
- var options = { assetMap: assetMap, currentLanguage: currentLanguage, mainLanguage: mainLanguage${variantOption}, ${relatedMapVar}: ${relatedMapVar} }
294
+ var options = { assetMap: assetMap, currentLanguage: currentLanguage, mainLanguage: mainLanguage${variantOption}${relatedOption} }
221
295
  return ${transformFn}(records, options)
222
296
  }
223
297
  `
@@ -248,6 +248,15 @@ function resolveMediaUrl(value, assetMap) {
248
248
  return isResolvedMediaUrl(resolved) ? resolved : null
249
249
  }
250
250
 
251
+ // Trims a per-row SEO URL cell (blog posts, custom pages); blank/non-string
252
+ // values — including columns that don't exist yet on tables provisioned before
253
+ // the feature — become null.
254
+ function normalizeSeoUrlField(value) {
255
+ if (typeof value !== 'string') return null
256
+ var trimmed = value.trim()
257
+ return trimmed.length > 0 ? trimmed : null
258
+ }
259
+
251
260
  function resolveAssetUrls(values, assetMap) {
252
261
  if (!Array.isArray(values)) return []
253
262
  return values
package/src/utils.ts CHANGED
@@ -12,7 +12,7 @@ import * as types from '@babel/types'
12
12
  import { ASTUtils } from '@teleporthq/teleport-plugin-common'
13
13
  import { GenericUtils, StringUtils } from '@teleporthq/teleport-shared'
14
14
  import { generateDataSourceFetcherWithCore } from './data-source-fetchers'
15
- import type { EcommerceProductTransformOptions } from './transformations'
15
+ import type { EntityTransformOptions } from './transformations'
16
16
  import { DATA_SOURCE_ISR_REVALIDATE_SECONDS } from './isr'
17
17
 
18
18
  const VALID_DATA_SOURCE_TYPES: DataSourceType[] = [
@@ -192,7 +192,7 @@ export const extractDataSourceIntoNextAPIFolder = (
192
192
  dataSources: Record<string, UIDLDataSource>,
193
193
  componentChunk: ChunkDefinition,
194
194
  extractedResources: GeneratorOptions['extractedResources'],
195
- transformOptions: EcommerceProductTransformOptions = {}
195
+ transformOptions: EntityTransformOptions = {}
196
196
  ) => {
197
197
  try {
198
198
  // Validate node content structure
@@ -596,6 +596,146 @@ const dateReplacer = (key, value) => {
596
596
  }`
597
597
  }
598
598
 
599
+ /**
600
+ * Every leaf condition of a filters array, unwrapping any `{ type: 'group' }`
601
+ * envelopes the inspector wrapped them in. Generator-side checks that ask
602
+ * "does any destination reference state / a route param?" must look through
603
+ * groups — asking `entry.destination` of a group silently answers "no", which
604
+ * is how a dynamic filter ends up baked into initialData at build time.
605
+ */
606
+ export const collectFilterLeafConditions = (entries: unknown): unknown[] => {
607
+ if (!Array.isArray(entries)) {
608
+ return []
609
+ }
610
+ const leaves: unknown[] = []
611
+ const walk = (entry: unknown): void => {
612
+ if (!entry || typeof entry !== 'object') {
613
+ return
614
+ }
615
+ const node = entry as { type?: string; children?: unknown[] }
616
+ if (node.type === 'group') {
617
+ ;(node.children || []).forEach(walk)
618
+ return
619
+ }
620
+ leaves.push(entry)
621
+ }
622
+ entries.forEach(walk)
623
+ return leaves
624
+ }
625
+
626
+ /**
627
+ * Rebuilds a filters array keeping its group structure, dropping leaf
628
+ * conditions that `keepCondition` rejects and any group left empty by that.
629
+ * Used so the SSG emit can discard unresolvable dynamic destinations without
630
+ * flattening an `or` group into ANDs on the way.
631
+ */
632
+ export const pruneFilterTree = (
633
+ entries: unknown[],
634
+ keepCondition: (condition: unknown) => boolean
635
+ ): unknown[] => {
636
+ const prune = (entry: unknown): unknown | null => {
637
+ if (!entry || typeof entry !== 'object') {
638
+ return null
639
+ }
640
+ const node = entry as { type?: string; children?: unknown[] }
641
+ if (node.type === 'group') {
642
+ const children = (node.children || []).map(prune).filter(Boolean)
643
+ if (children.length === 0) {
644
+ return null
645
+ }
646
+ return { ...node, children }
647
+ }
648
+ return keepCondition(entry) ? entry : null
649
+ }
650
+ return entries.map(prune).filter(Boolean) as unknown[]
651
+ }
652
+
653
+ /**
654
+ * Runtime helpers for the filter TREE the GUI authors.
655
+ *
656
+ * The inspector emits filters as a tree — a root `{ type: 'group', operator,
657
+ * children }` whose children are `{ type: 'condition', source, destination,
658
+ * operand }` leaves or further groups (an `or` group nested inside the root
659
+ * `and` is how "A and (B or C)" is expressed). Older projects, and params built
660
+ * by hand, still send a flat array of bare conditions or a plain `{ key: value }`
661
+ * map.
662
+ *
663
+ * Every fetcher used to hand-roll `parsedFilters.forEach(f => { if (!f.source)
664
+ * return ... })`, which silently DROPPED any group — a group has no `.source`,
665
+ * so the whole filter became a no-op and the page rendered unfiltered rows.
666
+ * These helpers exist so no fetcher has to know the shape: `normalizeFilterTree`
667
+ * turns all three input forms into one tree, and the walkers below consume it.
668
+ */
669
+ export const generateFilterTreeHelpersCode = (): string => {
670
+ return `const normalizeFilterTree = (parsed) => {
671
+ if (parsed === null || parsed === undefined) return null
672
+
673
+ const toNode = (entry) => {
674
+ if (!entry || typeof entry !== 'object') return null
675
+ if (entry.type === 'group') {
676
+ const children = Array.isArray(entry.children)
677
+ ? entry.children.map(toNode).filter(Boolean)
678
+ : []
679
+ if (children.length === 0) return null
680
+ return { type: 'group', operator: entry.operator === 'or' ? 'or' : 'and', children }
681
+ }
682
+ if (!entry.source || entry.destination === undefined) return null
683
+ return {
684
+ type: 'condition',
685
+ source: entry.source,
686
+ destination: entry.destination,
687
+ operand: entry.operand || '=',
688
+ }
689
+ }
690
+
691
+ const children = Array.isArray(parsed)
692
+ ? parsed.map(toNode).filter(Boolean)
693
+ : Object.entries(parsed).map(([source, destination]) =>
694
+ toNode({ source, destination, operand: '=' })
695
+ ).filter(Boolean)
696
+
697
+ if (children.length === 0) return null
698
+ // A single root group is already the tree; wrapping it in another and-group
699
+ // would only add a redundant level.
700
+ if (children.length === 1 && children[0].type === 'group') return children[0]
701
+ return { type: 'group', operator: 'and', children }
702
+ }
703
+
704
+ const evaluateFilterTree = (node, testCondition) => {
705
+ if (!node) return true
706
+ if (node.type === 'group') {
707
+ if (node.children.length === 0) return true
708
+ return node.operator === 'or'
709
+ ? node.children.some((child) => evaluateFilterTree(child, testCondition))
710
+ : node.children.every((child) => evaluateFilterTree(child, testCondition))
711
+ }
712
+ return testCondition(node)
713
+ }
714
+
715
+ const buildFilterTreeClause = (node, buildCondition) => {
716
+ if (!node) return null
717
+ if (node.type === 'group') {
718
+ const parts = []
719
+ for (const child of node.children) {
720
+ const clause = buildFilterTreeClause(child, buildCondition)
721
+ if (clause) parts.push(clause)
722
+ }
723
+ if (parts.length === 0) return null
724
+ if (parts.length === 1) return parts[0]
725
+ return '(' + parts.join(node.operator === 'or' ? ' OR ' : ' AND ') + ')'
726
+ }
727
+ return buildCondition(node)
728
+ }
729
+
730
+ const flattenFilterConditions = (node) => {
731
+ if (!node) return []
732
+ if (node.type === 'group') {
733
+ return node.children.reduce((acc, child) => acc.concat(flattenFilterConditions(child)), [])
734
+ }
735
+ return [node]
736
+ }`
737
+ }
738
+
599
739
  export const generateSortFilterHelperCode = (): string => {
600
740
  return `function getNestedValue(obj, path) {
601
741
  if (!obj || typeof obj !== 'object') return undefined
@@ -801,13 +941,14 @@ export const hasUnresolvableDynamicParams = (
801
941
  }
802
942
 
803
943
  if (param.type === 'static' && Array.isArray(param.content)) {
804
- const hasDynamicDestinations = param.content.some(
944
+ const leafConditions = collectFilterLeafConditions(param.content)
945
+ const hasDynamicDestinations = leafConditions.some(
805
946
  (item: any) =>
806
947
  item && typeof item === 'object' && ASTUtils.isUIDLDynamicReference(item?.destination)
807
948
  )
808
949
 
809
950
  if (hasDynamicDestinations && dynamicRouteAttr) {
810
- const allResolvable = param.content.every((item: any) => {
951
+ const allResolvable = leafConditions.every((item: any) => {
811
952
  if (!item || typeof item !== 'object') {
812
953
  return true
813
954
  }
@@ -901,7 +1042,7 @@ export const extractDataSourceIntoGetStaticProps = (
901
1042
  dependencies: Record<string, any>,
902
1043
  dynamicRouteAttr?: string,
903
1044
  folderPath?: string[],
904
- transformOptions: EcommerceProductTransformOptions = {}
1045
+ transformOptions: EntityTransformOptions = {}
905
1046
  ): { success: boolean; chunk?: any } => {
906
1047
  try {
907
1048
  // Validate node content
@@ -1165,9 +1306,9 @@ export const extractDataSourceIntoGetStaticProps = (
1165
1306
  // tslint:disable-next-line:no-any
1166
1307
  const resourceParamsForFilters = (node.content as any).resource?.params || {}
1167
1308
  const nodeFilters = resourceParamsForFilters.filters?.content || []
1168
- const hasDynamicFilterDestinations =
1169
- Array.isArray(nodeFilters) &&
1170
- nodeFilters.some((f: any) => ASTUtils.isUIDLDynamicReference(f?.destination))
1309
+ const hasDynamicFilterDestinations = collectFilterLeafConditions(nodeFilters).some((f: any) =>
1310
+ ASTUtils.isUIDLDynamicReference(f?.destination)
1311
+ )
1171
1312
 
1172
1313
  // Update all target JSX nodes with initialData (only those that don't already have it)
1173
1314
  for (const jsxNode of nodesToUpdate) {
@@ -1291,9 +1432,12 @@ export const extractDataSourceIntoGetStaticProps = (
1291
1432
  if (key === 'sorts' || key === 'filters' || key === 'queryColumns') {
1292
1433
  let itemsToProcess = validItems
1293
1434
  if (key === 'filters') {
1294
- // Keep filter items that are either static or route-resolvable (via dynamicRouteAttr)
1295
- // Filter out items with unresolvable dynamic destinations
1296
- itemsToProcess = validItems.filter((item: any) => {
1435
+ // Keep filter conditions that are either static or route-resolvable
1436
+ // (via dynamicRouteAttr) and drop the ones with unresolvable dynamic
1437
+ // destinations. Pruned through the tree so a condition nested in a
1438
+ // group is judged on its own destination, and an `or` group keeps
1439
+ // its shape on the way to the data source.
1440
+ itemsToProcess = pruneFilterTree(validItems, (item: any) => {
1297
1441
  if (!ASTUtils.isUIDLDynamicReference(item?.destination)) {
1298
1442
  return true
1299
1443
  }
@@ -1310,36 +1454,49 @@ export const extractDataSourceIntoGetStaticProps = (
1310
1454
  const hasRouteResolvableFilters =
1311
1455
  key === 'filters' &&
1312
1456
  dynamicRouteAttr &&
1313
- itemsToProcess.some(
1457
+ collectFilterLeafConditions(itemsToProcess).some(
1314
1458
  (item: any) =>
1315
1459
  item &&
1316
1460
  typeof item === 'object' &&
1317
1461
  ASTUtils.isUIDLDynamicReference(item?.destination)
1318
1462
  )
1319
1463
 
1320
- const arrayExpr = types.arrayExpression(
1321
- itemsToProcess.map((item: any) => {
1322
- if (typeof item === 'string') {
1323
- return types.stringLiteral(item)
1324
- }
1325
- if (typeof item === 'number') {
1326
- return types.numericLiteral(item)
1327
- }
1328
- if (typeof item === 'boolean') {
1329
- return types.booleanLiteral(item)
1330
- }
1331
- if (typeof item === 'object' && item !== null) {
1332
- if (
1333
- hasRouteResolvableFilters &&
1334
- ASTUtils.isUIDLDynamicReference(item?.destination)
1335
- ) {
1336
- return buildFilterObjectAST(item, dynamicRouteAttr)
1337
- }
1338
- return ASTUtils.objectToObjectExpression(item)
1339
- }
1464
+ // Recurses through groups so a route-resolvable destination nested
1465
+ // inside one still gets its `context.params.*` AST instead of being
1466
+ // emitted as a raw UIDL dynamic-reference object.
1467
+ const buildFilterEntryAST = (item: any): types.Expression => {
1468
+ if (typeof item === 'string') {
1469
+ return types.stringLiteral(item)
1470
+ }
1471
+ if (typeof item === 'number') {
1472
+ return types.numericLiteral(item)
1473
+ }
1474
+ if (typeof item === 'boolean') {
1475
+ return types.booleanLiteral(item)
1476
+ }
1477
+ if (typeof item !== 'object' || item === null) {
1340
1478
  return types.nullLiteral()
1341
- })
1342
- )
1479
+ }
1480
+ if (item.type === 'group' && Array.isArray(item.children)) {
1481
+ return types.objectExpression([
1482
+ types.objectProperty(types.identifier('type'), types.stringLiteral('group')),
1483
+ types.objectProperty(
1484
+ types.identifier('operator'),
1485
+ types.stringLiteral(item.operator === 'or' ? 'or' : 'and')
1486
+ ),
1487
+ types.objectProperty(
1488
+ types.identifier('children'),
1489
+ types.arrayExpression(item.children.map(buildFilterEntryAST))
1490
+ ),
1491
+ ])
1492
+ }
1493
+ if (hasRouteResolvableFilters && ASTUtils.isUIDLDynamicReference(item?.destination)) {
1494
+ return buildFilterObjectAST(item, dynamicRouteAttr)
1495
+ }
1496
+ return ASTUtils.objectToObjectExpression(item)
1497
+ }
1498
+
1499
+ const arrayExpr = types.arrayExpression(itemsToProcess.map(buildFilterEntryAST))
1343
1500
  // Wrap in JSON.stringify()
1344
1501
  astValue = types.callExpression(
1345
1502
  types.memberExpression(types.identifier('JSON'), types.identifier('stringify')),