@teleporthq/teleport-plugin-next-data-source 0.43.74 → 0.44.0

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 (231) hide show
  1. package/__tests__/_helpers/pagination-widget-fixture.ts +95 -5
  2. package/__tests__/blog-adjacent-arranged.test.ts +176 -0
  3. package/__tests__/blog-post-contents.test.ts +212 -0
  4. package/__tests__/blog-post-page-enrichment.test.ts +28 -5
  5. package/__tests__/blog-post-picture-stand-in.test.ts +48 -0
  6. package/__tests__/blog-post-reading-time.test.ts +55 -0
  7. package/__tests__/booking-item-transform.test.ts +120 -0
  8. package/__tests__/browser-row-policy.test.ts +13 -3
  9. package/__tests__/content-preset-wiring.test.ts +108 -0
  10. package/__tests__/fetchers.test.ts +1 -1
  11. package/__tests__/fixed-list-sort.test.ts +183 -0
  12. package/__tests__/infinite-scroll.test.ts +154 -5
  13. package/__tests__/list-sort-order.test.ts +61 -0
  14. package/__tests__/null-filter-where.test.ts +132 -0
  15. package/__tests__/pagination-filter-groups.test.ts +186 -0
  16. package/__tests__/pg-date-parser.test.ts +73 -0
  17. package/__tests__/plain-list-sort.test.ts +215 -0
  18. package/__tests__/product-price-sort.ts +58 -25
  19. package/__tests__/range-control.test.ts +159 -0
  20. package/__tests__/rest-api-key-header.test.ts +18 -0
  21. package/__tests__/search-input-marker.test.ts +86 -0
  22. package/__tests__/search-url-sync.test.ts +61 -16
  23. package/__tests__/shared-resource-prefetch.test.ts +65 -0
  24. package/__tests__/table-read-route-protected-tables.test.ts +33 -3
  25. package/dist/cjs/count-effect.d.ts +7 -1
  26. package/dist/cjs/count-effect.d.ts.map +1 -1
  27. package/dist/cjs/count-effect.js +15 -5
  28. package/dist/cjs/count-effect.js.map +1 -1
  29. package/dist/cjs/fetchers/pg-client-import.d.ts +12 -0
  30. package/dist/cjs/fetchers/pg-client-import.d.ts.map +1 -0
  31. package/dist/cjs/fetchers/pg-client-import.js +15 -0
  32. package/dist/cjs/fetchers/pg-client-import.js.map +1 -0
  33. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  34. package/dist/cjs/fetchers/postgresql.js +4 -1
  35. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  36. package/dist/cjs/fetchers/raw-query.d.ts.map +1 -1
  37. package/dist/cjs/fetchers/raw-query.js +2 -1
  38. package/dist/cjs/fetchers/raw-query.js.map +1 -1
  39. package/dist/cjs/fetchers/rest-api.d.ts.map +1 -1
  40. package/dist/cjs/fetchers/rest-api.js +3 -1
  41. package/dist/cjs/fetchers/rest-api.js.map +1 -1
  42. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  43. package/dist/cjs/fetchers/teleport.js +5 -2
  44. package/dist/cjs/fetchers/teleport.js.map +1 -1
  45. package/dist/cjs/fetchers/utils/browser-row-policy.d.ts +7 -6
  46. package/dist/cjs/fetchers/utils/browser-row-policy.d.ts.map +1 -1
  47. package/dist/cjs/fetchers/utils/browser-row-policy.js +7 -33
  48. package/dist/cjs/fetchers/utils/browser-row-policy.js.map +1 -1
  49. package/dist/cjs/filter-utils.d.ts +19 -7
  50. package/dist/cjs/filter-utils.d.ts.map +1 -1
  51. package/dist/cjs/filter-utils.js +49 -13
  52. package/dist/cjs/filter-utils.js.map +1 -1
  53. package/dist/cjs/infinite-scroll.d.ts +25 -2
  54. package/dist/cjs/infinite-scroll.d.ts.map +1 -1
  55. package/dist/cjs/infinite-scroll.js +144 -33
  56. package/dist/cjs/infinite-scroll.js.map +1 -1
  57. package/dist/cjs/list-sort-order.d.ts +14 -0
  58. package/dist/cjs/list-sort-order.d.ts.map +1 -0
  59. package/dist/cjs/list-sort-order.js +36 -0
  60. package/dist/cjs/list-sort-order.js.map +1 -0
  61. package/dist/cjs/pagination-controls.d.ts +22 -3
  62. package/dist/cjs/pagination-controls.d.ts.map +1 -1
  63. package/dist/cjs/pagination-controls.js +29 -3
  64. package/dist/cjs/pagination-controls.js.map +1 -1
  65. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  66. package/dist/cjs/pagination-plugin.js +267 -212
  67. package/dist/cjs/pagination-plugin.js.map +1 -1
  68. package/dist/cjs/product-price-sort.d.ts +0 -9
  69. package/dist/cjs/product-price-sort.d.ts.map +1 -1
  70. package/dist/cjs/product-price-sort.js +1 -13
  71. package/dist/cjs/product-price-sort.js.map +1 -1
  72. package/dist/cjs/range-control.d.ts +45 -0
  73. package/dist/cjs/range-control.d.ts.map +1 -0
  74. package/dist/cjs/range-control.js +165 -0
  75. package/dist/cjs/range-control.js.map +1 -0
  76. package/dist/cjs/row-order.d.ts +20 -0
  77. package/dist/cjs/row-order.d.ts.map +1 -0
  78. package/dist/cjs/row-order.js +46 -0
  79. package/dist/cjs/row-order.js.map +1 -0
  80. package/dist/cjs/search-input.d.ts +42 -0
  81. package/dist/cjs/search-input.d.ts.map +1 -0
  82. package/dist/cjs/search-input.js +158 -0
  83. package/dist/cjs/search-input.js.map +1 -0
  84. package/dist/cjs/sort-utils.d.ts +10 -0
  85. package/dist/cjs/sort-utils.d.ts.map +1 -1
  86. package/dist/cjs/sort-utils.js +22 -1
  87. package/dist/cjs/sort-utils.js.map +1 -1
  88. package/dist/cjs/transformations/blog-adjacent-posts.d.ts +11 -1
  89. package/dist/cjs/transformations/blog-adjacent-posts.d.ts.map +1 -1
  90. package/dist/cjs/transformations/blog-adjacent-posts.js +14 -1
  91. package/dist/cjs/transformations/blog-adjacent-posts.js.map +1 -1
  92. package/dist/cjs/transformations/blog-comments.d.ts +1 -1
  93. package/dist/cjs/transformations/blog-comments.d.ts.map +1 -1
  94. package/dist/cjs/transformations/blog-comments.js +1 -1
  95. package/dist/cjs/transformations/blog-comments.js.map +1 -1
  96. package/dist/cjs/transformations/blog-post.d.ts +14 -0
  97. package/dist/cjs/transformations/blog-post.d.ts.map +1 -1
  98. package/dist/cjs/transformations/blog-post.js +4 -1
  99. package/dist/cjs/transformations/blog-post.js.map +1 -1
  100. package/dist/cjs/transformations/booking-item.d.ts +24 -0
  101. package/dist/cjs/transformations/booking-item.d.ts.map +1 -0
  102. package/dist/cjs/transformations/booking-item.js +27 -0
  103. package/dist/cjs/transformations/booking-item.js.map +1 -0
  104. package/dist/cjs/transformations/heading-anchors.d.ts +32 -8
  105. package/dist/cjs/transformations/heading-anchors.d.ts.map +1 -1
  106. package/dist/cjs/transformations/heading-anchors.js +34 -10
  107. package/dist/cjs/transformations/heading-anchors.js.map +1 -1
  108. package/dist/cjs/transformations/index.d.ts +27 -13
  109. package/dist/cjs/transformations/index.d.ts.map +1 -1
  110. package/dist/cjs/transformations/index.js +73 -16
  111. package/dist/cjs/transformations/index.js.map +1 -1
  112. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  113. package/dist/cjs/utils.d.ts.map +1 -1
  114. package/dist/cjs/utils.js +52 -3
  115. package/dist/cjs/utils.js.map +1 -1
  116. package/dist/esm/count-effect.d.ts +7 -1
  117. package/dist/esm/count-effect.d.ts.map +1 -1
  118. package/dist/esm/count-effect.js +15 -5
  119. package/dist/esm/count-effect.js.map +1 -1
  120. package/dist/esm/fetchers/pg-client-import.d.ts +12 -0
  121. package/dist/esm/fetchers/pg-client-import.d.ts.map +1 -0
  122. package/dist/esm/fetchers/pg-client-import.js +12 -0
  123. package/dist/esm/fetchers/pg-client-import.js.map +1 -0
  124. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  125. package/dist/esm/fetchers/postgresql.js +5 -2
  126. package/dist/esm/fetchers/postgresql.js.map +1 -1
  127. package/dist/esm/fetchers/raw-query.d.ts.map +1 -1
  128. package/dist/esm/fetchers/raw-query.js +2 -1
  129. package/dist/esm/fetchers/raw-query.js.map +1 -1
  130. package/dist/esm/fetchers/rest-api.d.ts.map +1 -1
  131. package/dist/esm/fetchers/rest-api.js +3 -1
  132. package/dist/esm/fetchers/rest-api.js.map +1 -1
  133. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  134. package/dist/esm/fetchers/teleport.js +6 -3
  135. package/dist/esm/fetchers/teleport.js.map +1 -1
  136. package/dist/esm/fetchers/utils/browser-row-policy.d.ts +7 -6
  137. package/dist/esm/fetchers/utils/browser-row-policy.d.ts.map +1 -1
  138. package/dist/esm/fetchers/utils/browser-row-policy.js +5 -31
  139. package/dist/esm/fetchers/utils/browser-row-policy.js.map +1 -1
  140. package/dist/esm/filter-utils.d.ts +19 -7
  141. package/dist/esm/filter-utils.d.ts.map +1 -1
  142. package/dist/esm/filter-utils.js +46 -12
  143. package/dist/esm/filter-utils.js.map +1 -1
  144. package/dist/esm/infinite-scroll.d.ts +25 -2
  145. package/dist/esm/infinite-scroll.d.ts.map +1 -1
  146. package/dist/esm/infinite-scroll.js +142 -32
  147. package/dist/esm/infinite-scroll.js.map +1 -1
  148. package/dist/esm/list-sort-order.d.ts +14 -0
  149. package/dist/esm/list-sort-order.d.ts.map +1 -0
  150. package/dist/esm/list-sort-order.js +32 -0
  151. package/dist/esm/list-sort-order.js.map +1 -0
  152. package/dist/esm/pagination-controls.d.ts +22 -3
  153. package/dist/esm/pagination-controls.d.ts.map +1 -1
  154. package/dist/esm/pagination-controls.js +28 -2
  155. package/dist/esm/pagination-controls.js.map +1 -1
  156. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  157. package/dist/esm/pagination-plugin.js +271 -216
  158. package/dist/esm/pagination-plugin.js.map +1 -1
  159. package/dist/esm/product-price-sort.d.ts +0 -9
  160. package/dist/esm/product-price-sort.d.ts.map +1 -1
  161. package/dist/esm/product-price-sort.js +0 -11
  162. package/dist/esm/product-price-sort.js.map +1 -1
  163. package/dist/esm/range-control.d.ts +45 -0
  164. package/dist/esm/range-control.d.ts.map +1 -0
  165. package/dist/esm/range-control.js +137 -0
  166. package/dist/esm/range-control.js.map +1 -0
  167. package/dist/esm/row-order.d.ts +20 -0
  168. package/dist/esm/row-order.d.ts.map +1 -0
  169. package/dist/esm/row-order.js +41 -0
  170. package/dist/esm/row-order.js.map +1 -0
  171. package/dist/esm/search-input.d.ts +42 -0
  172. package/dist/esm/search-input.d.ts.map +1 -0
  173. package/dist/esm/search-input.js +129 -0
  174. package/dist/esm/search-input.js.map +1 -0
  175. package/dist/esm/sort-utils.d.ts +10 -0
  176. package/dist/esm/sort-utils.d.ts.map +1 -1
  177. package/dist/esm/sort-utils.js +20 -0
  178. package/dist/esm/sort-utils.js.map +1 -1
  179. package/dist/esm/transformations/blog-adjacent-posts.d.ts +11 -1
  180. package/dist/esm/transformations/blog-adjacent-posts.d.ts.map +1 -1
  181. package/dist/esm/transformations/blog-adjacent-posts.js +14 -1
  182. package/dist/esm/transformations/blog-adjacent-posts.js.map +1 -1
  183. package/dist/esm/transformations/blog-comments.d.ts +1 -1
  184. package/dist/esm/transformations/blog-comments.d.ts.map +1 -1
  185. package/dist/esm/transformations/blog-comments.js +1 -1
  186. package/dist/esm/transformations/blog-comments.js.map +1 -1
  187. package/dist/esm/transformations/blog-post.d.ts +14 -0
  188. package/dist/esm/transformations/blog-post.d.ts.map +1 -1
  189. package/dist/esm/transformations/blog-post.js +4 -1
  190. package/dist/esm/transformations/blog-post.js.map +1 -1
  191. package/dist/esm/transformations/booking-item.d.ts +24 -0
  192. package/dist/esm/transformations/booking-item.d.ts.map +1 -0
  193. package/dist/esm/transformations/booking-item.js +23 -0
  194. package/dist/esm/transformations/booking-item.js.map +1 -0
  195. package/dist/esm/transformations/heading-anchors.d.ts +32 -8
  196. package/dist/esm/transformations/heading-anchors.d.ts.map +1 -1
  197. package/dist/esm/transformations/heading-anchors.js +33 -9
  198. package/dist/esm/transformations/heading-anchors.js.map +1 -1
  199. package/dist/esm/transformations/index.d.ts +27 -13
  200. package/dist/esm/transformations/index.d.ts.map +1 -1
  201. package/dist/esm/transformations/index.js +72 -16
  202. package/dist/esm/transformations/index.js.map +1 -1
  203. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  204. package/dist/esm/utils.d.ts.map +1 -1
  205. package/dist/esm/utils.js +52 -3
  206. package/dist/esm/utils.js.map +1 -1
  207. package/package.json +5 -5
  208. package/src/count-effect.ts +31 -8
  209. package/src/fetchers/pg-client-import.ts +16 -0
  210. package/src/fetchers/postgresql.ts +25 -23
  211. package/src/fetchers/raw-query.ts +2 -1
  212. package/src/fetchers/rest-api.ts +3 -1
  213. package/src/fetchers/teleport.ts +22 -23
  214. package/src/fetchers/utils/browser-row-policy.ts +10 -20
  215. package/src/filter-utils.ts +102 -28
  216. package/src/infinite-scroll.ts +240 -79
  217. package/src/list-sort-order.ts +34 -0
  218. package/src/pagination-controls.ts +26 -3
  219. package/src/pagination-plugin.ts +370 -306
  220. package/src/product-price-sort.ts +0 -11
  221. package/src/range-control.ts +207 -0
  222. package/src/row-order.ts +55 -0
  223. package/src/search-input.ts +190 -0
  224. package/src/sort-utils.ts +25 -0
  225. package/src/transformations/blog-adjacent-posts.ts +89 -3
  226. package/src/transformations/blog-comments.ts +5 -5
  227. package/src/transformations/blog-post.ts +64 -13
  228. package/src/transformations/booking-item.ts +71 -0
  229. package/src/transformations/heading-anchors.ts +79 -16
  230. package/src/transformations/index.ts +104 -14
  231. package/src/utils.ts +74 -3
