@teleporthq/teleport-plugin-next-data-source 0.43.55 → 0.43.57
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 +425 -0
- package/__tests__/data-source-cache-wrapping.test.ts +152 -0
- package/__tests__/ecommerce-product-storefront-tax.test.ts +118 -0
- package/dist/cjs/cache/ast.d.ts +71 -0
- package/dist/cjs/cache/ast.d.ts.map +1 -0
- package/dist/cjs/cache/ast.js +229 -0
- package/dist/cjs/cache/ast.js.map +1 -0
- package/dist/cjs/cache/config.d.ts +22 -0
- package/dist/cjs/cache/config.d.ts.map +1 -0
- package/dist/cjs/cache/config.js +67 -0
- package/dist/cjs/cache/config.js.map +1 -0
- package/dist/cjs/cache/types.d.ts +31 -0
- package/dist/cjs/cache/types.d.ts.map +1 -0
- package/dist/cjs/cache/types.js +3 -0
- package/dist/cjs/cache/types.js.map +1 -0
- package/dist/cjs/data-source-fetchers.d.ts +5 -3
- package/dist/cjs/data-source-fetchers.d.ts.map +1 -1
- package/dist/cjs/data-source-fetchers.js +51 -8
- package/dist/cjs/data-source-fetchers.js.map +1 -1
- package/dist/cjs/fetchers/teleport.d.ts +2 -2
- package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
- package/dist/cjs/fetchers/teleport.js +3 -2
- package/dist/cjs/fetchers/teleport.js.map +1 -1
- package/dist/cjs/index.d.ts +2 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +14 -9
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/loading-state.d.ts.map +1 -1
- package/dist/cjs/loading-state.js +74 -2
- package/dist/cjs/loading-state.js.map +1 -1
- package/dist/cjs/pagination-plugin.d.ts.map +1 -1
- package/dist/cjs/pagination-plugin.js +204 -64
- package/dist/cjs/pagination-plugin.js.map +1 -1
- package/dist/cjs/transformations/ecommerce-product.d.ts +15 -1
- package/dist/cjs/transformations/ecommerce-product.d.ts.map +1 -1
- package/dist/cjs/transformations/ecommerce-product.js +7 -3
- package/dist/cjs/transformations/ecommerce-product.js.map +1 -1
- package/dist/cjs/transformations/index.d.ts +13 -2
- package/dist/cjs/transformations/index.d.ts.map +1 -1
- package/dist/cjs/transformations/index.js +21 -3
- package/dist/cjs/transformations/index.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/utils.d.ts +4 -3
- package/dist/cjs/utils.d.ts.map +1 -1
- package/dist/cjs/utils.js +6 -4
- package/dist/cjs/utils.js.map +1 -1
- package/dist/esm/cache/ast.d.ts +71 -0
- package/dist/esm/cache/ast.d.ts.map +1 -0
- package/dist/esm/cache/ast.js +196 -0
- package/dist/esm/cache/ast.js.map +1 -0
- package/dist/esm/cache/config.d.ts +22 -0
- package/dist/esm/cache/config.d.ts.map +1 -0
- package/dist/esm/cache/config.js +62 -0
- package/dist/esm/cache/config.js.map +1 -0
- package/dist/esm/cache/types.d.ts +31 -0
- package/dist/esm/cache/types.d.ts.map +1 -0
- package/dist/esm/cache/types.js +2 -0
- package/dist/esm/cache/types.js.map +1 -0
- package/dist/esm/data-source-fetchers.d.ts +5 -3
- package/dist/esm/data-source-fetchers.d.ts.map +1 -1
- package/dist/esm/data-source-fetchers.js +51 -8
- package/dist/esm/data-source-fetchers.js.map +1 -1
- package/dist/esm/fetchers/teleport.d.ts +2 -2
- package/dist/esm/fetchers/teleport.d.ts.map +1 -1
- package/dist/esm/fetchers/teleport.js +3 -2
- package/dist/esm/fetchers/teleport.js.map +1 -1
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +12 -8
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/loading-state.d.ts.map +1 -1
- package/dist/esm/loading-state.js +74 -2
- package/dist/esm/loading-state.js.map +1 -1
- package/dist/esm/pagination-plugin.d.ts.map +1 -1
- package/dist/esm/pagination-plugin.js +205 -65
- package/dist/esm/pagination-plugin.js.map +1 -1
- package/dist/esm/transformations/ecommerce-product.d.ts +15 -1
- package/dist/esm/transformations/ecommerce-product.d.ts.map +1 -1
- package/dist/esm/transformations/ecommerce-product.js +7 -3
- package/dist/esm/transformations/ecommerce-product.js.map +1 -1
- package/dist/esm/transformations/index.d.ts +13 -2
- package/dist/esm/transformations/index.d.ts.map +1 -1
- package/dist/esm/transformations/index.js +20 -3
- package/dist/esm/transformations/index.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/utils.d.ts +4 -3
- package/dist/esm/utils.d.ts.map +1 -1
- package/dist/esm/utils.js +6 -4
- package/dist/esm/utils.js.map +1 -1
- package/package.json +5 -5
- package/src/cache/ast.ts +265 -0
- package/src/cache/config.ts +79 -0
- package/src/cache/types.ts +31 -0
- package/src/data-source-fetchers.ts +66 -12
- package/src/fetchers/teleport.ts +3 -3
- package/src/index.ts +10 -4
- package/src/loading-state.ts +65 -2
- package/src/pagination-plugin.ts +315 -116
- package/src/transformations/ecommerce-product.ts +53 -2
- package/src/transformations/index.ts +25 -4
- package/src/utils.ts +10 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teleporthq/teleport-plugin-next-data-source",
|
|
3
|
-
"version": "0.43.
|
|
3
|
+
"version": "0.43.57",
|
|
4
4
|
"description": "A plugin for managing data sources for Next.js projects.",
|
|
5
5
|
"author": "teleportHQ",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@babel/parser": "^7.26.10",
|
|
28
28
|
"@babel/types": "^7.5.5",
|
|
29
|
-
"@teleporthq/teleport-plugin-common": "^0.43.
|
|
30
|
-
"@teleporthq/teleport-shared": "^0.43.
|
|
31
|
-
"@teleporthq/teleport-types": "^0.43.
|
|
29
|
+
"@teleporthq/teleport-plugin-common": "^0.43.57",
|
|
30
|
+
"@teleporthq/teleport-shared": "^0.43.57",
|
|
31
|
+
"@teleporthq/teleport-types": "^0.43.57"
|
|
32
32
|
},
|
|
33
|
-
"gitHead": "
|
|
33
|
+
"gitHead": "1aaae3dc987920579371e068d33d000e92cfc218"
|
|
34
34
|
}
|
package/src/cache/ast.ts
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import * as types from '@babel/types'
|
|
2
|
+
import { UIDLDependency } from '@teleporthq/teleport-types'
|
|
3
|
+
import { ResolvedDataSourceCache } from './types'
|
|
4
|
+
|
|
5
|
+
/** Identifiers the emitted page imports from `utils/tq-cache/client`. */
|
|
6
|
+
export const CACHE_CLIENT_IMPORTS = [
|
|
7
|
+
'tqCacheKey',
|
|
8
|
+
'tqCacheGet',
|
|
9
|
+
'tqCacheSet',
|
|
10
|
+
'tqCacheSetVersion',
|
|
11
|
+
'tqMarkHydrated',
|
|
12
|
+
'tqCacheRevalidate',
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
/** Local identifiers used inside the generated `fetchData` body. */
|
|
16
|
+
const KEY_VAR = '__tqKey'
|
|
17
|
+
const HIT_VAR = '__tqHit'
|
|
18
|
+
|
|
19
|
+
export const cachedValueVarFor = (index: number): string => `ds_${index}_cached`
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Registers the client runtime's named imports on the page.
|
|
23
|
+
*
|
|
24
|
+
* Each identifier is its own dependency entry because that is how the
|
|
25
|
+
* import-statements plugin keys them; they share one path, so they collapse
|
|
26
|
+
* into a single `import { … } from '…/utils/tq-cache/client'`.
|
|
27
|
+
*/
|
|
28
|
+
export const registerCacheClientImports = (
|
|
29
|
+
dependencies: Record<string, UIDLDependency>,
|
|
30
|
+
folderPath: string[] | undefined
|
|
31
|
+
): void => {
|
|
32
|
+
const depth = (folderPath ? folderPath.length : 0) + 1
|
|
33
|
+
const path = `${'../'.repeat(depth)}utils/tq-cache/client`
|
|
34
|
+
|
|
35
|
+
CACHE_CLIENT_IMPORTS.forEach((identifier) => {
|
|
36
|
+
if (!dependencies[identifier]) {
|
|
37
|
+
dependencies[identifier] = {
|
|
38
|
+
type: 'local',
|
|
39
|
+
path,
|
|
40
|
+
meta: { namedImport: true },
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* `const __tqKey = tqCacheKey(params)` +
|
|
48
|
+
* `const __tqHit = tqCacheGet(SCOPE, __tqKey)` +
|
|
49
|
+
* `if (__tqHit !== undefined) return Promise.resolve(__tqHit)`
|
|
50
|
+
*
|
|
51
|
+
* The early return is what keeps a hit from ever touching the network — and,
|
|
52
|
+
* just as importantly, from raising the in-flight flag that would paint the
|
|
53
|
+
* loading slot. `loading-state` deliberately attaches its bookkeeping to the
|
|
54
|
+
* network chain BELOW this, never to the `Promise.resolve`.
|
|
55
|
+
*/
|
|
56
|
+
const buildCachePeekPreamble = (scope: string): types.Statement[] => [
|
|
57
|
+
types.variableDeclaration('const', [
|
|
58
|
+
types.variableDeclarator(
|
|
59
|
+
types.identifier(KEY_VAR),
|
|
60
|
+
types.callExpression(types.identifier('tqCacheKey'), [types.identifier('params')])
|
|
61
|
+
),
|
|
62
|
+
]),
|
|
63
|
+
types.variableDeclaration('const', [
|
|
64
|
+
types.variableDeclarator(
|
|
65
|
+
types.identifier(HIT_VAR),
|
|
66
|
+
types.callExpression(types.identifier('tqCacheGet'), [
|
|
67
|
+
types.stringLiteral(scope),
|
|
68
|
+
types.identifier(KEY_VAR),
|
|
69
|
+
])
|
|
70
|
+
),
|
|
71
|
+
]),
|
|
72
|
+
types.ifStatement(
|
|
73
|
+
types.binaryExpression('!==', types.identifier(HIT_VAR), types.identifier('undefined')),
|
|
74
|
+
types.blockStatement([
|
|
75
|
+
types.returnStatement(
|
|
76
|
+
types.callExpression(
|
|
77
|
+
types.memberExpression(types.identifier('Promise'), types.identifier('resolve')),
|
|
78
|
+
[types.identifier(HIT_VAR)]
|
|
79
|
+
)
|
|
80
|
+
),
|
|
81
|
+
])
|
|
82
|
+
),
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The terminal `.then` of a cached fetch: record the version the server
|
|
87
|
+
* answered with (which purges the scope if it moved) and store the rows.
|
|
88
|
+
*
|
|
89
|
+
* `tqCacheSet` returns the value it was given, so the promise still resolves to
|
|
90
|
+
* exactly what the uncached chain resolved to — `response?.data`.
|
|
91
|
+
*/
|
|
92
|
+
export const buildCacheStoreThen = (
|
|
93
|
+
cache: ResolvedDataSourceCache
|
|
94
|
+
): types.ArrowFunctionExpression => {
|
|
95
|
+
const responseData = types.optionalMemberExpression(
|
|
96
|
+
types.identifier('response'),
|
|
97
|
+
types.identifier('data'),
|
|
98
|
+
false,
|
|
99
|
+
true
|
|
100
|
+
)
|
|
101
|
+
const responseVersion = types.optionalMemberExpression(
|
|
102
|
+
types.identifier('response'),
|
|
103
|
+
types.identifier('version'),
|
|
104
|
+
false,
|
|
105
|
+
true
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return types.arrowFunctionExpression(
|
|
109
|
+
[types.identifier('response')],
|
|
110
|
+
types.blockStatement([
|
|
111
|
+
types.expressionStatement(
|
|
112
|
+
types.callExpression(types.identifier('tqCacheSetVersion'), [
|
|
113
|
+
types.stringLiteral(cache.scope),
|
|
114
|
+
types.cloneNode(responseVersion, true),
|
|
115
|
+
])
|
|
116
|
+
),
|
|
117
|
+
types.returnStatement(
|
|
118
|
+
types.callExpression(types.identifier('tqCacheSet'), [
|
|
119
|
+
types.stringLiteral(cache.scope),
|
|
120
|
+
types.identifier(KEY_VAR),
|
|
121
|
+
responseData,
|
|
122
|
+
types.numericLiteral(cache.ttlSeconds),
|
|
123
|
+
types.cloneNode(responseVersion, true),
|
|
124
|
+
])
|
|
125
|
+
),
|
|
126
|
+
])
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Wraps a network chain in the peek preamble, producing the cached body. */
|
|
131
|
+
export const buildCachedFetchBody = (
|
|
132
|
+
networkChain: types.Expression,
|
|
133
|
+
cache: ResolvedDataSourceCache
|
|
134
|
+
): types.BlockStatement =>
|
|
135
|
+
types.blockStatement([
|
|
136
|
+
...buildCachePeekPreamble(cache.scope),
|
|
137
|
+
types.returnStatement(networkChain),
|
|
138
|
+
])
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Finds the `return <fetch(...)…>` statement inside a cached `fetchData` body.
|
|
142
|
+
*
|
|
143
|
+
* Shared with `loading-state` so both modules agree on what "the network chain"
|
|
144
|
+
* means: the body also contains an early `return Promise.resolve(hit)`, and
|
|
145
|
+
* attaching in-flight bookkeeping to THAT would flip the loading flag on every
|
|
146
|
+
* cache hit — the exact flash this feature exists to remove.
|
|
147
|
+
*/
|
|
148
|
+
export const findCachedFetchNetworkChain = (
|
|
149
|
+
body: types.BlockStatement
|
|
150
|
+
): { statement: types.ReturnStatement; index: number } | undefined => {
|
|
151
|
+
for (let index = body.body.length - 1; index >= 0; index--) {
|
|
152
|
+
const statement = body.body[index]
|
|
153
|
+
if (statement.type !== 'ReturnStatement' || !statement.argument) {
|
|
154
|
+
continue
|
|
155
|
+
}
|
|
156
|
+
if (
|
|
157
|
+
statement.argument.type === 'CallExpression' &&
|
|
158
|
+
callChainStartsWithFetch(statement.argument)
|
|
159
|
+
) {
|
|
160
|
+
return { statement, index }
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return undefined
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const callChainStartsWithFetch = (expression: types.Expression): boolean => {
|
|
167
|
+
let current: types.Expression = expression
|
|
168
|
+
while (current.type === 'CallExpression') {
|
|
169
|
+
const { callee } = current
|
|
170
|
+
if (callee.type === 'Identifier' && callee.name === 'fetch') {
|
|
171
|
+
return true
|
|
172
|
+
}
|
|
173
|
+
if (callee.type === 'MemberExpression') {
|
|
174
|
+
current = callee.object as types.Expression
|
|
175
|
+
continue
|
|
176
|
+
}
|
|
177
|
+
return false
|
|
178
|
+
}
|
|
179
|
+
return false
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Hoists a cached provider's `params` into a component const and adds the
|
|
184
|
+
* synchronous cache peek beside it.
|
|
185
|
+
*
|
|
186
|
+
* The peek is what removes the loading flash on a page or search change: those
|
|
187
|
+
* change the provider's `key`, so it REMOUNTS with `data: undefined` and paints
|
|
188
|
+
* `renderLoading()` before any promise — even an already-resolved one — can
|
|
189
|
+
* resume. Feeding a hit into `initialData` makes the provider skip its first
|
|
190
|
+
* fetch entirely and paint the cached rows on the very first frame.
|
|
191
|
+
*
|
|
192
|
+
* `sticky` is set because this value is handed to a mounted provider: once it
|
|
193
|
+
* has been, TTL expiry must not flip it back to `undefined`, or the list would
|
|
194
|
+
* blank out without refetching (its `params` never changed).
|
|
195
|
+
*
|
|
196
|
+
* Reference identity comes from the runtime's own map, not from this `useMemo`
|
|
197
|
+
* — the memo only keeps the peek from re-running on unrelated renders.
|
|
198
|
+
*/
|
|
199
|
+
export const buildCachedParamsDeclarations = (params: {
|
|
200
|
+
index: number
|
|
201
|
+
paramsMemo: types.Expression
|
|
202
|
+
cache: ResolvedDataSourceCache
|
|
203
|
+
}): {
|
|
204
|
+
declarations: types.Statement[]
|
|
205
|
+
paramsIdentifier: string
|
|
206
|
+
cachedIdentifier: string
|
|
207
|
+
} => {
|
|
208
|
+
const paramsIdentifier = `ds_${params.index}_params`
|
|
209
|
+
const cachedIdentifier = cachedValueVarFor(params.index)
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
paramsIdentifier,
|
|
213
|
+
cachedIdentifier,
|
|
214
|
+
declarations: [
|
|
215
|
+
types.variableDeclaration('const', [
|
|
216
|
+
types.variableDeclarator(types.identifier(paramsIdentifier), params.paramsMemo),
|
|
217
|
+
]),
|
|
218
|
+
types.variableDeclaration('const', [
|
|
219
|
+
types.variableDeclarator(
|
|
220
|
+
types.identifier(cachedIdentifier),
|
|
221
|
+
types.callExpression(types.identifier('useMemo'), [
|
|
222
|
+
types.arrowFunctionExpression(
|
|
223
|
+
[],
|
|
224
|
+
types.callExpression(types.identifier('tqCacheGet'), [
|
|
225
|
+
types.stringLiteral(params.cache.scope),
|
|
226
|
+
types.callExpression(types.identifier('tqCacheKey'), [
|
|
227
|
+
types.identifier(paramsIdentifier),
|
|
228
|
+
]),
|
|
229
|
+
types.objectExpression([
|
|
230
|
+
types.objectProperty(types.identifier('sticky'), types.booleanLiteral(true)),
|
|
231
|
+
]),
|
|
232
|
+
])
|
|
233
|
+
),
|
|
234
|
+
types.arrayExpression([types.identifier(paramsIdentifier)]),
|
|
235
|
+
])
|
|
236
|
+
),
|
|
237
|
+
]),
|
|
238
|
+
],
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* `useEffect(() => { tqMarkHydrated(); tqCacheRevalidate([...]) }, [])`
|
|
244
|
+
*
|
|
245
|
+
* One effect per page, covering every scope on it in a single request. Flipping
|
|
246
|
+
* the hydration latch here rather than at import time is what keeps the first
|
|
247
|
+
* client render byte-identical to the server render.
|
|
248
|
+
*/
|
|
249
|
+
export const buildCacheHydrationEffect = (scopes: string[]): types.Statement =>
|
|
250
|
+
types.expressionStatement(
|
|
251
|
+
types.callExpression(types.identifier('useEffect'), [
|
|
252
|
+
types.arrowFunctionExpression(
|
|
253
|
+
[],
|
|
254
|
+
types.blockStatement([
|
|
255
|
+
types.expressionStatement(types.callExpression(types.identifier('tqMarkHydrated'), [])),
|
|
256
|
+
types.expressionStatement(
|
|
257
|
+
types.callExpression(types.identifier('tqCacheRevalidate'), [
|
|
258
|
+
types.arrayExpression(scopes.map((scope) => types.stringLiteral(scope))),
|
|
259
|
+
])
|
|
260
|
+
),
|
|
261
|
+
])
|
|
262
|
+
),
|
|
263
|
+
types.arrayExpression([]),
|
|
264
|
+
])
|
|
265
|
+
)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { DataCache } from '@teleporthq/teleport-shared'
|
|
2
|
+
import { UIDLDataCacheConfig } from '@teleporthq/teleport-types'
|
|
3
|
+
import { DataSourceServerCacheOptions, ResolvedDataSourceCache } from './types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reads the `cache` block off a `cms-list-repeater`.
|
|
7
|
+
*
|
|
8
|
+
* Returns `undefined` — meaning "emit exactly what this generator emitted
|
|
9
|
+
* before caching existed" — for anything that is off, incomplete, or caches
|
|
10
|
+
* into neither layer.
|
|
11
|
+
*/
|
|
12
|
+
export const resolveCacheFromRepeaterContent = (
|
|
13
|
+
cache: UIDLDataCacheConfig | undefined,
|
|
14
|
+
resourceDefinition: { dataSourceId?: string; tableName?: string } | undefined
|
|
15
|
+
): ResolvedDataSourceCache | undefined => {
|
|
16
|
+
if (!cache || cache.enabled !== true) {
|
|
17
|
+
return undefined
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const client = cache.client === true
|
|
21
|
+
const server = cache.server === true
|
|
22
|
+
if (!client && !server) {
|
|
23
|
+
return undefined
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const scope =
|
|
27
|
+
cache.versionScope ||
|
|
28
|
+
(resourceDefinition?.dataSourceId && resourceDefinition?.tableName
|
|
29
|
+
? DataCache.cacheScopeFor(resourceDefinition.dataSourceId, resourceDefinition.tableName)
|
|
30
|
+
: '')
|
|
31
|
+
if (!scope) {
|
|
32
|
+
return undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const ttlSeconds =
|
|
36
|
+
typeof cache.ttlSeconds === 'number' && cache.ttlSeconds > 0
|
|
37
|
+
? Math.trunc(cache.ttlSeconds)
|
|
38
|
+
: DataCache.DEFAULT_CACHE_TTL_SECONDS
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
scope,
|
|
42
|
+
ttlSeconds,
|
|
43
|
+
client,
|
|
44
|
+
server,
|
|
45
|
+
sMaxAge: typeof cache.cdnSMaxAge === 'number' && cache.cdnSMaxAge > 0 ? cache.cdnSMaxAge : 0,
|
|
46
|
+
staleWhileRevalidate:
|
|
47
|
+
typeof cache.cdnStaleWhileRevalidate === 'number' && cache.cdnStaleWhileRevalidate > 0
|
|
48
|
+
? cache.cdnStaleWhileRevalidate
|
|
49
|
+
: undefined,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Folds several mappers' requests for the same generated file into one.
|
|
55
|
+
*
|
|
56
|
+
* Enabled if ANY mapper wants it, but with the SHORTEST requested TTL and the
|
|
57
|
+
* shortest shared-cache window — a mapper that asked for fresher data must
|
|
58
|
+
* never be made staler by a sibling that asked for less.
|
|
59
|
+
*/
|
|
60
|
+
export const mergeServerCacheOptions = (
|
|
61
|
+
entries: DataSourceServerCacheOptions[]
|
|
62
|
+
): DataSourceServerCacheOptions | undefined => {
|
|
63
|
+
const enabled = entries.filter((entry) => entry.ttlSeconds > 0 || (entry.sMaxAge || 0) > 0)
|
|
64
|
+
if (!enabled.length) {
|
|
65
|
+
return undefined
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const positive = (values: number[]) => values.filter((value) => value > 0)
|
|
69
|
+
const ttls = positive(enabled.map((entry) => entry.ttlSeconds))
|
|
70
|
+
const sMaxAges = positive(enabled.map((entry) => entry.sMaxAge || 0))
|
|
71
|
+
const swrs = positive(enabled.map((entry) => entry.staleWhileRevalidate || 0))
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
scope: enabled[0].scope,
|
|
75
|
+
ttlSeconds: ttls.length ? Math.min(...ttls) : 0,
|
|
76
|
+
sMaxAge: sMaxAges.length ? Math.min(...sMaxAges) : 0,
|
|
77
|
+
staleWhileRevalidate: swrs.length ? Math.min(...swrs) : undefined,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A mapper's caching setup once the UIDL has been read and the defaults filled
|
|
3
|
+
* in. `client` and `server` are independent — both layers may run at once.
|
|
4
|
+
*/
|
|
5
|
+
export interface ResolvedDataSourceCache {
|
|
6
|
+
scope: string
|
|
7
|
+
ttlSeconds: number
|
|
8
|
+
client: boolean
|
|
9
|
+
server: boolean
|
|
10
|
+
/** `Cache-Control: s-maxage`. Zero unless a shared window was granted. */
|
|
11
|
+
sMaxAge: number
|
|
12
|
+
staleWhileRevalidate?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Cache configuration as it reaches the emitters.
|
|
17
|
+
*
|
|
18
|
+
* Resolved per generated data-source FILE, not per array mapper: the
|
|
19
|
+
* `utils/data-sources/<file>.js` module is shared by every mapper reading the
|
|
20
|
+
* same `(type, table, dataSourceId)` triple, so two mappers on two pages must
|
|
21
|
+
* agree on one server-side setup. See `mergeServerCacheOptions`.
|
|
22
|
+
*/
|
|
23
|
+
export interface DataSourceServerCacheOptions {
|
|
24
|
+
/** `<dataSourceId>:<tableName>` — the unit of invalidation. */
|
|
25
|
+
scope: string
|
|
26
|
+
/** In-process TTL, seconds. `0` disables the server cache. */
|
|
27
|
+
ttlSeconds: number
|
|
28
|
+
/** `Cache-Control: s-maxage`, seconds. `0` keeps the response private. */
|
|
29
|
+
sMaxAge?: number
|
|
30
|
+
staleWhileRevalidate?: number
|
|
31
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DataSourceType, UIDLDataSource
|
|
1
|
+
import { DataSourceType, UIDLDataSource } from '@teleporthq/teleport-types'
|
|
2
2
|
import {
|
|
3
3
|
generatePostgreSQLFetcher,
|
|
4
4
|
generateMySQLFetcher,
|
|
@@ -33,6 +33,9 @@ import {
|
|
|
33
33
|
} from './fetchers'
|
|
34
34
|
import { validateDatabaseConfig } from './validation'
|
|
35
35
|
import { generateCountFetcher } from './count-fetchers'
|
|
36
|
+
import type { EcommerceProductTransformOptions } from './transformations'
|
|
37
|
+
import { DataCache } from '@teleporthq/teleport-shared'
|
|
38
|
+
import type { DataSourceServerCacheOptions } from './cache/types'
|
|
36
39
|
|
|
37
40
|
export { generateRawQueryFetcher, parseQueryTemplateVariables }
|
|
38
41
|
|
|
@@ -81,7 +84,7 @@ export const getDataSourceDependencies = (
|
|
|
81
84
|
export function generateDataSourceFetcher(
|
|
82
85
|
dataSource: UIDLDataSource,
|
|
83
86
|
tableName: string,
|
|
84
|
-
|
|
87
|
+
transformOptions: EcommerceProductTransformOptions = {}
|
|
85
88
|
): string {
|
|
86
89
|
if (!dataSource || typeof dataSource !== 'object') {
|
|
87
90
|
throw new Error('Invalid data source: data source must be a valid object')
|
|
@@ -237,7 +240,7 @@ export function generateDataSourceFetcher(
|
|
|
237
240
|
if (!validation.isValid) {
|
|
238
241
|
throw new Error(`Teleport config validation failed: ${validation.error}`)
|
|
239
242
|
}
|
|
240
|
-
return generateTeleportFetcher(config, tableName,
|
|
243
|
+
return generateTeleportFetcher(config, tableName, transformOptions)
|
|
241
244
|
}
|
|
242
245
|
|
|
243
246
|
default:
|
|
@@ -258,9 +261,10 @@ export function generateDataSourceFetcherWithCore(
|
|
|
258
261
|
dataSource: UIDLDataSource,
|
|
259
262
|
tableName: string,
|
|
260
263
|
isApiRoute: boolean = false,
|
|
261
|
-
|
|
264
|
+
transformOptions: EcommerceProductTransformOptions = {},
|
|
265
|
+
cacheOptions?: DataSourceServerCacheOptions
|
|
262
266
|
): string {
|
|
263
|
-
const apiHandler = generateDataSourceFetcher(dataSource, tableName,
|
|
267
|
+
const apiHandler = generateDataSourceFetcher(dataSource, tableName, transformOptions)
|
|
264
268
|
|
|
265
269
|
// Extract the handler function body from the API route
|
|
266
270
|
// The API route is structured as: export default async function handler(req, res) { ... }
|
|
@@ -285,14 +289,64 @@ export function generateDataSourceFetcherWithCore(
|
|
|
285
289
|
// Combine and deduplicate imports
|
|
286
290
|
const allImports = Array.from(new Set([...imports, ...countImports]))
|
|
287
291
|
|
|
292
|
+
// Caching is applied HERE rather than in the API route because four separate
|
|
293
|
+
// places emit an `api/<file>` route and a fifth path (`getStaticProps` →
|
|
294
|
+
// `fetchData` → `handler`) never goes through a route at all. Wrapping the
|
|
295
|
+
// module's exports is the one point that covers all five, and leaves every
|
|
296
|
+
// route emitter untouched.
|
|
297
|
+
//
|
|
298
|
+
// The import depth differs by destination: this module is written to
|
|
299
|
+
// `utils/data-sources/` normally, but the `isApiRoute` variant is inlined
|
|
300
|
+
// straight into `pages/api/`.
|
|
301
|
+
const cachePreamble = cacheOptions
|
|
302
|
+
? `import { tqWithCache } from '${isApiRoute ? '../../' : '../'}tq-cache/server'`
|
|
303
|
+
: ''
|
|
304
|
+
|
|
305
|
+
const cacheWiring = cacheOptions
|
|
306
|
+
? `
|
|
307
|
+
const __tqDataCache = ${JSON.stringify({
|
|
308
|
+
scope: cacheOptions.scope,
|
|
309
|
+
ttl: cacheOptions.ttlSeconds,
|
|
310
|
+
sMaxAge: cacheOptions.sMaxAge || 0,
|
|
311
|
+
swr: cacheOptions.staleWhileRevalidate || 0,
|
|
312
|
+
})}
|
|
313
|
+
const __tqCountCache = ${JSON.stringify({
|
|
314
|
+
scope: DataCache.countScopeFor(cacheOptions.scope),
|
|
315
|
+
// Its OWN keyspace, but the DATA scope's version row. A write bumps one
|
|
316
|
+
// row; if the count kept a separate version nothing would ever bump it,
|
|
317
|
+
// and the total would stay stale after every create and delete — the
|
|
318
|
+
// grid showing one more row than the pager counts.
|
|
319
|
+
versionScope: cacheOptions.scope,
|
|
320
|
+
ttl: cacheOptions.ttlSeconds,
|
|
321
|
+
sMaxAge: cacheOptions.sMaxAge || 0,
|
|
322
|
+
swr: cacheOptions.staleWhileRevalidate || 0,
|
|
323
|
+
})}
|
|
324
|
+
|
|
325
|
+
const cachedHandler = tqWithCache(handler, __tqDataCache)
|
|
326
|
+
const cachedGetCount = tqWithCache(getCount, __tqCountCache)
|
|
327
|
+
`
|
|
328
|
+
: ''
|
|
329
|
+
|
|
330
|
+
const handlerExport = cacheOptions ? 'cachedHandler' : 'handler'
|
|
331
|
+
const countExport = cacheOptions ? 'cachedGetCount' : 'getCount'
|
|
332
|
+
|
|
288
333
|
// For API routes, export just the handler function
|
|
289
334
|
// For utils files, export the full object with all functions
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
335
|
+
//
|
|
336
|
+
// The uncached branch is spelled out verbatim rather than interpolated, so an
|
|
337
|
+
// uncached project keeps generating byte-identical output — `handler as
|
|
338
|
+
// handler` would be harmless at runtime and still a diff in every snapshot.
|
|
339
|
+
const exports = !cacheOptions
|
|
340
|
+
? isApiRoute
|
|
341
|
+
? 'export default handler'
|
|
342
|
+
: `export { fetchData, fetchCount, handler, getCount }
|
|
293
343
|
export default { fetchData, fetchCount, handler, getCount }`
|
|
344
|
+
: isApiRoute
|
|
345
|
+
? `export default ${handlerExport}`
|
|
346
|
+
: `export { fetchData, fetchCount, ${handlerExport} as handler, ${countExport} as getCount }
|
|
347
|
+
export default { fetchData, fetchCount, handler: ${handlerExport}, getCount: ${countExport} }`
|
|
294
348
|
|
|
295
|
-
return `${allImports.join('\n')}
|
|
349
|
+
return `${[...allImports, cachePreamble].filter(Boolean).join('\n')}
|
|
296
350
|
|
|
297
351
|
async function fetchData(params = {}) {
|
|
298
352
|
const req = {
|
|
@@ -314,7 +368,7 @@ async function fetchData(params = {}) {
|
|
|
314
368
|
},
|
|
315
369
|
}
|
|
316
370
|
|
|
317
|
-
await
|
|
371
|
+
await ${handlerExport}(req, res)
|
|
318
372
|
|
|
319
373
|
if (statusCode !== 200 || !result || !result.success) {
|
|
320
374
|
throw new Error(result?.error || 'Failed to fetch data')
|
|
@@ -343,7 +397,7 @@ async function fetchCount(params = {}) {
|
|
|
343
397
|
},
|
|
344
398
|
}
|
|
345
399
|
|
|
346
|
-
await
|
|
400
|
+
await ${countExport}(req, res)
|
|
347
401
|
|
|
348
402
|
if (statusCode !== 200 || !result || !result.success) {
|
|
349
403
|
throw new Error(result?.error || 'Failed to get count')
|
|
@@ -355,7 +409,7 @@ async function fetchCount(params = {}) {
|
|
|
355
409
|
${countFetcherWithoutImports}
|
|
356
410
|
|
|
357
411
|
${handlerWithoutImports}
|
|
358
|
-
|
|
412
|
+
${cacheWiring}
|
|
359
413
|
${exports}
|
|
360
414
|
`
|
|
361
415
|
}
|
package/src/fetchers/teleport.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { UIDLEcommerceCategory } from '@teleporthq/teleport-types'
|
|
2
1
|
import {
|
|
3
2
|
replaceSecretReference,
|
|
4
3
|
generateDateFormatterCode,
|
|
@@ -9,6 +8,7 @@ import {
|
|
|
9
8
|
getTransformationCode,
|
|
10
9
|
getTransformExpression,
|
|
11
10
|
getTransformWrapperCode,
|
|
11
|
+
type EcommerceProductTransformOptions,
|
|
12
12
|
} from '../transformations'
|
|
13
13
|
|
|
14
14
|
interface TeleportDBConfig {
|
|
@@ -52,7 +52,7 @@ export const validateTeleportConfig = (
|
|
|
52
52
|
export const generateTeleportFetcher = (
|
|
53
53
|
config: Record<string, unknown>,
|
|
54
54
|
tableName: string,
|
|
55
|
-
|
|
55
|
+
transformOptions: EcommerceProductTransformOptions = {}
|
|
56
56
|
): string => {
|
|
57
57
|
const dbConfig = config as TeleportDBConfig
|
|
58
58
|
const schema = dbConfig.options?.schema
|
|
@@ -142,7 +142,7 @@ const getClient = () => {
|
|
|
142
142
|
${generateSafeJSONParseCode()}
|
|
143
143
|
|
|
144
144
|
${generateSearchEscapeHelpersCode()}
|
|
145
|
-
${getTransformationCode(tableName,
|
|
145
|
+
${getTransformationCode(tableName, transformOptions)}
|
|
146
146
|
${getTransformWrapperCode(tableName)}
|
|
147
147
|
const processFilters = (filters, conditions, queryParams, paramIndex) => {
|
|
148
148
|
if (!filters) return paramIndex
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
hasUnresolvableDynamicParams,
|
|
15
15
|
} from './utils'
|
|
16
16
|
import { createNextArrayMapperPaginationPlugin } from './pagination-plugin'
|
|
17
|
+
import { buildProductTransformOptions } from './transformations'
|
|
17
18
|
import { DATA_SOURCE_ISR_REVALIDATE_SECONDS } from './isr'
|
|
18
19
|
import * as types from '@babel/types'
|
|
19
20
|
|
|
@@ -608,7 +609,7 @@ export const createNextPagesDataSourcePlugin: ComponentPluginFactory<{}> = () =>
|
|
|
608
609
|
dependencies,
|
|
609
610
|
dynamicRouteAttr,
|
|
610
611
|
uidl.outputOptions?.folderPath,
|
|
611
|
-
options
|
|
612
|
+
buildProductTransformOptions(options)
|
|
612
613
|
)
|
|
613
614
|
|
|
614
615
|
if (result.success && result.chunk) {
|
|
@@ -646,7 +647,7 @@ export const createNextPagesDataSourcePlugin: ComponentPluginFactory<{}> = () =>
|
|
|
646
647
|
dataSources,
|
|
647
648
|
componentChunk,
|
|
648
649
|
options.extractedResources,
|
|
649
|
-
options
|
|
650
|
+
buildProductTransformOptions(options)
|
|
650
651
|
)
|
|
651
652
|
}
|
|
652
653
|
})
|
|
@@ -1268,7 +1269,7 @@ export const createNextComponentDataSourcePlugin: ComponentPluginFactory<{}> = (
|
|
|
1268
1269
|
dataSources,
|
|
1269
1270
|
componentChunk,
|
|
1270
1271
|
options.extractedResources,
|
|
1271
|
-
options
|
|
1272
|
+
buildProductTransformOptions(options)
|
|
1272
1273
|
)
|
|
1273
1274
|
})
|
|
1274
1275
|
|
|
@@ -1354,7 +1355,7 @@ export const createNextComponentDataSourcePlugin: ComponentPluginFactory<{}> = (
|
|
|
1354
1355
|
dataSource,
|
|
1355
1356
|
firstDataSourceInfo.tableName,
|
|
1356
1357
|
false,
|
|
1357
|
-
options
|
|
1358
|
+
buildProductTransformOptions(options)
|
|
1358
1359
|
)
|
|
1359
1360
|
options.extractedResources[`utils/${fileName}`] = {
|
|
1360
1361
|
fileName,
|
|
@@ -1398,3 +1399,8 @@ export * from './utils'
|
|
|
1398
1399
|
export * from './array-mapper-pagination'
|
|
1399
1400
|
export * from './pagination-plugin'
|
|
1400
1401
|
export * from './count-fetchers'
|
|
1402
|
+
// The product transform's build-time options (category taxonomy + storefront
|
|
1403
|
+
// tax rate) — every package that emits a data-source fetcher has to derive
|
|
1404
|
+
// them the same way.
|
|
1405
|
+
export { buildProductTransformOptions } from './transformations'
|
|
1406
|
+
export type { EcommerceProductTransformOptions } from './transformations'
|