@teleporthq/teleport-plugin-next-data-source 0.43.66 → 0.43.67

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 (179) hide show
  1. package/__tests__/array-mapper-cache-wiring.test.ts +11 -0
  2. package/__tests__/data-source-request-locale.test.ts +313 -0
  3. package/__tests__/gated-data-provider.test.ts +88 -0
  4. package/__tests__/localized-columns.test.ts +217 -0
  5. package/__tests__/numbered-pagination.test.ts +15 -0
  6. package/__tests__/product-filter-fields.ts +230 -0
  7. package/__tests__/product-filter-sql.fixture.json +7 -0
  8. package/__tests__/refetch-loading-state.test.ts +23 -0
  9. package/__tests__/search-clear-button.test.ts +122 -0
  10. package/dist/cjs/content-localization.d.ts +37 -0
  11. package/dist/cjs/content-localization.d.ts.map +1 -0
  12. package/dist/cjs/content-localization.js +29 -0
  13. package/dist/cjs/content-localization.js.map +1 -0
  14. package/dist/cjs/count-effect.d.ts +11 -1
  15. package/dist/cjs/count-effect.d.ts.map +1 -1
  16. package/dist/cjs/count-effect.js +30 -2
  17. package/dist/cjs/count-effect.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/firestore.d.ts.map +1 -1
  25. package/dist/cjs/fetchers/firestore.js +1 -1
  26. package/dist/cjs/fetchers/firestore.js.map +1 -1
  27. package/dist/cjs/fetchers/mariadb.d.ts.map +1 -1
  28. package/dist/cjs/fetchers/mariadb.js +1 -1
  29. package/dist/cjs/fetchers/mariadb.js.map +1 -1
  30. package/dist/cjs/fetchers/mongodb.d.ts.map +1 -1
  31. package/dist/cjs/fetchers/mongodb.js +1 -1
  32. package/dist/cjs/fetchers/mongodb.js.map +1 -1
  33. package/dist/cjs/fetchers/mysql.d.ts.map +1 -1
  34. package/dist/cjs/fetchers/mysql.js +1 -1
  35. package/dist/cjs/fetchers/mysql.js.map +1 -1
  36. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  37. package/dist/cjs/fetchers/postgresql.js +2 -1
  38. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  39. package/dist/cjs/fetchers/redshift.d.ts.map +1 -1
  40. package/dist/cjs/fetchers/redshift.js +1 -1
  41. package/dist/cjs/fetchers/redshift.js.map +1 -1
  42. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  43. package/dist/cjs/fetchers/teleport.js +10 -3
  44. package/dist/cjs/fetchers/teleport.js.map +1 -1
  45. package/dist/cjs/fetchers/turso.d.ts.map +1 -1
  46. package/dist/cjs/fetchers/turso.js +1 -1
  47. package/dist/cjs/fetchers/turso.js.map +1 -1
  48. package/dist/cjs/index.d.ts +5 -0
  49. package/dist/cjs/index.d.ts.map +1 -1
  50. package/dist/cjs/index.js +60 -8
  51. package/dist/cjs/index.js.map +1 -1
  52. package/dist/cjs/loading-state.d.ts +16 -1
  53. package/dist/cjs/loading-state.d.ts.map +1 -1
  54. package/dist/cjs/loading-state.js +69 -3
  55. package/dist/cjs/loading-state.js.map +1 -1
  56. package/dist/cjs/localized-columns.d.ts +56 -0
  57. package/dist/cjs/localized-columns.d.ts.map +1 -0
  58. package/dist/cjs/localized-columns.js +124 -0
  59. package/dist/cjs/localized-columns.js.map +1 -0
  60. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  61. package/dist/cjs/pagination-plugin.js +123 -45
  62. package/dist/cjs/pagination-plugin.js.map +1 -1
  63. package/dist/cjs/product-filter-fields.d.ts +87 -0
  64. package/dist/cjs/product-filter-fields.d.ts.map +1 -0
  65. package/dist/cjs/product-filter-fields.js +161 -0
  66. package/dist/cjs/product-filter-fields.js.map +1 -0
  67. package/dist/cjs/request-locale.d.ts +49 -0
  68. package/dist/cjs/request-locale.d.ts.map +1 -0
  69. package/dist/cjs/request-locale.js +130 -0
  70. package/dist/cjs/request-locale.js.map +1 -0
  71. package/dist/cjs/search-clear.d.ts +31 -0
  72. package/dist/cjs/search-clear.d.ts.map +1 -0
  73. package/dist/cjs/search-clear.js +103 -0
  74. package/dist/cjs/search-clear.js.map +1 -0
  75. package/dist/cjs/transformations/index.d.ts +16 -2
  76. package/dist/cjs/transformations/index.d.ts.map +1 -1
  77. package/dist/cjs/transformations/index.js +14 -2
  78. package/dist/cjs/transformations/index.js.map +1 -1
  79. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  80. package/dist/cjs/utils.d.ts +11 -1
  81. package/dist/cjs/utils.d.ts.map +1 -1
  82. package/dist/cjs/utils.js +59 -15
  83. package/dist/cjs/utils.js.map +1 -1
  84. package/dist/esm/content-localization.d.ts +37 -0
  85. package/dist/esm/content-localization.d.ts.map +1 -0
  86. package/dist/esm/content-localization.js +24 -0
  87. package/dist/esm/content-localization.js.map +1 -0
  88. package/dist/esm/count-effect.d.ts +11 -1
  89. package/dist/esm/count-effect.d.ts.map +1 -1
  90. package/dist/esm/count-effect.js +28 -1
  91. package/dist/esm/count-effect.js.map +1 -1
  92. package/dist/esm/fetchers/airtable.d.ts.map +1 -1
  93. package/dist/esm/fetchers/airtable.js +1 -1
  94. package/dist/esm/fetchers/airtable.js.map +1 -1
  95. package/dist/esm/fetchers/clickhouse.d.ts.map +1 -1
  96. package/dist/esm/fetchers/clickhouse.js +1 -1
  97. package/dist/esm/fetchers/clickhouse.js.map +1 -1
  98. package/dist/esm/fetchers/firestore.d.ts.map +1 -1
  99. package/dist/esm/fetchers/firestore.js +1 -1
  100. package/dist/esm/fetchers/firestore.js.map +1 -1
  101. package/dist/esm/fetchers/mariadb.d.ts.map +1 -1
  102. package/dist/esm/fetchers/mariadb.js +1 -1
  103. package/dist/esm/fetchers/mariadb.js.map +1 -1
  104. package/dist/esm/fetchers/mongodb.d.ts.map +1 -1
  105. package/dist/esm/fetchers/mongodb.js +1 -1
  106. package/dist/esm/fetchers/mongodb.js.map +1 -1
  107. package/dist/esm/fetchers/mysql.d.ts.map +1 -1
  108. package/dist/esm/fetchers/mysql.js +1 -1
  109. package/dist/esm/fetchers/mysql.js.map +1 -1
  110. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  111. package/dist/esm/fetchers/postgresql.js +2 -1
  112. package/dist/esm/fetchers/postgresql.js.map +1 -1
  113. package/dist/esm/fetchers/redshift.d.ts.map +1 -1
  114. package/dist/esm/fetchers/redshift.js +1 -1
  115. package/dist/esm/fetchers/redshift.js.map +1 -1
  116. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  117. package/dist/esm/fetchers/teleport.js +10 -3
  118. package/dist/esm/fetchers/teleport.js.map +1 -1
  119. package/dist/esm/fetchers/turso.d.ts.map +1 -1
  120. package/dist/esm/fetchers/turso.js +1 -1
  121. package/dist/esm/fetchers/turso.js.map +1 -1
  122. package/dist/esm/index.d.ts +5 -0
  123. package/dist/esm/index.d.ts.map +1 -1
  124. package/dist/esm/index.js +55 -7
  125. package/dist/esm/index.js.map +1 -1
  126. package/dist/esm/loading-state.d.ts +16 -1
  127. package/dist/esm/loading-state.d.ts.map +1 -1
  128. package/dist/esm/loading-state.js +69 -3
  129. package/dist/esm/loading-state.js.map +1 -1
  130. package/dist/esm/localized-columns.d.ts +56 -0
  131. package/dist/esm/localized-columns.d.ts.map +1 -0
  132. package/dist/esm/localized-columns.js +119 -0
  133. package/dist/esm/localized-columns.js.map +1 -0
  134. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  135. package/dist/esm/pagination-plugin.js +124 -46
  136. package/dist/esm/pagination-plugin.js.map +1 -1
  137. package/dist/esm/product-filter-fields.d.ts +87 -0
  138. package/dist/esm/product-filter-fields.d.ts.map +1 -0
  139. package/dist/esm/product-filter-fields.js +154 -0
  140. package/dist/esm/product-filter-fields.js.map +1 -0
  141. package/dist/esm/request-locale.d.ts +49 -0
  142. package/dist/esm/request-locale.d.ts.map +1 -0
  143. package/dist/esm/request-locale.js +98 -0
  144. package/dist/esm/request-locale.js.map +1 -0
  145. package/dist/esm/search-clear.d.ts +31 -0
  146. package/dist/esm/search-clear.d.ts.map +1 -0
  147. package/dist/esm/search-clear.js +74 -0
  148. package/dist/esm/search-clear.js.map +1 -0
  149. package/dist/esm/transformations/index.d.ts +16 -2
  150. package/dist/esm/transformations/index.d.ts.map +1 -1
  151. package/dist/esm/transformations/index.js +14 -2
  152. package/dist/esm/transformations/index.js.map +1 -1
  153. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  154. package/dist/esm/utils.d.ts +11 -1
  155. package/dist/esm/utils.d.ts.map +1 -1
  156. package/dist/esm/utils.js +59 -15
  157. package/dist/esm/utils.js.map +1 -1
  158. package/package.json +5 -5
  159. package/src/content-localization.ts +53 -0
  160. package/src/count-effect.ts +53 -2
  161. package/src/fetchers/airtable.ts +4 -2
  162. package/src/fetchers/clickhouse.ts +4 -2
  163. package/src/fetchers/firestore.ts +4 -2
  164. package/src/fetchers/mariadb.ts +4 -2
  165. package/src/fetchers/mongodb.ts +4 -2
  166. package/src/fetchers/mysql.ts +4 -2
  167. package/src/fetchers/postgresql.ts +16 -2
  168. package/src/fetchers/redshift.ts +4 -2
  169. package/src/fetchers/teleport.ts +48 -9
  170. package/src/fetchers/turso.ts +4 -2
  171. package/src/index.ts +79 -11
  172. package/src/loading-state.ts +134 -3
  173. package/src/localized-columns.ts +133 -0
  174. package/src/pagination-plugin.ts +177 -53
  175. package/src/product-filter-fields.ts +173 -0
  176. package/src/request-locale.ts +113 -0
  177. package/src/search-clear.ts +102 -0
  178. package/src/transformations/index.ts +30 -5
  179. package/src/utils.ts +83 -15