@@ -0,0 +1,215 @@
1
+ import * as types from '@babel/types'
2
+ import generator from '@babel/generator'
3
+ import {
4
+ ChunkType,
5
+ FileType,
6
+ ComponentStructure,
7
+ ChunkDefinition,
8
+ } from '@teleporthq/teleport-types'
9
+ import { createNextArrayMapperPaginationPlugin } from '../src/pagination-plugin'
10
+
11
+ /**
12
+ * A PLAIN list — no pagination, no search — fetches with the sort its
13
+ * repeater carries, like every other list: the three paginated / searchable
14
+ * kinds rebuild their params with it, while a plain list's params were kept
15
+ * as they came and the sort never reached the fetch, so the rows arrived in
16
+ * the table's own order whatever the editor said.
17
+ */
18
+
19
+ const element = (name: string, attributes: types.JSXAttribute[] = []) =>
20
+ types.jsxElement(
21
+ types.jsxOpeningElement(types.jsxIdentifier(name), attributes, true),
22
+ null,
23
+ [],
24
+ true
25
+ )
26
+
27
+ const makeDataProviderJSX = (): types.JSXElement =>
28
+ element('DataProvider', [
29
+ types.jsxAttribute(
30
+ types.jsxIdentifier('name'),
31
+ types.jsxExpressionContainer(types.stringLiteral('items'))
32
+ ),
33
+ types.jsxAttribute(
34
+ types.jsxIdentifier('renderSuccess'),
35
+ types.jsxExpressionContainer(
36
+ types.arrowFunctionExpression(
37
+ [types.identifier('items')],
38
+ element('Repeater', [
39
+ types.jsxAttribute(
40
+ types.jsxIdentifier('renderItem'),
41
+ types.jsxExpressionContainer(
42
+ types.arrowFunctionExpression([types.identifier('post')], element('div'))
43
+ )
44
+ ),
45
+ ])
46
+ )
47
+ )
48
+ ),
49
+ ])
50
+
51
+ const makeComponentChunk = (): ChunkDefinition => ({
52
+ name: 'jsx-component',
53
+ type: ChunkType.AST,
54
+ fileType: FileType.JS,
55
+ linkAfter: [],
56
+ content: types.variableDeclaration('const', [
57
+ types.variableDeclarator(
58
+ types.identifier('PostsList'),
59
+ types.arrowFunctionExpression(
60
+ [types.identifier('props')],
61
+ types.blockStatement([types.returnStatement(makeDataProviderJSX())])
62
+ )
63
+ ),
64
+ ]),
65
+ meta: {},
66
+ })
67
+
68
+ type SortKind = 'fixed' | 'state' | 'none'
69
+
70
+ const SORTS: Record<SortKind, Record<string, unknown>> = {
71
+ fixed: {
72
+ sort: { type: 'static', content: 'title' },
73
+ sortDirection: { type: 'static', content: 'asc' },
74
+ },
75
+ state: {
76
+ sort: { type: 'expr', content: 'sortBy.split("-")[0]' },
77
+ sortDirection: { type: 'expr', content: 'sortBy.split("-")[1]' },
78
+ },
79
+ none: {},
80
+ }
81
+
82
+ const runPlugin = async (
83
+ sort: SortKind,
84
+ options: { legacySorts?: boolean; params?: boolean; prefetched?: boolean } = {}
85
+ ): Promise<string> => {
86
+ const chunk = makeComponentChunk()
87
+ if (options.prefetched) {
88
+ // A page's list, seeded with its server prefetch.
89
+ const provider = (
90
+ (chunk.content as types.VariableDeclaration).declarations[0]
91
+ .init as types.ArrowFunctionExpression
92
+ ).body as types.BlockStatement
93
+ const jsx = (provider.body[0] as types.ReturnStatement).argument as types.JSXElement
94
+ jsx.openingElement.attributes.push(
95
+ types.jsxAttribute(
96
+ types.jsxIdentifier('initialData'),
97
+ types.jsxExpressionContainer(
98
+ types.memberExpression(types.identifier('props'), types.identifier('items'))
99
+ )
100
+ )
101
+ )
102
+ }
103
+ if (options.params) {
104
+ // The params the data-source plugin writes for a plain list: its filters.
105
+ const provider = (
106
+ (chunk.content as types.VariableDeclaration).declarations[0]
107
+ .init as types.ArrowFunctionExpression
108
+ ).body as types.BlockStatement
109
+ const jsx = (provider.body[0] as types.ReturnStatement).argument as types.JSXElement
110
+ jsx.openingElement.attributes.push(
111
+ types.jsxAttribute(
112
+ types.jsxIdentifier('params'),
113
+ types.jsxExpressionContainer(
114
+ types.objectExpression([
115
+ types.objectProperty(types.identifier('filters'), types.stringLiteral('[]')),
116
+ ])
117
+ )
118
+ )
119
+ )
120
+ }
121
+ const structure: ComponentStructure = {
122
+ uidl: {
123
+ name: 'PostsList',
124
+ node: {
125
+ type: 'data-source-list',
126
+ content: {
127
+ renderPropIdentifier: 'items',
128
+ resourceDefinition: {
129
+ dataSourceId: 'ds1',
130
+ tableName: 'posts',
131
+ dataSourceType: 'postgresql',
132
+ },
133
+ resource: {
134
+ params: {
135
+ queryColumns: { content: ['title'] },
136
+ ...(options.legacySorts
137
+ ? { sorts: { content: [{ field: 'created_at', order: 'desc' }] } }
138
+ : {}),
139
+ },
140
+ },
141
+ nodes: {
142
+ success: {
143
+ type: 'cms-list-repeater',
144
+ content: {
145
+ renderPropIdentifier: 'post',
146
+ nodes: { list: { type: 'element', content: { elementType: 'div' } } },
147
+ ...SORTS[sort],
148
+ },
149
+ },
150
+ },
151
+ },
152
+ },
153
+ },
154
+ chunks: [chunk],
155
+ dependencies: {},
156
+ options: {
157
+ dataSources: {
158
+ ds1: {
159
+ id: 'ds1',
160
+ name: 'Content',
161
+ type: 'postgresql',
162
+ config: { host: 'h', database: 'd', user: 'u', password: 'p' },
163
+ },
164
+ },
165
+ extractedResources: {},
166
+ },
167
+ } as never
168
+ await createNextArrayMapperPaginationPlugin()(structure)
169
+ return generator(chunk.content as types.Node).code
170
+ }
171
+
172
+ const squash = (code: string) => code.replace(/\s+/g, ' ')
173
+
174
+ describe('a plain list fetches with its repeater’s sort', () => {
175
+ it('sends a sort the editor fixed', async () => {
176
+ const code = squash(await runPlugin('fixed'))
177
+ expect(code).toContain(
178
+ 'params={useMemo(() => ({ sorts: JSON.stringify([{ field: "title", order: "asc" }]) }), [])}'
179
+ )
180
+ })
181
+
182
+ it('sends a sort bound to state, and refetches when the state changes', async () => {
183
+ const code = squash(await runPlugin('state'))
184
+ expect(code).toContain(
185
+ 'sorts: JSON.stringify([{ field: sortBy.split("-")?.[0], order: sortBy.split("-")?.[1] }])'
186
+ )
187
+ expect(code).toMatch(/\}\), \[sortBy\]\)\}/)
188
+ })
189
+
190
+ it('adds the sort beside the params the list already had', async () => {
191
+ const code = squash(await runPlugin('fixed', { params: true }))
192
+ expect(code).toContain(
193
+ 'params={useMemo(() => ({ filters: "[]", sorts: JSON.stringify([{ field: "title", order: "asc" }]) }), [])}'
194
+ )
195
+ })
196
+
197
+ it('keeps the resource’s own sorts first, as every other list does', async () => {
198
+ const code = squash(await runPlugin('fixed', { legacySorts: true }))
199
+ expect(code).toContain('sorts: JSON.stringify([{ field: "created_at", order: "desc" }])')
200
+ expect(code).not.toContain('field: "title"')
201
+ })
202
+
203
+ it('drops the prefetched rows for a sort bound to state, which the server cannot know', async () => {
204
+ expect(squash(await runPlugin('state', { prefetched: true }))).not.toContain('initialData')
205
+ expect(squash(await runPlugin('fixed', { prefetched: true }))).toContain(
206
+ 'initialData={props.items}'
207
+ )
208
+ })
209
+
210
+ it('leaves a list without a sort as it was', async () => {
211
+ const code = squash(await runPlugin('none'))
212
+ expect(code).not.toContain('sorts')
213
+ expect(code).not.toContain('params=')
214
+ })
215
+ })
@@ -5,12 +5,12 @@ import {
5
5
  buildProductEffectivePriceSql,
6
6
  generateSortFallbackFieldHelper,
7
7
  generateSortFieldSqlHelper,
8
- generateSortTiebreakSql,
9
8
  supportsProductPriceSort,
10
9
  } from '../src/product-price-sort'
