@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.
Files changed (101) hide show
  1. package/__tests__/array-mapper-cache-wiring.test.ts +425 -0
  2. package/__tests__/data-source-cache-wrapping.test.ts +152 -0
  3. package/__tests__/ecommerce-product-storefront-tax.test.ts +118 -0
  4. package/dist/cjs/cache/ast.d.ts +71 -0
  5. package/dist/cjs/cache/ast.d.ts.map +1 -0
  6. package/dist/cjs/cache/ast.js +229 -0
  7. package/dist/cjs/cache/ast.js.map +1 -0
  8. package/dist/cjs/cache/config.d.ts +22 -0
  9. package/dist/cjs/cache/config.d.ts.map +1 -0
  10. package/dist/cjs/cache/config.js +67 -0
  11. package/dist/cjs/cache/config.js.map +1 -0
  12. package/dist/cjs/cache/types.d.ts +31 -0
  13. package/dist/cjs/cache/types.d.ts.map +1 -0
  14. package/dist/cjs/cache/types.js +3 -0
  15. package/dist/cjs/cache/types.js.map +1 -0
  16. package/dist/cjs/data-source-fetchers.d.ts +5 -3
  17. package/dist/cjs/data-source-fetchers.d.ts.map +1 -1
  18. package/dist/cjs/data-source-fetchers.js +51 -8
  19. package/dist/cjs/data-source-fetchers.js.map +1 -1
  20. package/dist/cjs/fetchers/teleport.d.ts +2 -2
  21. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  22. package/dist/cjs/fetchers/teleport.js +3 -2
  23. package/dist/cjs/fetchers/teleport.js.map +1 -1
  24. package/dist/cjs/index.d.ts +2 -0
  25. package/dist/cjs/index.d.ts.map +1 -1
  26. package/dist/cjs/index.js +14 -9
  27. package/dist/cjs/index.js.map +1 -1
  28. package/dist/cjs/loading-state.d.ts.map +1 -1
  29. package/dist/cjs/loading-state.js +74 -2
  30. package/dist/cjs/loading-state.js.map +1 -1
  31. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  32. package/dist/cjs/pagination-plugin.js +204 -64
  33. package/dist/cjs/pagination-plugin.js.map +1 -1
  34. package/dist/cjs/transformations/ecommerce-product.d.ts +15 -1
  35. package/dist/cjs/transformations/ecommerce-product.d.ts.map +1 -1
  36. package/dist/cjs/transformations/ecommerce-product.js +7 -3
  37. package/dist/cjs/transformations/ecommerce-product.js.map +1 -1
  38. package/dist/cjs/transformations/index.d.ts +13 -2
  39. package/dist/cjs/transformations/index.d.ts.map +1 -1
  40. package/dist/cjs/transformations/index.js +21 -3
  41. package/dist/cjs/transformations/index.js.map +1 -1
  42. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  43. package/dist/cjs/utils.d.ts +4 -3
  44. package/dist/cjs/utils.d.ts.map +1 -1
  45. package/dist/cjs/utils.js +6 -4
  46. package/dist/cjs/utils.js.map +1 -1
  47. package/dist/esm/cache/ast.d.ts +71 -0
  48. package/dist/esm/cache/ast.d.ts.map +1 -0
  49. package/dist/esm/cache/ast.js +196 -0
  50. package/dist/esm/cache/ast.js.map +1 -0
  51. package/dist/esm/cache/config.d.ts +22 -0
  52. package/dist/esm/cache/config.d.ts.map +1 -0
  53. package/dist/esm/cache/config.js +62 -0
  54. package/dist/esm/cache/config.js.map +1 -0
  55. package/dist/esm/cache/types.d.ts +31 -0
  56. package/dist/esm/cache/types.d.ts.map +1 -0
  57. package/dist/esm/cache/types.js +2 -0
  58. package/dist/esm/cache/types.js.map +1 -0
  59. package/dist/esm/data-source-fetchers.d.ts +5 -3
  60. package/dist/esm/data-source-fetchers.d.ts.map +1 -1
  61. package/dist/esm/data-source-fetchers.js +51 -8
  62. package/dist/esm/data-source-fetchers.js.map +1 -1
  63. package/dist/esm/fetchers/teleport.d.ts +2 -2
  64. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  65. package/dist/esm/fetchers/teleport.js +3 -2
  66. package/dist/esm/fetchers/teleport.js.map +1 -1
  67. package/dist/esm/index.d.ts +2 -0
  68. package/dist/esm/index.d.ts.map +1 -1
  69. package/dist/esm/index.js +12 -8
  70. package/dist/esm/index.js.map +1 -1
  71. package/dist/esm/loading-state.d.ts.map +1 -1
  72. package/dist/esm/loading-state.js +74 -2
  73. package/dist/esm/loading-state.js.map +1 -1
  74. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  75. package/dist/esm/pagination-plugin.js +205 -65
  76. package/dist/esm/pagination-plugin.js.map +1 -1
  77. package/dist/esm/transformations/ecommerce-product.d.ts +15 -1
  78. package/dist/esm/transformations/ecommerce-product.d.ts.map +1 -1
  79. package/dist/esm/transformations/ecommerce-product.js +7 -3
  80. package/dist/esm/transformations/ecommerce-product.js.map +1 -1
  81. package/dist/esm/transformations/index.d.ts +13 -2
  82. package/dist/esm/transformations/index.d.ts.map +1 -1
  83. package/dist/esm/transformations/index.js +20 -3
  84. package/dist/esm/transformations/index.js.map +1 -1
  85. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  86. package/dist/esm/utils.d.ts +4 -3
  87. package/dist/esm/utils.d.ts.map +1 -1
  88. package/dist/esm/utils.js +6 -4
  89. package/dist/esm/utils.js.map +1 -1
  90. package/package.json +5 -5
  91. package/src/cache/ast.ts +265 -0
  92. package/src/cache/config.ts +79 -0
  93. package/src/cache/types.ts +31 -0
  94. package/src/data-source-fetchers.ts +66 -12
  95. package/src/fetchers/teleport.ts +3 -3
  96. package/src/index.ts +10 -4
  97. package/src/loading-state.ts +65 -2
  98. package/src/pagination-plugin.ts +315 -116
  99. package/src/transformations/ecommerce-product.ts +53 -2
  100. package/src/transformations/index.ts +25 -4
  101. 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.55",
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.54",
30
- "@teleporthq/teleport-shared": "^0.43.54",
31
- "@teleporthq/teleport-types": "^0.43.54"
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": "eedc7b3e2518265eb876f900e69bc9c9304a26ee"
33
+ "gitHead": "1aaae3dc987920579371e068d33d000e92cfc218"
34
34
  }
@@ -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, UIDLEcommerceCategory } from '@teleporthq/teleport-types'
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
- categories?: UIDLEcommerceCategory[]
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, categories)
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
- categories?: UIDLEcommerceCategory[]
264
+ transformOptions: EcommerceProductTransformOptions = {},
265
+ cacheOptions?: DataSourceServerCacheOptions
262
266
  ): string {
263
- const apiHandler = generateDataSourceFetcher(dataSource, tableName, categories)
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
- const exports = isApiRoute
291
- ? 'export default handler'
292
- : `export { fetchData, fetchCount, handler, getCount }
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 handler(req, res)
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 getCount(req, res)
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
  }
@@ -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
- categories?: UIDLEcommerceCategory[]
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, categories)}
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.ecommerceSettings?.categories
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.ecommerceSettings?.categories
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.ecommerceSettings?.categories
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.ecommerceSettings?.categories
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'