@@ -0,0 +1,113 @@
1
+ import * as types from '@babel/types'
2
+ import type { UIDLDependency } from '@teleporthq/teleport-types'
3
+
4
+ /**
5
+ * How a generated data-source fetch says which language it is for.
6
+ *
7
+ * A multi-language project stores a translatable column once per language
8
+ * (`name`, `es_name`, …) and the generated fetcher resolves each row against
9
+ * ONE of them (see `transformations/` and `localized-columns.ts`). Nothing
10
+ * about the API route knows the page it serves — an API route has no locale
11
+ * segment in its URL — so the page tells it, through this query parameter:
12
+ *
13
+ * - a client-side fetch (`DataProvider` `params`) sends `router.locale`;
14
+ * - a build-time fetch inside `getStaticProps` sends `context.locale`.
15
+ *
16
+ * Both are what Next.js resolved from the `/es/...` route, so the rows a page
17
+ * renders are in the language the page is in. The same name the details-page
18
+ * fetch (`teleport-plugin-next-static-props`) has always sent.
19
+ */
20
+ export const REQUEST_LOCALE_PARAM = 'locale'
21
+
22
+ /** The dependency behind the `router` every client-side locale read closes over. */
23
+ const USE_ROUTER_DEPENDENCY: UIDLDependency = {
24
+ type: 'library',
25
+ path: 'next/router',
26
+ version: '^12.1.10',
27
+ meta: { namedImport: true },
28
+ }
29
+
30
+ /**
31
+ * `router?.locale` — the client's current locale. Optional-chained like every
32
+ * other generated `router` read: `useRouter()` answers `null` during a static
33
+ * export's first paint.
34
+ */
35
+ export const buildClientLocaleExpression = (): types.Expression =>
36
+ types.optionalMemberExpression(
37
+ types.identifier('router'),
38
+ types.identifier('locale'),
39
+ false,
40
+ true
41
+ )
42
+
43
+ /** `locale: router?.locale` — the param of a client-side fetch. */
44
+ export const buildClientLocaleParam = (): types.ObjectProperty =>
45
+ types.objectProperty(types.identifier(REQUEST_LOCALE_PARAM), buildClientLocaleExpression())
46
+
47
+ /** `locale: context.locale` — the param of a fetch inside getStaticProps. */
48
+ export const buildServerLocaleParam = (): types.ObjectProperty =>
49
+ types.objectProperty(
50
+ types.identifier(REQUEST_LOCALE_PARAM),
51
+ types.memberExpression(types.identifier('context'), types.identifier('locale'))
52
+ )
53
+
54
+ /** Whether an object literal already carries the locale param. */
55
+ export const hasLocaleParam = (properties: types.ObjectExpression['properties']): boolean =>
56
+ properties.some(
57
+ (property) =>
58
+ property.type === 'ObjectProperty' &&
59
+ ((property.key.type === 'Identifier' && property.key.name === REQUEST_LOCALE_PARAM) ||
60
+ (property.key.type === 'StringLiteral' && property.key.value === REQUEST_LOCALE_PARAM))
61
+ )
62
+
63
+ /**
64
+ * Adds the client locale to a fetch's params, once, and to the `useMemo` deps
65
+ * that must rebuild them: a locale switch is a client-side transition on the
66
+ * SAME page instance, so only a dependency change refetches the rows in the
67
+ * new language.
68
+ */
69
+ export const appendClientLocaleParam = (
70
+ paramsProps: types.ObjectExpression['properties'],
71
+ memoDeps: types.Expression[]
72
+ ): void => {
73
+ if (!hasLocaleParam(paramsProps)) {
74
+ paramsProps.push(buildClientLocaleParam())
75
+ }
76
+ memoDeps.push(buildClientLocaleExpression())
77
+ }
78
+
79
+ /** Any declaration of `router` — a second one, however it is initialised, is a syntax error. */
80
+ const declaresRouter = (statement: types.Statement): boolean =>
81
+ statement.type === 'VariableDeclaration' &&
82
+ statement.declarations.some(
83
+ (declaration) => declaration.id.type === 'Identifier' && declaration.id.name === 'router'
84
+ )
85
+
86
+ /**
87
+ * Puts `const router = useRouter()` (and its import) in scope, once.
88
+ *
89
+ * Idempotent with every sibling plugin that declares the same hook — the i18n
90
+ * locale mapper, the URL search-params plugin and the base component plugin
91
+ * all guard on the same identifier, so whichever runs first wins and the rest
92
+ * find it in place. Unshifted to the top of the body: nothing reads it before
93
+ * an effect runs, and hooks must not sit below a conditional return.
94
+ */
95
+ export const ensureRouterDeclaration = (
96
+ body: types.Statement[],
97
+ dependencies: Record<string, UIDLDependency>
98
+ ): void => {
99
+ if (!dependencies.useRouter) {
100
+ dependencies.useRouter = { ...USE_ROUTER_DEPENDENCY }
101
+ }
102
+ if (body.some(declaresRouter)) {
103
+ return
104
+ }
105
+ body.unshift(
106
+ types.variableDeclaration('const', [
107
+ types.variableDeclarator(
108
+ types.identifier('router'),
109
+ types.callExpression(types.identifier('useRouter'), [])
110
+ ),
111
+ ])
112
+ )
113
+ }
@@ -0,0 +1,102 @@
1
+ import * as types from '@babel/types'
2
+ import { setJSXExpressionAttribute } from './pagination-controls'
3
+
4
+ /**
5
+ * The "clear search" button a listing's search field carries — marked
6
+ * `data-tq-search-clear` by the editor (`listing-search-field.ts`), the same
7
+ * marker the canvas renderer acts on. The search keyword is the mapper's own
8
+ * React state, which no authored workflow can reach, so the generator wires
9
+ * the button here: a click empties the query, and the button renders only
10
+ * while the query is non-empty.
11
+ *
12
+ * A button belongs to the search input it shares a parent with — the field
13
+ * wrapper — never to "the next search-enabled mapper": a page with two lists
14
+ * and one clear button would otherwise wire it to the wrong list.
15
+ */
16
+
17
+ export const SEARCH_CLEAR_ATTR = 'data-tq-search-clear'
18
+
19
+ // tslint:disable:no-any
20
+
21
+ export interface SearchClearButton {
22
+ node: any
23
+ parent: any
24
+ index: number
25
+ }
26
+
27
+ function hasSearchClearMarker(node: any): boolean {
28
+ return !!node?.openingElement?.attributes?.some(
29
+ (attr: any) => attr.type === 'JSXAttribute' && attr.name?.name === SEARCH_CLEAR_ATTR
30
+ )
31
+ }
32
+
33
+ /** Every marked button under `root`, with the JSX parent and child index it sits at. */
34
+ export function findAllSearchClearButtonsInJSX(root: any): SearchClearButton[] {
35
+ const results: SearchClearButton[] = []
36
+ const traverse = (node: any): void => {
37
+ if (!node) {
38
+ return
39
+ }
40
+ if (node.type === 'JSXElement' && Array.isArray(node.children)) {
41
+ node.children.forEach((child: any, index: number) => {
42
+ if (child?.type === 'JSXElement' && hasSearchClearMarker(child)) {
43
+ results.push({ node: child, parent: node, index })
44
+ }
45
+ traverse(child)
46
+ })
47
+ return
48
+ }
49
+ if (node.body) {
50
+ if (Array.isArray(node.body)) {
51
+ node.body.forEach((statement: any) => traverse(statement))
52
+ } else {
53
+ traverse(node.body)
54
+ }
55
+ }
56
+ if (node.consequent) {
57
+ traverse(node.consequent)
58
+ }
59
+ if (node.alternate) {
60
+ traverse(node.alternate)
61
+ }
62
+ if (node.expression) {
63
+ traverse(node.expression)
64
+ }
65
+ if (node.argument) {
66
+ traverse(node.argument)
67
+ }
68
+ }
69
+ traverse(root)
70
+ return results
71
+ }
72
+
73
+ /** The search input the button shares its parent with, or null. */
74
+ export function findSiblingSearchInput(button: SearchClearButton, searchInputs: any[]): any | null {
75
+ const siblings: any[] = button.parent.children ?? []
76
+ return siblings.find((sibling) => searchInputs.includes(sibling)) ?? null
77
+ }
78
+
79
+ /**
80
+ * Wires one button: `onClick` empties the query, and the element is replaced
81
+ * in its parent by `{query ? <button…/> : null}`.
82
+ */
83
+ export function wireSearchClearButton(
84
+ button: SearchClearButton,
85
+ vars: { searchQueryVar: string; setSearchQueryVar: string }
86
+ ): void {
87
+ setJSXExpressionAttribute(
88
+ button.node,
89
+ 'onClick',
90
+ types.arrowFunctionExpression(
91
+ [],
92
+ types.callExpression(types.identifier(vars.setSearchQueryVar), [types.stringLiteral('')])
93
+ )
94
+ )
95
+ button.parent.children[button.index] = types.jsxExpressionContainer(
96
+ types.conditionalExpression(
97
+ types.identifier(vars.searchQueryVar),
98
+ button.node,
99
+ types.nullLiteral()
100
+ )
101
+ )
102
+ }
@@ -7,6 +7,12 @@ import {
7
7
  generateEcommerceProductTransformationCode,
8
8
  type EcommerceProductTransformOptions,
9
9
  } from './ecommerce-product'