11
10
  import { generatePostgreSQLFetcher } from '../src/fetchers/postgresql'
12
11
  import { generateTeleportFetcher } from '../src/fetchers/teleport'
13
12
  import { generateDataSourceFetcher } from '../src/data-source-fetchers'
13
+ import { generateRowOrderTiebreakHelper, rowIdentityColumn } from '../src/row-order'
14
14
 
15
15
  /**
16
16
  * Ordering a products list by what customers actually pay.
@@ -121,8 +121,8 @@ describe('the generated Postgres route', () => {
121
121
  it('keeps a plain-column ORDER BY to fall back to', () => {
122
122
  // If a database ever rejects the sub-select, the page must degrade to the
123
123
  // previous ordering rather than serve nothing.
124
- expect(productsRoute).toContain('const usedDiscountAwareSort = orderBySql !== plainOrderBySql')
125
- expect(productsRoute).toContain('if (!usedDiscountAwareSort) throw sortError')
124
+ expect(productsRoute).toContain('const usedRewrittenSort = orderBySql !== plainOrderBySql')
125
+ expect(productsRoute).toContain('if (!usedRewrittenSort) throw sortError')
126
126
  expect(productsRoute).toContain('baseSql + plainOrderBySql + sqlTail')
127
127
  })
128
128
 
@@ -201,23 +201,57 @@ describe('degrading safely', () => {
201
201
  })
202
202
 
203
203
  describe('stable pagination', () => {
204
- it('breaks ties on the products table so pages cannot repeat or skip rows', () => {
205
- // "Sort by discount" ties every undiscounted product at zero. Equal rows
206
- // with no defined order can appear on two pages, or on none.
207
- expect(generateSortTiebreakSql('teleport_products')).toBe(', "id" ASC')
208
- })
209
-
210
- it('adds nothing to a table it does not own', () => {
211
- expect(generateSortTiebreakSql('teleport_orders')).toBe('')
212
- })
213
-
214
- it('is applied to both the real and the fallback ORDER BY', () => {
215
- const route = generatePostgreSQLFetcher(PG_CONFIG, 'teleport_products')
216
- const orderByLines = route
217
- .split('\n')
218
- .filter((line) => line.includes('ORDER BY') && line.includes('join'))
219
- expect(orderByLines).toHaveLength(2)
220
- orderByLines.forEach((line) => expect(line).toContain('"id" ASC'))
204
+ // Rows that share every requested key have no order of their own: "sort by
205
+ // discount" ties every undiscounted product at zero, an import dates hundreds
206
+ // of articles alike. Equal rows can appear on two pages, or on none.
207
+ const tiebreakOf = (identityColumn: string | null) =>
208
+ // eslint-disable-next-line no-new-func
209
+ new Function(`${generateRowOrderTiebreakHelper(identityColumn)}\nreturn rowOrderTiebreak`)()
210
+
211
+ it("ends every order with the row's own key, in the order's direction", () => {
212
+ const tiebreak = tiebreakOf('id')
213
+ expect(tiebreak(['created_at'], 'DESC')).toBe(', "id" DESC')
214
+ expect(tiebreak(['price'], 'ASC')).toBe(', "id" ASC')
215
+ expect(tiebreak(['id'], 'ASC')).toBe('')
216
+ expect(tiebreakOf(null)(['created_at'], 'DESC')).toBe('')
217
+ })
218
+
219
+ it("takes the table's primary key, else its id, and the products table's id", () => {
220
+ const saved = (columns: unknown[]) => ({ selectedTables: { posts: { columns } } })
221
+ expect(
222
+ rowIdentityColumn(saved([{ name: 'sku', isPrimaryKey: true }, { name: 'id' }]), 'posts')
223
+ ).toBe('sku')
224
+ expect(rowIdentityColumn(saved([{ name: 'title' }, { name: 'id' }]), 'posts')).toBe('id')
225
+ expect(rowIdentityColumn(saved([{ name: 'title' }]), 'posts')).toBeNull()
226
+ expect(rowIdentityColumn({}, 'teleport_products')).toBe('id')
227
+ expect(rowIdentityColumn({}, 'teleport_orders')).toBeNull()
228
+ })
229
+
230
+ it('breaks ties on every listing a visitor sorts: the blog listing and the booking page too', () => {
231
+ // "Sort by reading time" ties every five-minute post, "by price" every
232
+ // item at the same price.
233
+ expect(rowIdentityColumn({}, 'teleport_blog_posts')).toBe('id')
234
+ expect(rowIdentityColumn({}, 'teleport_booking_items')).toBe('id')
235
+ })
236
+
237
+ it('is applied to the real and the fallback ORDER BY, and to a single sort column', () => {
238
+ for (const route of [
239
+ generatePostgreSQLFetcher(PG_CONFIG, 'teleport_products'),
240
+ generateTeleportFetcher(
241
+ {
242
+ selectedTables: {
243
+ teleport_help_articles: { columns: [{ name: 'id', isPrimaryKey: true }] },
244
+ },
245
+ },
246
+ 'teleport_help_articles'
247
+ ),
248
+ ]) {
249
+ const orderByLines = route.split('\n').filter((line) => line.includes('ORDER BY ${'))
250
+ expect(orderByLines.length).toBeGreaterThanOrEqual(3)
251
+ orderByLines.forEach((line) => expect(line).toMatch(/tiebreak|rowOrderTiebreak/))
252
+ expect(route).toContain('const rowOrderTiebreak = (fields, direction) =>')
253
+ expect(() => parse(route, { sourceType: 'module', plugins: ['jsx'] })).not.toThrow()
254
+ }
221
255
  })
222
256
  })
223
257
 
@@ -247,12 +281,11 @@ describe('the fetcher a generated storefront actually gets', () => {
247
281
  })
248
282
 
249
283
  it('keeps the plain-column fallback and the tiebreaker', () => {
250
- expect(teleportProducts).toContain(
251
- 'const usedDiscountAwareSort = orderBySql !== plainOrderBySql'
252
- )
253
- expect(teleportProducts).toContain('if (!usedDiscountAwareSort) throw sortError')
284
+ expect(teleportProducts).toContain('const usedRewrittenSort = orderBySql !== plainOrderBySql')
285
+ expect(teleportProducts).toContain('if (!usedRewrittenSort) throw sortError')
254
286
  expect(teleportProducts).toContain('baseSql + plainOrderBySql + sqlTail')
255
- expect(teleportProducts).toContain('"id" ASC')
287
+ expect(teleportProducts).toContain('const rowOrderTiebreak = (fields, direction) =>')
288
+ expect(teleportProducts).toMatch(/\$\{tiebreak\}/)
256
289
  })
257
290
 
258
291
  it('parses as valid JavaScript', () => {
@@ -0,0 +1,159 @@
1
+ import * as types from '@babel/types'
2
+ import generator from '@babel/generator'
3
+ import { ComponentStructure } from '@teleporthq/teleport-types'
4
+ import { createNextArrayMapperPaginationPlugin } from '../src/pagination-plugin'
5
+ import {
6
+ makeUidlNode,
7
+ makeWidgetComponentChunk,
8
+ RANGE_DRAWN_TEXT,
9
+ type RepeaterOptions,
10
+ } from './_helpers/pagination-widget-fixture'
11
+
12
+ type ControlOptions = Parameters<typeof makeWidgetComponentChunk>[0]
13
+
14
+ const runPlugin = async (repeater: RepeaterOptions, controls: ControlOptions): Promise<string> => {
15
+ const chunk = makeWidgetComponentChunk(controls)
16
+ const structure: ComponentStructure = {
17
+ uidl: { name: 'TestComponent', node: makeUidlNode(repeater) },
18
+ chunks: [chunk],
19
+ dependencies: {},
20
+ options: { dataSources: {}, extractedResources: {} },
21
+ } as never
22
+ await createNextArrayMapperPaginationPlugin()(structure)
23
+ return generator(chunk.content as types.Node).code
24
+ }
25
+
26
+ const collapse = (code: string): string => code.replace(/\s+/g, ' ')
27
+
28
+ const TOTAL_STATE = 'const [ds_0_total, setDs_0_total] = useState(null)'
29
+ const DRAWN = `ds_0_total === null ? "${RANGE_DRAWN_TEXT}" :`
30
+ const FROM_BY_PAGE = '(ds_0_total === 0 ? 0 : (ds_0_page - 1) * 20 + 1).toLocaleString("en-US")'
31
+ const FROM_FIRST_ROW = '(ds_0_total === 0 ? 0 : 1).toLocaleString("en-US")'
32
+ const TO = 'Math.min(ds_0_page * 20, ds_0_total).toLocaleString("en-US")'
33
+ const TOTAL = 'ds_0_total.toLocaleString("en-US")'
34
+
35
+ describe('pagination plugin — range control', () => {
36
+ it('fills the authored words with the page window in buttons mode', async () => {
37
+ const flat = collapse(await runPlugin({}, { controls: ['previous', 'range', 'next'] }))
38
+
39
+ // The total is a state of its own — `maxPages` cannot give it back — and
40
+ // the count effect fills both from the same response.
41
+ expect(flat).toContain(TOTAL_STATE)
42
+ expect(flat).toContain('setDs_0_maxPages(')
43
+ expect(flat).toContain('setDs_0_total(data.count)')
44
+
45
+ // The drawn text stays until the count has answered; then the template.
46
+ expect(flat).toContain(`${DRAWN} \`Showing \${${FROM_BY_PAGE}}–\${${TO}} of \${${TOTAL}}\``)
47
+ // The marker and template ride along, so the DOM carries the contract.
48
+ expect(flat).toContain('data-tq-pagination-control="range"')
49
+ expect(flat).toContain('data-tq-range-template=')
50
+ })
51
+
52
+ it('counts from the first row in an appending list and survives the mode pruning', async () => {
53
+ const flat = collapse(
54
+ await runPlugin(
55
+ { infiniteScroll: true, infiniteScrollLoadMore: true },
56
+ { controls: ['previous', 'range', 'load-more', 'next'] }
57
+ )
58
+ )
59
+
60
+ // Previous/Next are pruned for an appending list; the range is not.
61
+ expect(flat).not.toContain('thq-previous-elm')
62
+ expect(flat).not.toContain('thq-next-elm')
63
+ expect(flat).toContain('thq-load-more-elm')
64
+ expect(flat).toContain('data-tq-pagination-control="range"')
65
+
66
+ // Everything loaded so far: 1 .. min(pages × perPage, total).
67
+ expect(flat).toContain(`${DRAWN} \`Showing \${${FROM_FIRST_ROW}}–\${${TO}} of \${${TOTAL}}\``)
68
+ expect(flat).not.toContain(FROM_BY_PAGE)
69
+ })
70
+
71
+ it('is kept and wired beside a sentinel-driven infinite scroll too', async () => {
72
+ const flat = collapse(await runPlugin({ infiniteScroll: true }, { controls: ['range'] }))
73
+
74
+ expect(flat).toContain('ds_0_sentinelRef')
75
+ expect(flat).toContain(TOTAL_STATE)
76
+ expect(flat).toContain(FROM_FIRST_ROW)
77
+ })
78
+
79
+ it('survives numbered mode with the page window', async () => {
80
+ const flat = collapse(
81
+ await runPlugin(
82
+ { paginationMode: 'numbered' },
83
+ { controls: ['first', 'previous', 'pages', 'range', 'next', 'last'] }
84
+ )
85
+ )
86
+
87
+ expect(flat).toContain('ds_0_pageTokens.map(token =>')
88
+ expect(flat).toContain('data-tq-pagination-control="range"')
89
+ expect(flat).toContain(FROM_BY_PAGE)
90
+ })
91
+
92
+ it('reads the page off the combined state beside a search input', async () => {
93
+ const flat = collapse(
94
+ await runPlugin({ searchEnabled: true }, { controls: ['previous', 'range', 'next'] })
95
+ )
96
+
97
+ expect(flat).toContain(TOTAL_STATE)
98
+ expect(flat).toContain('setDs_0_total(data.count)')
99
+ expect(flat).toContain(
100
+ '(ds_0_total === 0 ? 0 : (ds_0_state.page - 1) * 20 + 1).toLocaleString("en-US")'
101
+ )
102
+ expect(flat).toContain('Math.min(ds_0_state.page * 20, ds_0_total).toLocaleString("en-US")')
103
+ })
104
+
105
+ it('reads a template the JSX printer escaped the braces of', async () => {
106
+ const flat = collapse(
107
+ await runPlugin(
108
+ {},
109
+ {
110
+ controls: ['range'],
111
+ rangeTemplate: 'Showing &#123;from&#125;–&#123;to&#125; of &#123;total&#125;',
112
+ }
113
+ )
114
+ )
115
+ expect(flat).not.toContain('`Showing &#123;')
116
+ expect(flat).toMatch(/`Showing \$\{[^`]*–\$\{[^`]*of \$\{[^`]*`/)
117
+ })
118
+
119
+ it('falls back to "{from}–{to} of {total}" when no template is authored', async () => {
120
+ const flat = collapse(await runPlugin({}, { controls: ['range'], rangeTemplate: null }))
121
+
122
+ expect(flat).not.toContain('data-tq-range-template')
123
+ expect(flat).toContain(`${DRAWN} \`\${${FROM_BY_PAGE}}–\${${TO}} of \${${TOTAL}}\``)
124
+ })
125
+
126
+ it('wires a range outside every widget to the mapper it names, wherever it sits', async () => {
127
+ // Placed BEFORE the provider — a toolbar above the list — so the
128
+ // container-ownership rule could never have reached it.
129
+ const flat = collapse(
130
+ await runPlugin(
131
+ { nodeId: 'mapper-1' },
132
+ { controls: ['previous', 'next'], standaloneRange: { forNodeId: 'mapper-1' } }
133
+ )
134
+ )
135
+
136
+ expect(flat).toContain('data-tq-pagination-for="mapper-1"')
137
+ expect(flat).toContain(TOTAL_STATE)
138
+ expect(flat).toContain(`${DRAWN} \`Showing \${${FROM_BY_PAGE}}–\${${TO}} of \${${TOTAL}}\``)
139
+ })
140
+
141
+ it('leaves a range naming a mapper this component does not render as drawn', async () => {
142
+ const flat = collapse(
143
+ await runPlugin(
144
+ { nodeId: 'mapper-1' },
145
+ { controls: ['previous', 'next'], standaloneRange: { forNodeId: 'someone-else' } }
146
+ )
147
+ )
148
+
149
+ expect(flat).toContain(`>${RANGE_DRAWN_TEXT}</div>`)
150
+ expect(flat).not.toContain('ds_0_total === null')
151
+ })
152
+
153
+ it('changes nothing for a component without a range control', async () => {
154
+ const flat = collapse(await runPlugin({}, { controls: ['previous', 'next'] }))
155
+
156
+ expect(flat).not.toContain('ds_0_total')
157
+ expect(flat).not.toContain('setDs_0_total')
158
+ })
159
+ })
@@ -0,0 +1,18 @@
1
+ import { generateRESTAPIFetcher } from '../src/fetchers/rest-api'
2
+
3
+ describe('REST API fetcher with API key authorization', () => {
4
+ /*
5
+ * The editor's data fetcher sends the key as `X-API-Key`. The live site sent
6
+ * `Authorization: ApiKey …`, so a source that worked on the canvas failed once
7
+ * published.
8
+ */
9
+ it('sends the key in the same header the editor sends', () => {
10
+ const code = generateRESTAPIFetcher({
11
+ url: 'https://api.example.com/items',
12
+ method: 'GET',
13
+ authorization: { type: 'api-key', credentials: { apiKey: 'k-1' } },
14
+ })
15
+ expect(code).toContain("headers['X-API-Key'] = 'k-1'")
16
+ expect(code).not.toContain('ApiKey ')
17
+ })
18
+ })
@@ -0,0 +1,86 @@
1
+ import * as types from '@babel/types'
2
+ import generator from '@babel/generator'
3
+ import { ComponentStructure } from '@teleporthq/teleport-types'
4
+ import { createNextArrayMapperPaginationPlugin } from '../src/pagination-plugin'
5
+ import {
6
+ makeMultiMapperUidl,
7
+ makeWidgetComponentChunk,
8
+ type RepeaterOptions,
9
+ } from './_helpers/pagination-widget-fixture'
10
+
11
+ type ControlOptions = Parameters<typeof makeWidgetComponentChunk>[0]
12
+
13
+ /**
14
+ * A page's OWN search box, marked `data-tq-search-for` with its mapper's node
15
+ * id, is wired like the platform's `search-input`: controlled by the mapper's
16
+ * query state. It is paired by the id it names, never by its place among the
17
+ * platform inputs.
18
+ */
19
+
20
+ const runPlugin = async (
21
+ repeaters: RepeaterOptions[],
22
+ controls: ControlOptions
23
+ ): Promise<string> => {
24
+ const chunk = makeWidgetComponentChunk(controls)
25
+ const structure: ComponentStructure = {
26
+ uidl: { name: 'TestComponent', node: makeMultiMapperUidl(repeaters) },
27
+ chunks: [chunk],
28
+ dependencies: {},
29
+ options: { dataSources: {}, extractedResources: {} },
30
+ } as never
31
+ await createNextArrayMapperPaginationPlugin()(structure)
32
+ return generator(chunk.content as types.Node).code
33
+ }
34
+
35
+ const collapse = (code: string): string => code.replace(/\s+/g, ' ')
36
+
37
+ const wiredTo = (index: number): string =>
38
+ `onChange={e => setDs_${index}_searchQuery(e.target.value)} value={ds_${index}_searchQuery}`
39
+
40
+ describe('pagination plugin — a page’s own search box', () => {
41
+ it('controls a marked input by the query of the mapper it names, and keeps Enter from submitting', async () => {
42
+ const flat = collapse(
43
+ await runPlugin([{ searchEnabled: true, nodeId: 'TQ_orders' }], {
44
+ controls: ['previous', 'next'],
45
+ standaloneSearch: [{ name: 'hm-search', forNodeId: 'TQ_orders' }],
46
+ })
47
+ )
48
+ expect(flat).toContain(`name="hm-search" data-tq-search-for="TQ_orders" ${wiredTo(0)}`)
49
+ expect(flat).toContain('onKeyDown={e => { if (e.key === "Enter") e.preventDefault(); }}')
50
+ })
51
+
52
+ it('pairs by the id named, whatever the order: the platform input keeps its place', async () => {
53
+ const flat = collapse(
54
+ await runPlugin(
55
+ [
56
+ { searchEnabled: true, nodeId: 'TQ_jobs' },
57
+ { searchEnabled: true, nodeId: 'TQ_orders' },
58
+ ],
59
+ {
60
+ controls: ['previous', 'next'],
61
+ mappers: 2,
62
+ standaloneSearch: [
63
+ { name: 'hm-search', forNodeId: 'TQ_orders' },
64
+ { name: 'platform-search' },
65
+ ],
66
+ }
67
+ )
68
+ )
69
+ // The marked box searches the second mapper; the platform box, first of its kind, the first.
70
+ expect(flat).toContain(`data-tq-search-for="TQ_orders" ${wiredTo(1)}`)
71
+ expect(flat).toContain(
72
+ `name="platform-search" className="products-list-search-input" ${wiredTo(0)}`
73
+ )
74
+ })
75
+
76
+ it('leaves a marked input alone when no search-enabled mapper carries its id', async () => {
77
+ const flat = collapse(
78
+ await runPlugin([{ searchEnabled: true, nodeId: 'TQ_orders' }], {
79
+ controls: ['previous', 'next'],
80
+ standaloneSearch: [{ name: 'hm-search', forNodeId: 'TQ_elsewhere' }],
81
+ })
82
+ )
83
+ expect(flat).toContain('name="hm-search" data-tq-search-for="TQ_elsewhere" />')
84
+ expect(flat).not.toContain('setDs_0_searchQuery(e.target.value)')
85
+ })
86
+ })