@teleporthq/teleport-plugin-next-data-source 0.43.55 → 0.43.59
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-allow-backorders.test.ts +156 -0
- package/__tests__/ecommerce-product-model.test.ts +179 -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 +27 -1
- package/dist/cjs/transformations/ecommerce-product.d.ts.map +1 -1
- package/dist/cjs/transformations/ecommerce-product.js +8 -3
- package/dist/cjs/transformations/ecommerce-product.js.map +1 -1
- package/dist/cjs/transformations/index.d.ts +14 -2
- package/dist/cjs/transformations/index.d.ts.map +1 -1
- package/dist/cjs/transformations/index.js +37 -3
- package/dist/cjs/transformations/index.js.map +1 -1
- package/dist/cjs/transformations/shared-utils.d.ts.map +1 -1
- package/dist/cjs/transformations/shared-utils.js +1 -1
- package/dist/cjs/transformations/shared-utils.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 +27 -1
- package/dist/esm/transformations/ecommerce-product.d.ts.map +1 -1
- package/dist/esm/transformations/ecommerce-product.js +8 -3
- package/dist/esm/transformations/ecommerce-product.js.map +1 -1
- package/dist/esm/transformations/index.d.ts +14 -2
- package/dist/esm/transformations/index.d.ts.map +1 -1
- package/dist/esm/transformations/index.js +36 -3
- package/dist/esm/transformations/index.js.map +1 -1
- package/dist/esm/transformations/shared-utils.d.ts.map +1 -1
- package/dist/esm/transformations/shared-utils.js +1 -1
- package/dist/esm/transformations/shared-utils.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 +161 -5
- package/src/transformations/index.ts +43 -4
- package/src/transformations/shared-utils.ts +14 -0
- package/src/utils.ts +10 -5
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import * as types from '@babel/types'
|
|
2
|
+
import generator from '@babel/generator'
|
|
3
|
+
import { parse } from '@babel/parser'
|
|
4
|
+
import { ASTStatementOrder } from '@teleporthq/teleport-shared'
|
|
5
|
+
import {
|
|
6
|
+
ChunkType,
|
|
7
|
+
FileType,
|
|
8
|
+
ComponentStructure,
|
|
9
|
+
ChunkDefinition,
|
|
10
|
+
UIDLDataCacheConfig,
|
|
11
|
+
UIDLDependency,
|
|
12
|
+
} from '@teleporthq/teleport-types'
|
|
13
|
+
import { createNextArrayMapperPaginationPlugin } from '../src/pagination-plugin'
|
|
14
|
+
|
|
15
|
+
type Category = 'paginated+search' | 'paginated-only' | 'search-only' | 'plain'
|
|
16
|
+
|
|
17
|
+
const CACHE: UIDLDataCacheConfig = {
|
|
18
|
+
enabled: true,
|
|
19
|
+
ttlSeconds: 60,
|
|
20
|
+
client: true,
|
|
21
|
+
server: true,
|
|
22
|
+
versionScope: 'ds1:products',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const makeDataProviderJSX = (): types.JSXElement => {
|
|
26
|
+
const repeater = types.jsxElement(
|
|
27
|
+
types.jsxOpeningElement(
|
|
28
|
+
types.jsxIdentifier('Repeater'),
|
|
29
|
+
[
|
|
30
|
+
types.jsxAttribute(
|
|
31
|
+
types.jsxIdentifier('renderItem'),
|
|
32
|
+
types.jsxExpressionContainer(
|
|
33
|
+
types.arrowFunctionExpression(
|
|
34
|
+
[types.identifier('product')],
|
|
35
|
+
types.jsxElement(
|
|
36
|
+
types.jsxOpeningElement(types.jsxIdentifier('div'), [], true),
|
|
37
|
+
null,
|
|
38
|
+
[],
|
|
39
|
+
true
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
),
|
|
44
|
+
],
|
|
45
|
+
true
|
|
46
|
+
),
|
|
47
|
+
null,
|
|
48
|
+
[],
|
|
49
|
+
true
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return types.jsxElement(
|
|
53
|
+
types.jsxOpeningElement(
|
|
54
|
+
types.jsxIdentifier('DataProvider'),
|
|
55
|
+
[
|
|
56
|
+
types.jsxAttribute(
|
|
57
|
+
types.jsxIdentifier('name'),
|
|
58
|
+
types.jsxExpressionContainer(types.stringLiteral('items'))
|
|
59
|
+
),
|
|
60
|
+
types.jsxAttribute(
|
|
61
|
+
types.jsxIdentifier('renderSuccess'),
|
|
62
|
+
types.jsxExpressionContainer(
|
|
63
|
+
types.arrowFunctionExpression([types.identifier('items')], repeater)
|
|
64
|
+
)
|
|
65
|
+
),
|
|
66
|
+
types.jsxAttribute(
|
|
67
|
+
types.jsxIdentifier('renderLoading'),
|
|
68
|
+
types.jsxExpressionContainer(
|
|
69
|
+
types.arrowFunctionExpression(
|
|
70
|
+
[],
|
|
71
|
+
types.jsxElement(
|
|
72
|
+
types.jsxOpeningElement(types.jsxIdentifier('p'), [], true),
|
|
73
|
+
null,
|
|
74
|
+
[],
|
|
75
|
+
true
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
),
|
|
80
|
+
],
|
|
81
|
+
true
|
|
82
|
+
),
|
|
83
|
+
null,
|
|
84
|
+
[],
|
|
85
|
+
true
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const makeComponentChunk = (dataProvider: types.JSXElement): ChunkDefinition => ({
|
|
90
|
+
name: 'jsx-component',
|
|
91
|
+
type: ChunkType.AST,
|
|
92
|
+
fileType: FileType.JS,
|
|
93
|
+
linkAfter: [],
|
|
94
|
+
content: types.variableDeclaration('const', [
|
|
95
|
+
types.variableDeclarator(
|
|
96
|
+
types.identifier('ProductsList'),
|
|
97
|
+
types.arrowFunctionExpression(
|
|
98
|
+
[types.identifier('props')],
|
|
99
|
+
types.blockStatement([types.returnStatement(dataProvider)])
|
|
100
|
+
)
|
|
101
|
+
),
|
|
102
|
+
]),
|
|
103
|
+
meta: {},
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
// tslint:disable-next-line:no-any
|
|
107
|
+
const makeUidlNode = (
|
|
108
|
+
category: Category,
|
|
109
|
+
cache?: UIDLDataCacheConfig,
|
|
110
|
+
dynamicSort = false
|
|
111
|
+
): any => ({
|
|
112
|
+
type: 'data-source-list',
|
|
113
|
+
content: {
|
|
114
|
+
renderPropIdentifier: 'items',
|
|
115
|
+
resourceDefinition: {
|
|
116
|
+
dataSourceId: 'ds1',
|
|
117
|
+
tableName: 'products',
|
|
118
|
+
dataSourceType: 'postgresql',
|
|
119
|
+
},
|
|
120
|
+
resource: { params: { queryColumns: { content: ['name'] } } },
|
|
121
|
+
nodes: {
|
|
122
|
+
success: {
|
|
123
|
+
type: 'cms-list-repeater',
|
|
124
|
+
content: {
|
|
125
|
+
renderPropIdentifier: 'product',
|
|
126
|
+
paginated: category === 'paginated+search' || category === 'paginated-only',
|
|
127
|
+
perPage: 20,
|
|
128
|
+
searchEnabled: category === 'paginated+search' || category === 'search-only',
|
|
129
|
+
searchDebounce: 300,
|
|
130
|
+
nodes: { list: { type: 'element', content: { elementType: 'div' } } },
|
|
131
|
+
...(cache ? { cache } : {}),
|
|
132
|
+
...(dynamicSort
|
|
133
|
+
? {
|
|
134
|
+
sort: { type: 'expr', content: 'sortBy.split("-")[0]' },
|
|
135
|
+
sortDirection: { type: 'expr', content: 'sortBy.split("-")[1]' },
|
|
136
|
+
}
|
|
137
|
+
: {}),
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
const runPlugin = async (options: {
|
|
145
|
+
category?: Category
|
|
146
|
+
cache?: UIDLDataCacheConfig
|
|
147
|
+
dynamicSort?: boolean
|
|
148
|
+
}) => {
|
|
149
|
+
const chunk = makeComponentChunk(makeDataProviderJSX())
|
|
150
|
+
const dependencies: Record<string, UIDLDependency> = {}
|
|
151
|
+
// tslint:disable-next-line:no-any
|
|
152
|
+
const extractedResources: Record<string, any> = {}
|
|
153
|
+
const structure: ComponentStructure = {
|
|
154
|
+
uidl: {
|
|
155
|
+
name: 'ProductsList',
|
|
156
|
+
node: makeUidlNode(
|
|
157
|
+
options.category || 'paginated+search',
|
|
158
|
+
options.cache,
|
|
159
|
+
options.dynamicSort
|
|
160
|
+
),
|
|
161
|
+
},
|
|
162
|
+
chunks: [chunk],
|
|
163
|
+
dependencies,
|
|
164
|
+
options: {
|
|
165
|
+
dataSources: {
|
|
166
|
+
ds1: {
|
|
167
|
+
id: 'ds1',
|
|
168
|
+
name: 'Store',
|
|
169
|
+
type: 'postgresql',
|
|
170
|
+
config: { host: 'h', database: 'd', user: 'u', password: 'p' },
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
extractedResources,
|
|
174
|
+
},
|
|
175
|
+
} as never
|
|
176
|
+
|
|
177
|
+
await createNextArrayMapperPaginationPlugin()(structure)
|
|
178
|
+
return { code: generator(chunk.content as types.Node).code, dependencies, extractedResources }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const CATEGORIES: Category[] = ['paginated+search', 'paginated-only', 'search-only', 'plain']
|
|
182
|
+
|
|
183
|
+
describe('array-mapper cache — client wiring', () => {
|
|
184
|
+
/**
|
|
185
|
+
* The regression pin that matters most: a project that never asked for
|
|
186
|
+
* caching has to generate exactly what it generated before.
|
|
187
|
+
*/
|
|
188
|
+
describe.each(CATEGORIES)('with caching OFF (%s)', (category) => {
|
|
189
|
+
it('emits no cache code at all', async () => {
|
|
190
|
+
const { code, dependencies } = await runPlugin({ category })
|
|
191
|
+
|
|
192
|
+
expect(code).not.toContain('tqCache')
|
|
193
|
+
expect(code).not.toContain('ds_0_cached')
|
|
194
|
+
expect(code).not.toContain('ds_0_params')
|
|
195
|
+
expect(Object.keys(dependencies)).not.toContain('tqCacheGet')
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
describe.each(CATEGORIES)('with caching ON (%s)', (category) => {
|
|
200
|
+
it('checks the cache before touching the network', async () => {
|
|
201
|
+
const { code } = await runPlugin({ category, cache: CACHE })
|
|
202
|
+
|
|
203
|
+
expect(code).toContain('const __tqKey = tqCacheKey(params)')
|
|
204
|
+
expect(code).toContain('const __tqHit = tqCacheGet("ds1:products", __tqKey)')
|
|
205
|
+
expect(code).toContain('return Promise.resolve(__tqHit)')
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('stores the rows and the version the server answered with', async () => {
|
|
209
|
+
const { code } = await runPlugin({ category, cache: CACHE })
|
|
210
|
+
|
|
211
|
+
expect(code).toContain('tqCacheSetVersion("ds1:products", response?.version)')
|
|
212
|
+
expect(code).toContain(
|
|
213
|
+
'return tqCacheSet("ds1:products", __tqKey, response?.data, 60, response?.version)'
|
|
214
|
+
)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('registers the runtime import once', async () => {
|
|
218
|
+
const { dependencies } = await runPlugin({ category, cache: CACHE })
|
|
219
|
+
|
|
220
|
+
expect(dependencies.tqCacheGet).toMatchObject({
|
|
221
|
+
type: 'local',
|
|
222
|
+
path: '../utils/tq-cache/client',
|
|
223
|
+
meta: { namedImport: true },
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('flips the hydration latch and revalidates once per page', async () => {
|
|
228
|
+
const { code } = await runPlugin({ category, cache: CACHE })
|
|
229
|
+
|
|
230
|
+
expect(code).toContain('tqMarkHydrated()')
|
|
231
|
+
expect(code).toContain('tqCacheRevalidate(["ds1:products"])')
|
|
232
|
+
expect(code.match(/tqCacheRevalidate/g)).toHaveLength(1)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The two ordering defects the /blog page shipped with. Source-text
|
|
237
|
+
* `toContain` assertions cannot see either of them, so they are pinned
|
|
238
|
+
* structurally: parse the emitted component and prove it executes.
|
|
239
|
+
*/
|
|
240
|
+
it('emits the hydration effect BEFORE the return — an appended hook never runs', async () => {
|
|
241
|
+
const { code } = await runPlugin({ category, cache: CACHE })
|
|
242
|
+
|
|
243
|
+
const file = parse(code, { sourceType: 'module', plugins: ['jsx'] })
|
|
244
|
+
const declaration = file.program.body.find(
|
|
245
|
+
(statement) => statement.type === 'VariableDeclaration'
|
|
246
|
+
) as types.VariableDeclaration
|
|
247
|
+
const body = (declaration.declarations[0].init as types.ArrowFunctionExpression)
|
|
248
|
+
.body as types.BlockStatement
|
|
249
|
+
|
|
250
|
+
const returnIndex = body.body.findIndex((statement) => statement.type === 'ReturnStatement')
|
|
251
|
+
const hydrationIndex = body.body.findIndex((statement) =>
|
|
252
|
+
generator(statement).code.includes('tqMarkHydrated')
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
expect(hydrationIndex).toBeGreaterThan(-1)
|
|
256
|
+
expect(hydrationIndex).toBeLessThan(returnIndex)
|
|
257
|
+
// Nothing may trail the component's return — trailing statements are
|
|
258
|
+
// unreachable and silently disable whole features.
|
|
259
|
+
expect(returnIndex).toBe(body.body.length - 1)
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it('emits statements in an executable order (no TDZ, nothing unreachable)', async () => {
|
|
263
|
+
const { code } = await runPlugin({ category, cache: CACHE })
|
|
264
|
+
|
|
265
|
+
const ast = parse(code, { sourceType: 'module', plugins: ['jsx'] })
|
|
266
|
+
// Zero repairs needed: the plugin placed everything correctly on its
|
|
267
|
+
// own, without leaning on the emit-time safety net.
|
|
268
|
+
expect(ASTStatementOrder.normalizeStatementOrder(ast)).toBe(0)
|
|
269
|
+
})
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The flash this feature exists to remove. A cache HIT must return before the
|
|
274
|
+
* in-flight flag is raised, or `persistDataDuringLoading={!ds_0_isFetching}`
|
|
275
|
+
* drops the provider into its loading slot for a frame.
|
|
276
|
+
*/
|
|
277
|
+
it('never raises the in-flight flag on a cache hit', async () => {
|
|
278
|
+
const { code } = await runPlugin({ cache: CACHE })
|
|
279
|
+
|
|
280
|
+
const hitReturn = code.indexOf('return Promise.resolve(__tqHit)')
|
|
281
|
+
const raise = code.indexOf('setDs_0_isFetching(true)')
|
|
282
|
+
|
|
283
|
+
expect(hitReturn).toBeGreaterThan(-1)
|
|
284
|
+
expect(raise).toBeGreaterThan(-1)
|
|
285
|
+
expect(hitReturn).toBeLessThan(raise)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('still tracks in-flight fetches on a cache MISS', async () => {
|
|
289
|
+
const { code } = await runPlugin({ cache: CACHE })
|
|
290
|
+
|
|
291
|
+
expect(code).toContain('ds_0_fetchesInFlight.current += 1')
|
|
292
|
+
expect(code).toContain('setDs_0_isFetching(true)')
|
|
293
|
+
expect(code).toContain('persistDataDuringLoading={!ds_0_isFetching}')
|
|
294
|
+
// The settle handler belongs to the network chain, not to the early return.
|
|
295
|
+
expect(code).toContain('.finally(')
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('feeds a cache hit into initialData so a remount paints instantly', async () => {
|
|
299
|
+
const { code } = await runPlugin({ category: 'paginated+search', cache: CACHE })
|
|
300
|
+
|
|
301
|
+
expect(code).toContain('const ds_0_params = useMemo(')
|
|
302
|
+
expect(code).toContain('params={ds_0_params}')
|
|
303
|
+
expect(code).toContain('const ds_0_cached = useMemo(')
|
|
304
|
+
expect(code).toContain(
|
|
305
|
+
'tqCacheGet("ds1:products", tqCacheKey(ds_0_params), {\n sticky: true\n })'
|
|
306
|
+
)
|
|
307
|
+
expect(code).toContain(': ds_0_cached')
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The exact crash this suite failed to see the first time:
|
|
312
|
+
* `ds_0_params` reads `ds_0_state` in its dependency array, so hoisting it
|
|
313
|
+
* ABOVE the `useState` throws
|
|
314
|
+
* `ReferenceError: Cannot access 'ds_0_state' before initialization`
|
|
315
|
+
* the moment the page renders.
|
|
316
|
+
*/
|
|
317
|
+
it('declares the hoisted params/peek AFTER the state they read', async () => {
|
|
318
|
+
const { code } = await runPlugin({ category: 'paginated+search', cache: CACHE })
|
|
319
|
+
|
|
320
|
+
const stateIndex = code.indexOf('const [ds_0_state')
|
|
321
|
+
const paramsIndex = code.indexOf('const ds_0_params')
|
|
322
|
+
const cachedIndex = code.indexOf('const ds_0_cached')
|
|
323
|
+
|
|
324
|
+
expect(stateIndex).toBeGreaterThan(-1)
|
|
325
|
+
expect(paramsIndex).toBeGreaterThan(stateIndex)
|
|
326
|
+
expect(cachedIndex).toBeGreaterThan(paramsIndex)
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* A state-bound sort already suppresses `initialData`, because the prefetch
|
|
331
|
+
* ran without sort params. The peek is `initialData` by another name, so
|
|
332
|
+
* leaving it in would let a stale first page mask the chosen sort.
|
|
333
|
+
*/
|
|
334
|
+
it('suppresses the peek when a state-bound sort is active', async () => {
|
|
335
|
+
const { code } = await runPlugin({ cache: CACHE, dynamicSort: true })
|
|
336
|
+
|
|
337
|
+
expect(code).not.toContain('ds_0_cached')
|
|
338
|
+
// The fetch-level cache still applies — only the mount-time peek is dropped.
|
|
339
|
+
expect(code).toContain('const __tqHit = tqCacheGet("ds1:products", __tqKey)')
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
it('does not cache in the browser when only the server layer is on', async () => {
|
|
343
|
+
const { code } = await runPlugin({ cache: { ...CACHE, client: false } })
|
|
344
|
+
|
|
345
|
+
expect(code).not.toContain('tqCacheGet')
|
|
346
|
+
expect(code).not.toContain('ds_0_cached')
|
|
347
|
+
})
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
describe('array-mapper cache — server wiring', () => {
|
|
351
|
+
it('wraps the generated data-source module when the server layer is on', async () => {
|
|
352
|
+
const { extractedResources } = await runPlugin({ cache: CACHE })
|
|
353
|
+
const moduleKey = Object.keys(extractedResources).find((key) => key.startsWith('utils/'))
|
|
354
|
+
|
|
355
|
+
expect(moduleKey).toBeDefined()
|
|
356
|
+
const content = extractedResources[moduleKey as string].content as string
|
|
357
|
+
expect(content).toContain("import { tqWithCache } from '../tq-cache/server'")
|
|
358
|
+
expect(content).toContain('"scope":"ds1:products"')
|
|
359
|
+
expect(content).toContain('"scope":"ds1:products:count"')
|
|
360
|
+
expect(content).toContain('"ttl":60')
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
it('leaves the module untouched when only the browser layer is on', async () => {
|
|
364
|
+
const { extractedResources } = await runPlugin({ cache: { ...CACHE, server: false } })
|
|
365
|
+
const moduleKey = Object.keys(extractedResources).find((key) => key.startsWith('utils/'))
|
|
366
|
+
|
|
367
|
+
expect(moduleKey).toBeDefined()
|
|
368
|
+
expect(extractedResources[moduleKey as string].content).not.toContain('tq-cache')
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
it('leaves the module untouched when caching is off entirely', async () => {
|
|
372
|
+
const { extractedResources } = await runPlugin({})
|
|
373
|
+
const moduleKey = Object.keys(extractedResources).find((key) => key.startsWith('utils/'))
|
|
374
|
+
|
|
375
|
+
expect(moduleKey).toBeDefined()
|
|
376
|
+
expect(extractedResources[moduleKey as string].content).not.toContain('tq-cache')
|
|
377
|
+
})
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* The generated data-source module is shared project-wide, so two pages caching
|
|
382
|
+
* the same table have to agree. If the merge were per-page, page ORDER would
|
|
383
|
+
* decide the TTL — and a page asking for 300s processed second would silently
|
|
384
|
+
* widen the window the 60s page was configured with.
|
|
385
|
+
*/
|
|
386
|
+
describe('array-mapper cache — one module, many pages', () => {
|
|
387
|
+
const runTwoPages = async (firstTtl: number, secondTtl: number) => {
|
|
388
|
+
// tslint:disable-next-line:no-any
|
|
389
|
+
const extractedResources: Record<string, any> = {}
|
|
390
|
+
// tslint:disable-next-line:no-any
|
|
391
|
+
const sharedOptions: any = {
|
|
392
|
+
dataSources: {
|
|
393
|
+
ds1: {
|
|
394
|
+
id: 'ds1',
|
|
395
|
+
name: 'Store',
|
|
396
|
+
type: 'postgresql',
|
|
397
|
+
config: { host: 'h', database: 'd', user: 'u', password: 'p' },
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
extractedResources,
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
for (const ttlSeconds of [firstTtl, secondTtl]) {
|
|
404
|
+
const chunk = makeComponentChunk(makeDataProviderJSX())
|
|
405
|
+
const structure: ComponentStructure = {
|
|
406
|
+
uidl: {
|
|
407
|
+
name: 'ProductsList',
|
|
408
|
+
node: makeUidlNode('paginated+search', { ...CACHE, ttlSeconds }),
|
|
409
|
+
},
|
|
410
|
+
chunks: [chunk],
|
|
411
|
+
dependencies: {},
|
|
412
|
+
options: sharedOptions,
|
|
413
|
+
} as never
|
|
414
|
+
await createNextArrayMapperPaginationPlugin()(structure)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const moduleKey = Object.keys(extractedResources).find((key) => key.startsWith('utils/'))
|
|
418
|
+
return extractedResources[moduleKey as string].content as string
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
it('takes the shortest TTL whichever page is generated first', async () => {
|
|
422
|
+
expect(await runTwoPages(300, 60)).toContain('"ttl":60')
|
|
423
|
+
expect(await runTwoPages(60, 300)).toContain('"ttl":60')
|
|
424
|
+
})
|
|
425
|
+
})
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { UIDLDataSource } from '@teleporthq/teleport-types'
|
|
2
|
+
import { generateDataSourceFetcherWithCore } from '../src/data-source-fetchers'
|
|
3
|
+
import { mergeServerCacheOptions } from '../src/cache/config'
|
|
4
|
+
|
|
5
|
+
const dataSource: UIDLDataSource = {
|
|
6
|
+
id: 'ds_1',
|
|
7
|
+
name: 'Store',
|
|
8
|
+
type: 'teleport',
|
|
9
|
+
config: {},
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const CACHE = { scope: 'ds_1:teleport_products', ttlSeconds: 60 }
|
|
13
|
+
|
|
14
|
+
describe('generateDataSourceFetcherWithCore — server cache wrapping', () => {
|
|
15
|
+
/**
|
|
16
|
+
* The regression pin. A project that never asked for caching must generate
|
|
17
|
+
* byte-identical output, or this feature is a breaking change for everyone.
|
|
18
|
+
*/
|
|
19
|
+
it('emits byte-identical output when no cache is configured', () => {
|
|
20
|
+
const before = generateDataSourceFetcherWithCore(dataSource, 'teleport_products', false, {})
|
|
21
|
+
const after = generateDataSourceFetcherWithCore(
|
|
22
|
+
dataSource,
|
|
23
|
+
'teleport_products',
|
|
24
|
+
false,
|
|
25
|
+
{},
|
|
26
|
+
undefined
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
expect(after).toBe(before)
|
|
30
|
+
expect(before).not.toContain('tq-cache')
|
|
31
|
+
expect(before).toContain('export { fetchData, fetchCount, handler, getCount }')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('wraps both the data handler and the count handler', () => {
|
|
35
|
+
const code = generateDataSourceFetcherWithCore(
|
|
36
|
+
dataSource,
|
|
37
|
+
'teleport_products',
|
|
38
|
+
false,
|
|
39
|
+
{},
|
|
40
|
+
CACHE
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
expect(code).toContain("import { tqWithCache } from '../tq-cache/server'")
|
|
44
|
+
expect(code).toContain('const cachedHandler = tqWithCache(handler, __tqDataCache)')
|
|
45
|
+
expect(code).toContain('const cachedGetCount = tqWithCache(getCount, __tqCountCache)')
|
|
46
|
+
expect(code).toContain(
|
|
47
|
+
'export { fetchData, fetchCount, cachedHandler as handler, cachedGetCount as getCount }'
|
|
48
|
+
)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* `getStaticProps` reaches the database through `fetchData`, not through the
|
|
53
|
+
* API route, so pointing it at the raw handler would leave the prefetch —
|
|
54
|
+
* which is page 1 of every list — permanently uncached.
|
|
55
|
+
*/
|
|
56
|
+
it('routes the getStaticProps prefetch through the cache too', () => {
|
|
57
|
+
const code = generateDataSourceFetcherWithCore(
|
|
58
|
+
dataSource,
|
|
59
|
+
'teleport_products',
|
|
60
|
+
false,
|
|
61
|
+
{},
|
|
62
|
+
CACHE
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
expect(code).toContain('await cachedHandler(req, res)')
|
|
66
|
+
expect(code).toContain('await cachedGetCount(req, res)')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('gives the count endpoint its own sub-scope but the same version', () => {
|
|
70
|
+
const code = generateDataSourceFetcherWithCore(
|
|
71
|
+
dataSource,
|
|
72
|
+
'teleport_products',
|
|
73
|
+
false,
|
|
74
|
+
{},
|
|
75
|
+
CACHE
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
expect(code).toContain('"scope":"ds_1:teleport_products"')
|
|
79
|
+
expect(code).toContain('"scope":"ds_1:teleport_products:count"')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
/** The inline API-route variant sits one directory deeper. */
|
|
83
|
+
it('uses the right import depth for the inlined API-route variant', () => {
|
|
84
|
+
const code = generateDataSourceFetcherWithCore(dataSource, 'teleport_products', true, {}, CACHE)
|
|
85
|
+
|
|
86
|
+
expect(code).toContain("import { tqWithCache } from '../../tq-cache/server'")
|
|
87
|
+
expect(code).toContain('export default cachedHandler')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('keeps the response private unless a shared window was granted', () => {
|
|
91
|
+
const withoutCdn = generateDataSourceFetcherWithCore(
|
|
92
|
+
dataSource,
|
|
93
|
+
'teleport_products',
|
|
94
|
+
false,
|
|
95
|
+
{},
|
|
96
|
+
CACHE
|
|
97
|
+
)
|
|
98
|
+
expect(withoutCdn).toContain('"sMaxAge":0')
|
|
99
|
+
|
|
100
|
+
const withCdn = generateDataSourceFetcherWithCore(
|
|
101
|
+
dataSource,
|
|
102
|
+
'teleport_products',
|
|
103
|
+
false,
|
|
104
|
+
{},
|
|
105
|
+
{
|
|
106
|
+
...CACHE,
|
|
107
|
+
sMaxAge: 30,
|
|
108
|
+
staleWhileRevalidate: 300,
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
expect(withCdn).toContain('"sMaxAge":30')
|
|
112
|
+
expect(withCdn).toContain('"swr":300')
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
describe('mergeServerCacheOptions', () => {
|
|
117
|
+
it('returns nothing when no mapper wants a server cache', () => {
|
|
118
|
+
expect(mergeServerCacheOptions([{ scope: 's', ttlSeconds: 0 }])).toBeUndefined()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Two mappers can read the same table from different pages and land in the
|
|
123
|
+
* same generated file. The one that asked for fresher data must win, or a
|
|
124
|
+
* sibling silently makes its list staler than it was configured to be.
|
|
125
|
+
*/
|
|
126
|
+
it('takes the SHORTEST requested TTL', () => {
|
|
127
|
+
expect(
|
|
128
|
+
mergeServerCacheOptions([
|
|
129
|
+
{ scope: 's', ttlSeconds: 600 },
|
|
130
|
+
{ scope: 's', ttlSeconds: 60 },
|
|
131
|
+
])
|
|
132
|
+
).toMatchObject({ ttlSeconds: 60 })
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('ignores disabled entries when picking the TTL', () => {
|
|
136
|
+
expect(
|
|
137
|
+
mergeServerCacheOptions([
|
|
138
|
+
{ scope: 's', ttlSeconds: 0 },
|
|
139
|
+
{ scope: 's', ttlSeconds: 120 },
|
|
140
|
+
])
|
|
141
|
+
).toMatchObject({ ttlSeconds: 120 })
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('takes the shortest shared-cache window', () => {
|
|
145
|
+
expect(
|
|
146
|
+
mergeServerCacheOptions([
|
|
147
|
+
{ scope: 's', ttlSeconds: 60, sMaxAge: 300 },
|
|
148
|
+
{ scope: 's', ttlSeconds: 60, sMaxAge: 30 },
|
|
149
|
+
])
|
|
150
|
+
).toMatchObject({ sMaxAge: 30 })
|
|
151
|
+
})
|
|
152
|
+
})
|