@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
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
FileType,
|
|
5
5
|
ChunkType,
|
|
6
6
|
UIDLComponentSEO,
|
|
7
|
+
UIDLDependency,
|
|
7
8
|
UIDLDynamicReference,
|
|
8
9
|
} from '@teleporthq/teleport-types'
|
|
9
10
|
import { UIDLUtils, StringUtils } from '@teleporthq/teleport-shared'
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
import { createNextArrayMapperPaginationPlugin } from './pagination-plugin'
|
|
17
18
|
import { buildProductTransformOptions } from './transformations'
|
|
18
19
|
import { DATA_SOURCE_ISR_REVALIDATE_SECONDS } from './isr'
|
|
20
|
+
import { ensureRouterDeclaration } from './request-locale'
|
|
19
21
|
import * as types from '@babel/types'
|
|
20
22
|
|
|
21
23
|
interface SearchConfig {
|
|
@@ -55,6 +57,41 @@ interface WrapContext {
|
|
|
55
57
|
// from user-defined or standard renderPropIdentifier values
|
|
56
58
|
const WRAPPED_DS_EXPR_PREFIX = '__dsExpr_'
|
|
57
59
|
|
|
60
|
+
/**
|
|
61
|
+
* The statements of the component function the `jsx-component` chunk declares
|
|
62
|
+
* (`const Page = (props) => { … }`), or `null` when the chunk holds another
|
|
63
|
+
* shape — a test may hand the plugin a bare element.
|
|
64
|
+
*/
|
|
65
|
+
const findComponentBody = (componentChunk: { content: unknown }): types.Statement[] | null => {
|
|
66
|
+
const content = componentChunk.content as types.Node | undefined
|
|
67
|
+
if (content?.type !== 'VariableDeclaration') {
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
const init = content.declarations[0]?.init
|
|
71
|
+
if (
|
|
72
|
+
(init?.type !== 'ArrowFunctionExpression' && init?.type !== 'FunctionExpression') ||
|
|
73
|
+
init.body.type !== 'BlockStatement'
|
|
74
|
+
) {
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
return init.body.body
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A client-side fetch in a localized project reads `router.locale` (see
|
|
82
|
+
* `extractDataSourceIntoNextAPIFolder`), so the router has to be declared in
|
|
83
|
+
* the component that fetches.
|
|
84
|
+
*/
|
|
85
|
+
const ensureRouterForClientFetches = (
|
|
86
|
+
componentChunk: { content: unknown },
|
|
87
|
+
dependencies: Record<string, UIDLDependency>
|
|
88
|
+
): void => {
|
|
89
|
+
const body = findComponentBody(componentChunk)
|
|
90
|
+
if (body) {
|
|
91
|
+
ensureRouterDeclaration(body, dependencies)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
58
95
|
function containsDataSourceDataReference(astNode: any): boolean {
|
|
59
96
|
if (!astNode || typeof astNode !== 'object') {
|
|
60
97
|
return false
|
|
@@ -490,6 +527,11 @@ export const createNextPagesDataSourcePlugin: ComponentPluginFactory<{}> = () =>
|
|
|
490
527
|
|
|
491
528
|
let getStaticPropsChunk = chunks.find((chunk) => chunk.name === 'getStaticProps')
|
|
492
529
|
|
|
530
|
+
const transformOptions = buildProductTransformOptions(options)
|
|
531
|
+
// Whether a provider on this page fetches client-side — in a localized
|
|
532
|
+
// project that fetch reads `router.locale`.
|
|
533
|
+
let wiredClientFetch = false
|
|
534
|
+
|
|
493
535
|
// Track the first data source info for wrapping dataSourceData expressions
|
|
494
536
|
let firstDataSourceInfo: DataSourceInfo | null = null
|
|
495
537
|
// Track fetcher import name for wrapped providers
|
|
@@ -609,7 +651,7 @@ export const createNextPagesDataSourcePlugin: ComponentPluginFactory<{}> = () =>
|
|
|
609
651
|
dependencies,
|
|
610
652
|
dynamicRouteAttr,
|
|
611
653
|
uidl.outputOptions?.folderPath,
|
|
612
|
-
|
|
654
|
+
transformOptions
|
|
613
655
|
)
|
|
614
656
|
|
|
615
657
|
if (result.success && result.chunk) {
|
|
@@ -641,17 +683,23 @@ export const createNextPagesDataSourcePlugin: ComponentPluginFactory<{}> = () =>
|
|
|
641
683
|
}
|
|
642
684
|
}
|
|
643
685
|
}
|
|
644
|
-
} else
|
|
686
|
+
} else if (
|
|
645
687
|
extractDataSourceIntoNextAPIFolder(
|
|
646
688
|
dataSourceNode,
|
|
647
689
|
dataSources,
|
|
648
690
|
componentChunk,
|
|
649
691
|
options.extractedResources,
|
|
650
|
-
|
|
692
|
+
transformOptions
|
|
651
693
|
)
|
|
694
|
+
) {
|
|
695
|
+
wiredClientFetch = true
|
|
652
696
|
}
|
|
653
697
|
})
|
|
654
698
|
|
|
699
|
+
if (wiredClientFetch && transformOptions.localization) {
|
|
700
|
+
ensureRouterForClientFetches(componentChunk, dependencies)
|
|
701
|
+
}
|
|
702
|
+
|
|
655
703
|
// After processing all data source nodes, wrap any element props containing dataSourceData
|
|
656
704
|
// expressions in a DataProvider
|
|
657
705
|
if (firstDataSourceInfo && componentChunk.content) {
|
|
@@ -1207,6 +1255,11 @@ export const createNextComponentDataSourcePlugin: ComponentPluginFactory<{}> = (
|
|
|
1207
1255
|
return structure
|
|
1208
1256
|
}
|
|
1209
1257
|
|
|
1258
|
+
const transformOptions = buildProductTransformOptions(options)
|
|
1259
|
+
// Whether a provider in this component fetches client-side — in a
|
|
1260
|
+
// localized project that fetch reads `router.locale`.
|
|
1261
|
+
let wiredClientFetch = false
|
|
1262
|
+
|
|
1210
1263
|
// Track the first data source info for wrapping dataSourceData expressions
|
|
1211
1264
|
let firstDataSourceInfo: DataSourceInfo | null = null
|
|
1212
1265
|
|
|
@@ -1264,15 +1317,23 @@ export const createNextComponentDataSourcePlugin: ComponentPluginFactory<{}> = (
|
|
|
1264
1317
|
}
|
|
1265
1318
|
}
|
|
1266
1319
|
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1320
|
+
if (
|
|
1321
|
+
extractDataSourceIntoNextAPIFolder(
|
|
1322
|
+
dataSourceNode,
|
|
1323
|
+
dataSources,
|
|
1324
|
+
componentChunk,
|
|
1325
|
+
options.extractedResources,
|
|
1326
|
+
transformOptions
|
|
1327
|
+
)
|
|
1328
|
+
) {
|
|
1329
|
+
wiredClientFetch = true
|
|
1330
|
+
}
|
|
1274
1331
|
})
|
|
1275
1332
|
|
|
1333
|
+
if (wiredClientFetch && transformOptions.localization) {
|
|
1334
|
+
ensureRouterForClientFetches(componentChunk, dependencies)
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1276
1337
|
// If no data source nodes were found but there are dataSources available,
|
|
1277
1338
|
// use the first available dataSource to create firstDataSourceInfo
|
|
1278
1339
|
// This handles the case where a component has element props with dataSourceData expressions
|
|
@@ -1355,7 +1416,7 @@ export const createNextComponentDataSourcePlugin: ComponentPluginFactory<{}> = (
|
|
|
1355
1416
|
dataSource,
|
|
1356
1417
|
firstDataSourceInfo.tableName,
|
|
1357
1418
|
false,
|
|
1358
|
-
|
|
1419
|
+
transformOptions
|
|
1359
1420
|
)
|
|
1360
1421
|
options.extractedResources[`utils/${fileName}`] = {
|
|
1361
1422
|
fileName,
|
|
@@ -1399,8 +1460,15 @@ export * from './utils'
|
|
|
1399
1460
|
export * from './array-mapper-pagination'
|
|
1400
1461
|
export * from './pagination-plugin'
|
|
1401
1462
|
export * from './count-fetchers'
|
|
1463
|
+
export * from './product-price-sort'
|
|
1464
|
+
export * from './product-filter-fields'
|
|
1402
1465
|
// The product transform's build-time options (category taxonomy + storefront
|
|
1403
1466
|
// tax rate) — every package that emits a data-source fetcher has to derive
|
|
1404
1467
|
// them the same way.
|
|
1405
1468
|
export { buildProductTransformOptions } from './transformations'
|
|
1406
1469
|
export type { EcommerceProductTransformOptions, EntityTransformOptions } from './transformations'
|
|
1470
|
+
// How a fetch says which language it is for, and when a project needs it to —
|
|
1471
|
+
// shared with every other package that emits a data-source fetch.
|
|
1472
|
+
export { isLocalizedProject, resolveContentLocalization } from './content-localization'
|
|
1473
|
+
export type { ContentLocalization } from './content-localization'
|
|
1474
|
+
export { REQUEST_LOCALE_PARAM, buildServerLocaleParam } from './request-locale'
|
package/src/loading-state.ts
CHANGED
|
@@ -67,6 +67,19 @@ export interface LoadingStateVars {
|
|
|
67
67
|
* stale rows back for the rest of the second request.
|
|
68
68
|
*/
|
|
69
69
|
inFlightRefVar: string
|
|
70
|
+
/**
|
|
71
|
+
* Ref counting the requests STARTED, so a response can tell whether a newer
|
|
72
|
+
* request has overtaken it. `DataProvider` keeps whatever resolves last:
|
|
73
|
+
* two controls changed in quick succession (a "Clear all" emptying several
|
|
74
|
+
* filter states, one state write after another) start two requests, and
|
|
75
|
+
* when the older, slower one settles after the newer it painted the OLDER
|
|
76
|
+
* rows over the newer — a list showing two products for a URL that filters
|
|
77
|
+
* nothing. Each request notes its sequence number and, if overtaken, hands
|
|
78
|
+
* over the newest settled rows instead of its own.
|
|
79
|
+
*/
|
|
80
|
+
fetchSeqRefVar: string
|
|
81
|
+
/** Ref holding the rows of the newest request that settled — what an overtaken one resolves with. */
|
|
82
|
+
latestDataRefVar: string
|
|
70
83
|
}
|
|
71
84
|
|
|
72
85
|
export function getLoadingStateVars(index: number): LoadingStateVars {
|
|
@@ -74,14 +87,18 @@ export function getLoadingStateVars(index: number): LoadingStateVars {
|
|
|
74
87
|
isFetchingVar: `ds_${index}_isFetching`,
|
|
75
88
|
setIsFetchingVar: `setDs_${index}_isFetching`,
|
|
76
89
|
inFlightRefVar: `ds_${index}_fetchesInFlight`,
|
|
90
|
+
fetchSeqRefVar: `ds_${index}_fetchSeq`,
|
|
91
|
+
latestDataRefVar: `ds_${index}_latestData`,
|
|
77
92
|
}
|
|
78
93
|
}
|
|
79
94
|
|
|
80
95
|
/**
|
|
81
96
|
* `const ds_N_fetchesInFlight = useRef(0)`
|
|
97
|
+
* `const ds_N_fetchSeq = useRef(0)`
|
|
98
|
+
* `const ds_N_latestData = useRef(undefined)`
|
|
82
99
|
* `const [ds_N_isFetching, setDs_N_isFetching] = useState(false)`
|
|
83
100
|
*
|
|
84
|
-
*
|
|
101
|
+
* All are stable across renders (ref objects and a `useState` setter), which
|
|
85
102
|
* is what lets the wrapped `fetchData` keep its empty `useCallback` dependency
|
|
86
103
|
* array — a changing `fetchData` identity would retrigger the provider's fetch
|
|
87
104
|
* effect on every render.
|
|
@@ -94,6 +111,18 @@ export function buildLoadingStateDeclarations(vars: LoadingStateVars): types.Sta
|
|
|
94
111
|
types.callExpression(types.identifier('useRef'), [types.numericLiteral(0)])
|
|
95
112
|
),
|
|
96
113
|
]),
|
|
114
|
+
types.variableDeclaration('const', [
|
|
115
|
+
types.variableDeclarator(
|
|
116
|
+
types.identifier(vars.fetchSeqRefVar),
|
|
117
|
+
types.callExpression(types.identifier('useRef'), [types.numericLiteral(0)])
|
|
118
|
+
),
|
|
119
|
+
]),
|
|
120
|
+
types.variableDeclaration('const', [
|
|
121
|
+
types.variableDeclarator(
|
|
122
|
+
types.identifier(vars.latestDataRefVar),
|
|
123
|
+
types.callExpression(types.identifier('useRef'), [types.identifier('undefined')])
|
|
124
|
+
),
|
|
125
|
+
]),
|
|
97
126
|
types.variableDeclaration('const', [
|
|
98
127
|
types.variableDeclarator(
|
|
99
128
|
types.arrayPattern([
|
|
@@ -183,10 +212,54 @@ function injectTrackingIntoCachedBody(body: types.BlockStatement, vars: LoadingS
|
|
|
183
212
|
|
|
184
213
|
const tracked = buildTrackedFetchBody(networkChain.statement.argument as types.Expression, vars)
|
|
185
214
|
body.body.splice(networkChain.index, 1, ...tracked.body)
|
|
215
|
+
markCacheHitAsNewest(body, vars)
|
|
186
216
|
|
|
187
217
|
return true
|
|
188
218
|
}
|
|
189
219
|
|
|
220
|
+
/**
|
|
221
|
+
* A cache hit answers synchronously with the rows of THIS request, so it is
|
|
222
|
+
* the newest settled result too: it advances the sequence and records its rows,
|
|
223
|
+
* or an older network request still in flight would resolve after it and paint
|
|
224
|
+
* over it. Adds the two statements at the head of the hit branch — before its
|
|
225
|
+
* `return Promise.resolve(__tqHit)` — and nothing else, so the flag the branch
|
|
226
|
+
* deliberately never raises stays untouched.
|
|
227
|
+
*/
|
|
228
|
+
function markCacheHitAsNewest(body: types.BlockStatement, vars: LoadingStateVars): void {
|
|
229
|
+
const hit = body.body.find(
|
|
230
|
+
(statement): statement is types.IfStatement =>
|
|
231
|
+
statement.type === 'IfStatement' &&
|
|
232
|
+
statement.test.type === 'BinaryExpression' &&
|
|
233
|
+
statement.test.left.type === 'Identifier' &&
|
|
234
|
+
statement.test.left.name === CACHE_HIT_IDENTIFIER &&
|
|
235
|
+
statement.consequent.type === 'BlockStatement'
|
|
236
|
+
)
|
|
237
|
+
if (!hit || hit.consequent.type !== 'BlockStatement') {
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
hit.consequent.body.unshift(
|
|
241
|
+
types.expressionStatement(types.updateExpression('++', refCurrent(vars.fetchSeqRefVar), true)),
|
|
242
|
+
types.expressionStatement(
|
|
243
|
+
types.assignmentExpression(
|
|
244
|
+
'=',
|
|
245
|
+
refCurrent(vars.latestDataRefVar),
|
|
246
|
+
types.identifier(CACHE_HIT_IDENTIFIER)
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** The identifier the cache wiring binds a hit to — see `cache/ast.ts`. */
|
|
253
|
+
const CACHE_HIT_IDENTIFIER = '__tqHit'
|
|
254
|
+
/** The rows a tracked request resolved with, inside its own `then`. */
|
|
255
|
+
const TRACKED_ROWS_IDENTIFIER = '__tqRows'
|
|
256
|
+
/** This request's sequence number. */
|
|
257
|
+
const TRACKED_SEQ_IDENTIFIER = '__tqSeq'
|
|
258
|
+
|
|
259
|
+
function refCurrent(refVar: string): types.MemberExpression {
|
|
260
|
+
return types.memberExpression(types.identifier(refVar), types.identifier('current'))
|
|
261
|
+
}
|
|
262
|
+
|
|
190
263
|
// tslint:disable-next-line:no-any
|
|
191
264
|
function containsIdentifier(node: any, name: string): boolean {
|
|
192
265
|
if (!node || typeof node !== 'object') {
|
|
@@ -257,9 +330,16 @@ function findMemoizedFetchDataArrow(
|
|
|
257
330
|
* Turns `(params) => fetch(...).then(...)` into
|
|
258
331
|
*
|
|
259
332
|
* (params) => {
|
|
333
|
+
* const __tqSeq = ++ds_N_fetchSeq.current
|
|
260
334
|
* ds_N_fetchesInFlight.current += 1
|
|
261
335
|
* setDs_N_isFetching(true)
|
|
262
|
-
* return fetch(...).then(...).
|
|
336
|
+
* return fetch(...).then(...).then((__tqRows) => {
|
|
337
|
+
* if (__tqSeq !== ds_N_fetchSeq.current) {
|
|
338
|
+
* return ds_N_latestData.current !== undefined ? ds_N_latestData.current : __tqRows
|
|
339
|
+
* }
|
|
340
|
+
* ds_N_latestData.current = __tqRows
|
|
341
|
+
* return __tqRows
|
|
342
|
+
* }).finally(() => {
|
|
263
343
|
* ds_N_fetchesInFlight.current -= 1
|
|
264
344
|
* if (ds_N_fetchesInFlight.current <= 0) {
|
|
265
345
|
* ds_N_fetchesInFlight.current = 0
|
|
@@ -268,6 +348,12 @@ function findMemoizedFetchDataArrow(
|
|
|
268
348
|
* })
|
|
269
349
|
* }
|
|
270
350
|
*
|
|
351
|
+
* The `then` is the stale guard (see `fetchSeqRefVar`): a request overtaken by
|
|
352
|
+
* a newer one resolves with the newest settled rows rather than its own, so
|
|
353
|
+
* the provider — which keeps whatever resolves LAST — never paints older rows
|
|
354
|
+
* over newer ones. An overtaken request whose successor has not settled yet
|
|
355
|
+
* resolves with its own rows; the successor then replaces them.
|
|
356
|
+
*
|
|
271
357
|
* `finally` (rather than a `then` pair) keeps the flag honest when the request
|
|
272
358
|
* rejects: the provider switches to its error status and the loading state must
|
|
273
359
|
* not stay on screen. The counter is clamped at 0 so a late settle from a
|
|
@@ -317,7 +403,52 @@ function buildTrackedFetchBody(
|
|
|
317
403
|
])
|
|
318
404
|
)
|
|
319
405
|
|
|
406
|
+
const latestData = refCurrent(vars.latestDataRefVar)
|
|
407
|
+
const staleGuard = types.arrowFunctionExpression(
|
|
408
|
+
[types.identifier(TRACKED_ROWS_IDENTIFIER)],
|
|
409
|
+
types.blockStatement([
|
|
410
|
+
types.ifStatement(
|
|
411
|
+
types.binaryExpression(
|
|
412
|
+
'!==',
|
|
413
|
+
types.identifier(TRACKED_SEQ_IDENTIFIER),
|
|
414
|
+
refCurrent(vars.fetchSeqRefVar)
|
|
415
|
+
),
|
|
416
|
+
types.blockStatement([
|
|
417
|
+
types.returnStatement(
|
|
418
|
+
types.conditionalExpression(
|
|
419
|
+
types.binaryExpression(
|
|
420
|
+
'!==',
|
|
421
|
+
types.cloneNode(latestData, true),
|
|
422
|
+
types.identifier('undefined')
|
|
423
|
+
),
|
|
424
|
+
types.cloneNode(latestData, true),
|
|
425
|
+
types.identifier(TRACKED_ROWS_IDENTIFIER)
|
|
426
|
+
)
|
|
427
|
+
),
|
|
428
|
+
])
|
|
429
|
+
),
|
|
430
|
+
types.expressionStatement(
|
|
431
|
+
types.assignmentExpression(
|
|
432
|
+
'=',
|
|
433
|
+
types.cloneNode(latestData, true),
|
|
434
|
+
types.identifier(TRACKED_ROWS_IDENTIFIER)
|
|
435
|
+
)
|
|
436
|
+
),
|
|
437
|
+
types.returnStatement(types.identifier(TRACKED_ROWS_IDENTIFIER)),
|
|
438
|
+
])
|
|
439
|
+
)
|
|
440
|
+
const guardedFetch = types.callExpression(
|
|
441
|
+
types.memberExpression(fetchExpression, types.identifier('then')),
|
|
442
|
+
[staleGuard]
|
|
443
|
+
)
|
|
444
|
+
|
|
320
445
|
return types.blockStatement([
|
|
446
|
+
types.variableDeclaration('const', [
|
|
447
|
+
types.variableDeclarator(
|
|
448
|
+
types.identifier(TRACKED_SEQ_IDENTIFIER),
|
|
449
|
+
types.updateExpression('++', refCurrent(vars.fetchSeqRefVar), true)
|
|
450
|
+
),
|
|
451
|
+
]),
|
|
321
452
|
types.expressionStatement(
|
|
322
453
|
types.assignmentExpression(
|
|
323
454
|
'+=',
|
|
@@ -329,7 +460,7 @@ function buildTrackedFetchBody(
|
|
|
329
460
|
types.callExpression(types.identifier(vars.setIsFetchingVar), [types.booleanLiteral(true)])
|
|
330
461
|
),
|
|
331
462
|
types.returnStatement(
|
|
332
|
-
types.callExpression(types.memberExpression(
|
|
463
|
+
types.callExpression(types.memberExpression(guardedFetch, types.identifier('finally')), [
|
|
333
464
|
settleHandler,
|
|
334
465
|
])
|
|
335
466
|
),
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filtering and sorting a translatable column in the visitor's language.
|
|
3
|
+
*
|
|
4
|
+
* A translatable column of a platform table exists once per language:
|
|
5
|
+
* `slug` holds the main language and `es_slug` the Spanish copy (see
|
|
6
|
+
* `ContentLocalization`). The row transform resolves what a page DISPLAYS —
|
|
7
|
+
* a Spanish listing shows `es_name` and links to `es_slug` — but the SQL a
|
|
8
|
+
* fetch runs still names the base column, so without this file:
|
|
9
|
+
*
|
|
10
|
+
* - the details page a Spanish card links to (`/es/products/<es_slug>`)
|
|
11
|
+
* looks the row up with `slug = $1` and answers 404;
|
|
12
|
+
* - "sort by name" on the Spanish listing orders the Spanish names by
|
|
13
|
+
* their ENGLISH counterparts.
|
|
14
|
+
*
|
|
15
|
+
* The teleport fetcher (`fetchers/teleport.ts`) therefore embeds the helper
|
|
16
|
+
* emitted here:
|
|
17
|
+
*
|
|
18
|
+
* - equality on a translatable column matches the value in ANY language:
|
|
19
|
+
* `("slug" = $1 OR "es_slug" = $1)`. Deliberately every language, not the
|
|
20
|
+
* request's: the language switcher keeps the URL and changes the locale,
|
|
21
|
+
* so `/products/<es_slug>` (English page, Spanish slug) has to resolve as
|
|
22
|
+
* well as `/es/products/<slug>`. Only `=` (and the `in` it is normalised
|
|
23
|
+
* to) — a translatable column is never compared with `<`, and `!=` on one
|
|
24
|
+
* has no sensible cross-language reading;
|
|
25
|
+
* - ordering by a translatable column orders by the REQUEST's language,
|
|
26
|
+
* falling back to the base column exactly like the transform does:
|
|
27
|
+
* `COALESCE(NULLIF("es_name", ''), "name")`.
|
|
28
|
+
*
|
|
29
|
+
* Which columns take part is decided at GENERATION time from the table's
|
|
30
|
+
* schema and the project's languages — never by prefix-sniffing column names,
|
|
31
|
+
* which would mistake `og_title` for a translation. A copy the schema does not
|
|
32
|
+
* list (a language added before a column became translatable) is simply not
|
|
33
|
+
* referenced, so the SQL can only ever name columns that exist.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import type { ContentLocalization } from './content-localization'
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Base column → `{ locale: copy column }`, for every translatable column of a
|
|
40
|
+
* table that has at least one per-language copy in the schema.
|
|
41
|
+
*/
|
|
42
|
+
export type LocalizedColumnMap = Record<string, Record<string, string>>
|
|
43
|
+
|
|
44
|
+
/** The only identifiers the generated SQL is allowed to quote — same rule as the search columns. */
|
|
45
|
+
const SQL_IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/
|
|
46
|
+
|
|
47
|
+
/** The column holding `column` in `locale` — the editor's `getI18nColumnName`. */
|
|
48
|
+
const localizedColumnName = (locale: string, column: string): string => `${locale}_${column}`
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Pairs every column with the per-language copies the schema actually holds.
|
|
52
|
+
* Empty (no families) when the project has one language or the table has no
|
|
53
|
+
* translated column — the emitted helper is then inert.
|
|
54
|
+
*/
|
|
55
|
+
export const collectLocalizedColumns = (
|
|
56
|
+
columnNames: string[],
|
|
57
|
+
localization: ContentLocalization | undefined
|
|
58
|
+
): LocalizedColumnMap => {
|
|
59
|
+
const families: LocalizedColumnMap = {}
|
|
60
|
+
if (!localization || localization.secondaryLocales.length === 0) {
|
|
61
|
+
return families
|
|
62
|
+
}
|
|
63
|
+
const present = new Set(columnNames.filter((name) => SQL_IDENTIFIER_PATTERN.test(name)))
|
|
64
|
+
present.forEach((column) => {
|
|
65
|
+
localization.secondaryLocales.forEach((locale) => {
|
|
66
|
+
const copy = localizedColumnName(locale, column)
|
|
67
|
+
if (!present.has(copy)) {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
if (!families[column]) {
|
|
71
|
+
families[column] = {}
|
|
72
|
+
}
|
|
73
|
+
families[column][locale] = copy
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
return families
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The `localizedEqualityClause` / `localizedSortFieldSql` helpers a fetcher
|
|
81
|
+
* embeds, with the column map baked in.
|
|
82
|
+
*
|
|
83
|
+
* Emitted as plain source with no backtick and no `${`, because the fetcher
|
|
84
|
+
* interpolates it into a template literal. Every identifier it quotes comes
|
|
85
|
+
* from the baked map, so a request can only ever name a column the schema had.
|
|
86
|
+
*/
|
|
87
|
+
export const generateLocalizedColumnsHelper = (families: LocalizedColumnMap): string => {
|
|
88
|
+
const lines = [
|
|
89
|
+
'// Per-language copies of the translatable columns, base column -> locale -> copy.',
|
|
90
|
+
'// Baked from the table schema and the project languages at generation time.',
|
|
91
|
+
`var LOCALIZED_COLUMNS = ${JSON.stringify(families)}`,
|
|
92
|
+
'',
|
|
93
|
+
'function quoteLocalizedColumn(column) {',
|
|
94
|
+
" return '\"' + column + '\"'",
|
|
95
|
+
'}',
|
|
96
|
+
'',
|
|
97
|
+
'// Equality on a translatable column matches the value in ANY language, so a',
|
|
98
|
+
'// localized slug resolves whichever locale the URL is under. Answers null',
|
|
99
|
+
'// for every other column and operand, leaving them to the ordinary branches.',
|
|
100
|
+
'function localizedEqualityClause(field, operand, value, pushParam) {',
|
|
101
|
+
' var copies = LOCALIZED_COLUMNS[field]',
|
|
102
|
+
" if (!copies || operand !== '=' || value === null || value === undefined) return null",
|
|
103
|
+
' // An empty set means "no filter", as it does in the ordinary array branch.',
|
|
104
|
+
' if (Array.isArray(value) && value.length === 0) return null',
|
|
105
|
+
' var columns = [field]',
|
|
106
|
+
' for (var locale in copies) {',
|
|
107
|
+
' if (Object.prototype.hasOwnProperty.call(copies, locale)) columns.push(copies[locale])',
|
|
108
|
+
' }',
|
|
109
|
+
' var param = Array.isArray(value) ? pushParam(value.map(String)) : pushParam(value)',
|
|
110
|
+
' var clauses = []',
|
|
111
|
+
' for (var i = 0; i < columns.length; i++) {',
|
|
112
|
+
' clauses.push(',
|
|
113
|
+
' Array.isArray(value)',
|
|
114
|
+
" ? quoteLocalizedColumn(columns[i]) + '::text = ANY(' + param + '::text[])'",
|
|
115
|
+
" : quoteLocalizedColumn(columns[i]) + ' = ' + param",
|
|
116
|
+
' )',
|
|
117
|
+
' }',
|
|
118
|
+
" return '(' + clauses.join(' OR ') + ')'",
|
|
119
|
+
'}',
|
|
120
|
+
'',
|
|
121
|
+
"// ORDER BY a translatable column in the request's language, falling back to",
|
|
122
|
+
'// the base column for a row with no translation — the same resolution the',
|
|
123
|
+
'// row transform applies to what is displayed. Null when the field or the',
|
|
124
|
+
'// locale has no copy, so the ordinary sort runs.',
|
|
125
|
+
'function localizedSortFieldSql(field, locale) {',
|
|
126
|
+
' var copies = LOCALIZED_COLUMNS[field]',
|
|
127
|
+
" var copy = copies && typeof locale === 'string' ? copies[locale] : null",
|
|
128
|
+
' if (!copy) return null',
|
|
129
|
+
" return 'COALESCE(NULLIF(' + quoteLocalizedColumn(copy) + \", ''), \" + quoteLocalizedColumn(field) + ')'",
|
|
130
|
+
'}',
|
|
131
|
+
]
|
|
132
|
+
return lines.join('\n')
|
|
133
|
+
}
|