10
+ import { REQUEST_LOCALE_PARAM } from '../request-locale'
11
+ import {
12
+ resolveContentLocalization,
13
+ type ContentLocalization,
14
+ type LocalizedProjectOptions,
15
+ } from '../content-localization'
10
16
 
11
17
  export type { EcommerceProductTransformOptions }
12
18
 
@@ -19,6 +25,13 @@ export type { EcommerceProductTransformOptions }
19
25
  export interface EntityTransformOptions extends EcommerceProductTransformOptions {
20
26
  /** Blog post-category taxonomy — see `blogSettings.categories`. */
21
27
  blogCategories?: UIDLEcommerceCategory[]
28
+ /**
29
+ * The languages the rows are stored in — absent for a single-language
30
+ * project. The transform resolves every translatable field against the
31
+ * request's locale (`?locale=es` → `es_name`, falling back to `name`), and
32
+ * the fetcher matches and sorts translatable columns the same way.
33
+ */
34
+ localization?: ContentLocalization
22
35
  }
23
36
 
24
37
  /**
@@ -47,12 +60,14 @@ const resolveAllowBackorders = (
47
60
  * "stock never blocks a purchase" flag — see `resolveAllowBackorders`.
48
61
  */
49
62
  export const buildProductTransformOptions = (
50
- options: Pick<GeneratorOptions, 'ecommerceSettings' | 'invoiceSettings' | 'blogSettings'>
63
+ options: Pick<GeneratorOptions, 'ecommerceSettings' | 'invoiceSettings' | 'blogSettings'> &
64
+ LocalizedProjectOptions
51
65
  ): EntityTransformOptions => ({
52
66
  categories: options.ecommerceSettings?.categories,
53
67
  blogCategories: options.blogSettings?.categories,
54
68
  storefrontTaxRate: StorefrontTax.resolveStorefrontTaxRate(options.invoiceSettings),
55
69
  allowBackorders: resolveAllowBackorders(options.ecommerceSettings),
70
+ localization: resolveContentLocalization(options),
56
71
  })
