@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.
- package/__tests__/array-mapper-cache-wiring.test.ts +11 -0
- package/__tests__/data-source-request-locale.test.ts +313 -0
- package/__tests__/gated-data-provider.test.ts +88 -0
- package/__tests__/localized-columns.test.ts +217 -0
- package/__tests__/numbered-pagination.test.ts +15 -0
- package/__tests__/product-filter-fields.ts +230 -0
- package/__tests__/product-filter-sql.fixture.json +7 -0
- package/__tests__/refetch-loading-state.test.ts +23 -0
- package/__tests__/search-clear-button.test.ts +122 -0
- package/dist/cjs/content-localization.d.ts +37 -0
- package/dist/cjs/content-localization.d.ts.map +1 -0
- package/dist/cjs/content-localization.js +29 -0
- package/dist/cjs/content-localization.js.map +1 -0
- package/dist/cjs/count-effect.d.ts +11 -1
- package/dist/cjs/count-effect.d.ts.map +1 -1
- package/dist/cjs/count-effect.js +30 -2
- package/dist/cjs/count-effect.js.map +1 -1
- package/dist/cjs/fetchers/airtable.d.ts.map +1 -1
- package/dist/cjs/fetchers/airtable.js +1 -1
- package/dist/cjs/fetchers/airtable.js.map +1 -1
- package/dist/cjs/fetchers/clickhouse.d.ts.map +1 -1
- package/dist/cjs/fetchers/clickhouse.js +1 -1
- package/dist/cjs/fetchers/clickhouse.js.map +1 -1
- package/dist/cjs/fetchers/firestore.d.ts.map +1 -1
- package/dist/cjs/fetchers/firestore.js +1 -1
- package/dist/cjs/fetchers/firestore.js.map +1 -1
- package/dist/cjs/fetchers/mariadb.d.ts.map +1 -1
- package/dist/cjs/fetchers/mariadb.js +1 -1
- package/dist/cjs/fetchers/mariadb.js.map +1 -1
- package/dist/cjs/fetchers/mongodb.d.ts.map +1 -1
- package/dist/cjs/fetchers/mongodb.js +1 -1
- package/dist/cjs/fetchers/mongodb.js.map +1 -1
- package/dist/cjs/fetchers/mysql.d.ts.map +1 -1
- package/dist/cjs/fetchers/mysql.js +1 -1
- package/dist/cjs/fetchers/mysql.js.map +1 -1
- package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
- package/dist/cjs/fetchers/postgresql.js +2 -1
- package/dist/cjs/fetchers/postgresql.js.map +1 -1
- package/dist/cjs/fetchers/redshift.d.ts.map +1 -1
- package/dist/cjs/fetchers/redshift.js +1 -1
- package/dist/cjs/fetchers/redshift.js.map +1 -1
- package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
- package/dist/cjs/fetchers/teleport.js +10 -3
- package/dist/cjs/fetchers/teleport.js.map +1 -1
- package/dist/cjs/fetchers/turso.d.ts.map +1 -1
- package/dist/cjs/fetchers/turso.js +1 -1
- package/dist/cjs/fetchers/turso.js.map +1 -1
- package/dist/cjs/index.d.ts +5 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +60 -8
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/loading-state.d.ts +16 -1
- package/dist/cjs/loading-state.d.ts.map +1 -1
- package/dist/cjs/loading-state.js +69 -3
- package/dist/cjs/loading-state.js.map +1 -1
- package/dist/cjs/localized-columns.d.ts +56 -0
- package/dist/cjs/localized-columns.d.ts.map +1 -0
- package/dist/cjs/localized-columns.js +124 -0
- package/dist/cjs/localized-columns.js.map +1 -0
- package/dist/cjs/pagination-plugin.d.ts.map +1 -1
- package/dist/cjs/pagination-plugin.js +123 -45
- package/dist/cjs/pagination-plugin.js.map +1 -1
- package/dist/cjs/product-filter-fields.d.ts +87 -0
- package/dist/cjs/product-filter-fields.d.ts.map +1 -0
- package/dist/cjs/product-filter-fields.js +161 -0
- package/dist/cjs/product-filter-fields.js.map +1 -0
- package/dist/cjs/request-locale.d.ts +49 -0
- package/dist/cjs/request-locale.d.ts.map +1 -0
- package/dist/cjs/request-locale.js +130 -0
- package/dist/cjs/request-locale.js.map +1 -0
- package/dist/cjs/search-clear.d.ts +31 -0
- package/dist/cjs/search-clear.d.ts.map +1 -0
- package/dist/cjs/search-clear.js +103 -0
- package/dist/cjs/search-clear.js.map +1 -0
- package/dist/cjs/transformations/index.d.ts +16 -2
- package/dist/cjs/transformations/index.d.ts.map +1 -1
- package/dist/cjs/transformations/index.js +14 -2
- package/dist/cjs/transformations/index.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/utils.d.ts +11 -1
- package/dist/cjs/utils.d.ts.map +1 -1
- package/dist/cjs/utils.js +59 -15
- package/dist/cjs/utils.js.map +1 -1
- package/dist/esm/content-localization.d.ts +37 -0
- package/dist/esm/content-localization.d.ts.map +1 -0
- package/dist/esm/content-localization.js +24 -0
- package/dist/esm/content-localization.js.map +1 -0
- package/dist/esm/count-effect.d.ts +11 -1
- package/dist/esm/count-effect.d.ts.map +1 -1
- package/dist/esm/count-effect.js +28 -1
- package/dist/esm/count-effect.js.map +1 -1
- package/dist/esm/fetchers/airtable.d.ts.map +1 -1
- package/dist/esm/fetchers/airtable.js +1 -1
- package/dist/esm/fetchers/airtable.js.map +1 -1
- package/dist/esm/fetchers/clickhouse.d.ts.map +1 -1
- package/dist/esm/fetchers/clickhouse.js +1 -1
- package/dist/esm/fetchers/clickhouse.js.map +1 -1
- package/dist/esm/fetchers/firestore.d.ts.map +1 -1
- package/dist/esm/fetchers/firestore.js +1 -1
- package/dist/esm/fetchers/firestore.js.map +1 -1
- package/dist/esm/fetchers/mariadb.d.ts.map +1 -1
- package/dist/esm/fetchers/mariadb.js +1 -1
- package/dist/esm/fetchers/mariadb.js.map +1 -1
- package/dist/esm/fetchers/mongodb.d.ts.map +1 -1
- package/dist/esm/fetchers/mongodb.js +1 -1
- package/dist/esm/fetchers/mongodb.js.map +1 -1
- package/dist/esm/fetchers/mysql.d.ts.map +1 -1
- package/dist/esm/fetchers/mysql.js +1 -1
- package/dist/esm/fetchers/mysql.js.map +1 -1
- package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
- package/dist/esm/fetchers/postgresql.js +2 -1
- package/dist/esm/fetchers/postgresql.js.map +1 -1
- package/dist/esm/fetchers/redshift.d.ts.map +1 -1
- package/dist/esm/fetchers/redshift.js +1 -1
- package/dist/esm/fetchers/redshift.js.map +1 -1
- package/dist/esm/fetchers/teleport.d.ts.map +1 -1
- package/dist/esm/fetchers/teleport.js +10 -3
- package/dist/esm/fetchers/teleport.js.map +1 -1
- package/dist/esm/fetchers/turso.d.ts.map +1 -1
- package/dist/esm/fetchers/turso.js +1 -1
- package/dist/esm/fetchers/turso.js.map +1 -1
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +55 -7
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/loading-state.d.ts +16 -1
- package/dist/esm/loading-state.d.ts.map +1 -1
- package/dist/esm/loading-state.js +69 -3
- package/dist/esm/loading-state.js.map +1 -1
- package/dist/esm/localized-columns.d.ts +56 -0
- package/dist/esm/localized-columns.d.ts.map +1 -0
- package/dist/esm/localized-columns.js +119 -0
- package/dist/esm/localized-columns.js.map +1 -0
- package/dist/esm/pagination-plugin.d.ts.map +1 -1
- package/dist/esm/pagination-plugin.js +124 -46
- package/dist/esm/pagination-plugin.js.map +1 -1
- package/dist/esm/product-filter-fields.d.ts +87 -0
- package/dist/esm/product-filter-fields.d.ts.map +1 -0
- package/dist/esm/product-filter-fields.js +154 -0
- package/dist/esm/product-filter-fields.js.map +1 -0
- package/dist/esm/request-locale.d.ts +49 -0
- package/dist/esm/request-locale.d.ts.map +1 -0
- package/dist/esm/request-locale.js +98 -0
- package/dist/esm/request-locale.js.map +1 -0
- package/dist/esm/search-clear.d.ts +31 -0
- package/dist/esm/search-clear.d.ts.map +1 -0
- package/dist/esm/search-clear.js +74 -0
- package/dist/esm/search-clear.js.map +1 -0
- package/dist/esm/transformations/index.d.ts +16 -2
- package/dist/esm/transformations/index.d.ts.map +1 -1
- package/dist/esm/transformations/index.js +14 -2
- package/dist/esm/transformations/index.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/utils.d.ts +11 -1
- package/dist/esm/utils.d.ts.map +1 -1
- package/dist/esm/utils.js +59 -15
- package/dist/esm/utils.js.map +1 -1
- package/package.json +5 -5
- package/src/content-localization.ts +53 -0
- package/src/count-effect.ts +53 -2
- package/src/fetchers/airtable.ts +4 -2
- package/src/fetchers/clickhouse.ts +4 -2
- package/src/fetchers/firestore.ts +4 -2
- package/src/fetchers/mariadb.ts +4 -2
- package/src/fetchers/mongodb.ts +4 -2
- package/src/fetchers/mysql.ts +4 -2
- package/src/fetchers/postgresql.ts +16 -2
- package/src/fetchers/redshift.ts +4 -2
- package/src/fetchers/teleport.ts +48 -9
- package/src/fetchers/turso.ts +4 -2
- package/src/index.ts +79 -11
- package/src/loading-state.ts +134 -3
- package/src/localized-columns.ts +133 -0
- package/src/pagination-plugin.ts +177 -53
- package/src/product-filter-fields.ts +173 -0
- package/src/request-locale.ts +113 -0
- package/src/search-clear.ts +102 -0
- package/src/transformations/index.ts +30 -5
- 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 = (
|
|
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 =
|
|
293
|
-
var
|
|
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
|
|
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)]
|