57
72
 
58
73
  export type TransformationType = 'blog-post' | 'ecommerce-product' | 'custom-page' | null
@@ -165,13 +180,24 @@ export const REVIEWS_PER_PRODUCT = 5
165
180
  * Returns the transform wrapper function code that handles asset map loading
166
181
  * and calls the appropriate transformer.
167
182
  * Returns empty string if no transformation is needed.
183
+ *
184
+ * The request's `locale` (see `REQUEST_LOCALE_PARAM`) is what every
185
+ * translatable field resolves against; the MAIN locale it falls back to is a
186
+ * project constant and is baked in from `options.localization` rather than
187
+ * asked of every caller. Without a localization every row reads as before —
188
+ * the base columns.
168
189
  */
169
- export const getTransformWrapperCode = (tableName: string): string => {
190
+ export const getTransformWrapperCode = (
191
+ tableName: string,
192
+ options: Pick<EntityTransformOptions, 'localization'> = {}
193
+ ): string => {
170
194
  const type = detectTransformationType(tableName)
171
195
  if (!type) {
172
196
  return ''
173
197
  }
174
198
 
199
+ const mainLanguageLiteral = JSON.stringify(options.localization?.mainLocale ?? null)
200
+
175
201
  const transformFn =
176
202
  type === 'blog-post'
177
203
  ? 'transformBlogPosts'
@@ -289,9 +315,8 @@ async function transformRecords(records, getClientFn, reqQuery) {
289
315
  } catch (e) {
290
316
  // Asset resolution is best-effort; continue without it
291
317
  }${relatedEnrichment}${variantEnrichment}
292
- var currentLanguage = (reqQuery && reqQuery.lang) || null
293
- var mainLanguage = (reqQuery && reqQuery.mainLang) || null
294
- var options = { assetMap: assetMap, currentLanguage: currentLanguage, mainLanguage: mainLanguage${variantOption}${relatedOption} }
318
+ var currentLanguage = reqQuery && typeof reqQuery.${REQUEST_LOCALE_PARAM} === 'string' && reqQuery.${REQUEST_LOCALE_PARAM} ? reqQuery.${REQUEST_LOCALE_PARAM} : null
319
+ var options = { assetMap: assetMap, currentLanguage: currentLanguage, mainLanguage: ${mainLanguageLiteral}${variantOption}${relatedOption} }
295
320
  return ${transformFn}(records, options)
296
321
  }
297
322
  `
package/src/utils.ts CHANGED
@@ -14,6 +14,7 @@ import { GenericUtils, StringUtils } from '@teleporthq/teleport-shared'
14
14
  import { generateDataSourceFetcherWithCore } from './data-source-fetchers'
15
15
  import type { EntityTransformOptions } from './transformations'
16
16
  import { DATA_SOURCE_ISR_REVALIDATE_SECONDS } from './isr'
17
+ import { buildServerLocaleParam, hasLocaleParam, buildClientLocaleParam } from './request-locale'
17
18
 
18
19
  const VALID_DATA_SOURCE_TYPES: DataSourceType[] = [
19
20
  'rest-api',
@@ -187,18 +188,28 @@ export const generateSafeFileName = (
187
188
  return StringUtils.camelCaseToDashCase(baseName)
188
189
  }
189
190
 
191
+ /**
192
+ * Wires a `DataProvider` to a client-side fetch of its API route.
193
+ *
194
+ * In a localized project (`transformOptions.localization`) the fetch carries
195
+ * the visitor's locale: it is added to the provider's `params` — created when
196
+ * the node declares none — so it reaches the route AND keys the client cache.
197
+ * The caller puts `router` in scope when this answers `true`.
198
+ *
199
+ * @returns `true` when a provider was wired to a client fetch.
200
+ */
190
201
  export const extractDataSourceIntoNextAPIFolder = (
191
202
  node: UIDLDataSourceItemNode | UIDLDataSourceListNode,
192
203
  dataSources: Record<string, UIDLDataSource>,
193
204
  componentChunk: ChunkDefinition,
194
205
  extractedResources: GeneratorOptions['extractedResources'],
195
206
  transformOptions: EntityTransformOptions = {}
196
- ) => {
207
+ ): boolean => {
197
208
  try {
198
209
  // Validate node content structure
199
210
  const contentValidation = validateNodeContent(node.content)
200
211
  if (!contentValidation.isValid) {
201
- return
212
+ return false
202
213
  }
203
214
 
204
215
  const { resourceDefinition } = node.content
@@ -206,31 +217,31 @@ export const extractDataSourceIntoNextAPIFolder = (
206
217
  // Validate resource definition
207
218
  const resourceValidation = validateResourceDefinition(resourceDefinition)
208
219
  if (!resourceValidation.isValid) {
209
- return
220
+ return false
210
221
  }
211
222
 
212
223
  const { dataSourceId, tableName, dataSourceType } = resourceDefinition
213
224
 
214
225
  // Check if dataSources object exists
215
226
  if (!dataSources || typeof dataSources !== 'object') {
216
- return
227
+ return false
217
228
  }
218
229
 
219
230
  // Check if data source exists
220
231
  const dataSource = dataSources[dataSourceId]
221
232
  if (!dataSource) {
222
- return
233
+ return false
223
234
  }
224
235
 
225
236
  // Validate data source configuration
226
237
  const configValidation = validateDataSourceConfig(dataSource)
227
238
  if (!configValidation.isValid) {
228
- return
239
+ return false
229
240
  }
230
241
 
231
242
  // Check if component chunk has meta and nodesLookup
232
243
  if (!componentChunk.meta || !componentChunk.meta.nodesLookup) {
233
- return
244
+ return false
234
245
  }
235
246
 
236
247
  // Generate safe file name
@@ -238,7 +249,7 @@ export const extractDataSourceIntoNextAPIFolder = (
238
249
 
239
250
  // Check if file name is valid
240
251
  if (!fileName || fileName === 'unknown') {
241
- return
252
+ return false
242
253
  }
243
254
 
244
255
  // Find JSX node by searching through nodesLookup
@@ -322,12 +333,12 @@ export const extractDataSourceIntoNextAPIFolder = (
322
333
  }
323
334
 
324
335
  if (!jsxNode || jsxNode.type !== 'JSXElement') {
325
- return
336
+ return false
326
337
  }
327
338
 
328
339
  // Ensure opening element and attributes exist
329
340
  if (!jsxNode.openingElement || !Array.isArray(jsxNode.openingElement.attributes)) {
330
- return
341
+ return false
331
342
  }
332
343
 
333
344
  // Check if this node has already been processed (has fetchData attribute)
@@ -336,12 +347,16 @@ export const extractDataSourceIntoNextAPIFolder = (
336
347
  )
337
348
 
338
349
  if (existingFetchData) {
339
- return
350
+ return false
340
351
  }
341
352
 
342
353
  // Check if there are resource params
343
354
  const resourceParams = node.content.resource?.params || {}
344
- const hasParams = Object.keys(resourceParams).length > 0
355
+ const localized = transformOptions.localization !== undefined
356
+ if (localized) {
357
+ addClientLocaleToParams(jsxNode)
358
+ }
359
+ const hasParams = localized || Object.keys(resourceParams).length > 0
345
360
 
346
361
  // Build resource path - use template literal if params exist
347
362
  let resourcePath: types.StringLiteral | types.TemplateLiteral
@@ -423,7 +438,7 @@ export const extractDataSourceIntoNextAPIFolder = (
423
438
 
424
439
  // Ensure extracted resources object exists
425
440
  if (!extractedResources || typeof extractedResources !== 'object') {
426
- return
441
+ return true
427
442
  }
428
443
 
429
444
  // Check if a utils file already exists for this data source (from getStaticProps)
@@ -440,7 +455,7 @@ export default dataSourceModule.handler
440
455
  path: ['pages', 'api'],
441
456
  content: apiRouteCode,
442
457
  }
443
- return
458
+ return true
444
459
  }
445
460
 
446
461
  // Generate fetcher code for API route (exports just the handler)
@@ -453,7 +468,7 @@ export default dataSourceModule.handler
453
468
  transformOptions
454
469
  )
455
470
  } catch (error) {
456
- return
471
+ return true
457
472
  }
458
473
 
459
474
  extractedResources[`api/${fileName}`] = {
@@ -462,9 +477,41 @@ export default dataSourceModule.handler
462
477
  path: ['pages', 'api'],
463
478
  content: fetcherCode,
464
479
  }
480
+ return true
465
481
  } catch (error) {
466
482
  // Catch any unexpected errors to prevent plugin from crashing
483
+ return false
484
+ }
485
+ }
486
+
487
+ /**
488
+ * Puts `locale: router?.locale` into a provider's `params` object literal,
489
+ * creating the attribute when the node declared no params at all.
490
+ */
491
+ const addClientLocaleToParams = (jsxNode: types.JSXElement): void => {
492
+ const attrs = jsxNode.openingElement.attributes
493
+ let paramsAttr = attrs.find(
494
+ (attr): attr is types.JSXAttribute =>
495
+ attr.type === 'JSXAttribute' &&
496
+ attr.name.type === 'JSXIdentifier' &&
497
+ attr.name.name === 'params'
498
+ )
499
+ if (!paramsAttr) {
500
+ paramsAttr = types.jsxAttribute(
501
+ types.jsxIdentifier('params'),
502
+ types.jsxExpressionContainer(types.objectExpression([]))
503
+ )
504
+ attrs.push(paramsAttr)
505
+ }
506
+ const value = paramsAttr.value
507
+ if (
508
+ value?.type !== 'JSXExpressionContainer' ||
509
+ value.expression.type !== 'ObjectExpression' ||
510
+ hasLocaleParam(value.expression.properties)
511
+ ) {
512
+ return
467
513
  }
514
+ value.expression.properties.push(buildClientLocaleParam())
468
515
  }
469
516
 
470
517
  export const isEmbeddedDataSource = (dataSourceType: string): boolean => {
@@ -766,6 +813,22 @@ const buildFilterTreeClause = (node, buildCondition) => {
766
813
  return buildCondition(node)
767
814
  }
768
815
 
816
+ // The generic multi-value operand. The editor sends the destination either as
817
+ // an array or as one comma-joined string (a URL-bound state such as
818
+ // "acme,globex"); both become an array, and "in" becomes "=" so every
819
+ // fetcher's existing array branch answers with IN (...). An empty set means
820
+ // "no filter", never "match nothing".
821
+ const normalizeInOperand = (operand, value) => {
822
+ if (operand !== 'in') return { operand, value }
823
+ const entries = Array.isArray(value) ? value : [value]
824
+ const values = entries
825
+ .flatMap((entry) => (typeof entry === 'string' ? entry.split(',') : [entry]))
826
+ .map((entry) => (typeof entry === 'string' ? entry.trim() : entry))
827
+ .filter((entry) => entry !== '' && entry !== null && entry !== undefined)
828
+ if (values.length === 0) return null
829
+ return { operand: '=', value: values }
830
+ }
831
+
769
832
  const flattenFilterConditions = (node) => {
770
833
  if (!node) return []
771
834
  if (node.type === 'group') {
@@ -1588,6 +1651,11 @@ export const extractDataSourceIntoGetStaticProps = (
1588
1651
  }
1589
1652
  })
1590
1653
 
1654
+ // getStaticProps runs once per language: the prefetch is for that language.
1655
+ if (transformOptions.localization !== undefined) {
1656
+ paramsProperties.push(buildServerLocaleParam())
1657
+ }
1658
+
1591
1659
  const fetchCallExpression = types.callExpression(
1592
1660
  types.memberExpression(types.identifier(fetcherImportName), types.identifier('fetchData')),
1593
1661
  [types.objectExpression(paramsProperties)]