@teleporthq/teleport-plugin-next-data-source 0.43.75 → 0.44.1

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 (199) hide show
  1. package/__tests__/_helpers/first-page-runtime.ts +184 -0
  2. package/__tests__/_helpers/pagination-widget-fixture.ts +154 -5
  3. package/__tests__/booking-item-transform.test.ts +120 -0
  4. package/__tests__/browser-row-policy.test.ts +210 -4
  5. package/__tests__/data-source-request-locale.test.ts +4 -3
  6. package/__tests__/fetchers.test.ts +7 -3
  7. package/__tests__/fixed-list-sort.test.ts +1 -1
  8. package/__tests__/infinite-scroll.test.ts +363 -9
  9. package/__tests__/list-sort-order.test.ts +61 -0
  10. package/__tests__/null-filter-where.test.ts +132 -0
  11. package/__tests__/page-data-visitor-read.test.ts +196 -0
  12. package/__tests__/pg-date-parser.test.ts +73 -0
  13. package/__tests__/plain-list-sort.test.ts +2 -2
  14. package/__tests__/plain-list-starting-sort.test.ts +323 -0
  15. package/__tests__/product-price-sort.ts +11 -6
  16. package/__tests__/range-control.test.ts +159 -0
  17. package/__tests__/refetch-loading-state.test.ts +158 -2
  18. package/__tests__/search-input-marker.test.ts +86 -0
  19. package/__tests__/search-url-sync.test.ts +34 -8
  20. package/__tests__/starting-sort-first-page.test.ts +285 -0
  21. package/__tests__/table-read-route-protected-tables.test.ts +33 -3
  22. package/dist/cjs/cache/ast.d.ts +2 -0
  23. package/dist/cjs/cache/ast.d.ts.map +1 -1
  24. package/dist/cjs/cache/ast.js +5 -2
  25. package/dist/cjs/cache/ast.js.map +1 -1
  26. package/dist/cjs/count-effect.d.ts +7 -1
  27. package/dist/cjs/count-effect.d.ts.map +1 -1
  28. package/dist/cjs/count-effect.js +15 -5
  29. package/dist/cjs/count-effect.js.map +1 -1
  30. package/dist/cjs/data-source-fetchers.d.ts.map +1 -1
  31. package/dist/cjs/data-source-fetchers.js +10 -2
  32. package/dist/cjs/data-source-fetchers.js.map +1 -1
  33. package/dist/cjs/fetchers/pg-client-import.d.ts +12 -0
  34. package/dist/cjs/fetchers/pg-client-import.d.ts.map +1 -0
  35. package/dist/cjs/fetchers/pg-client-import.js +15 -0
  36. package/dist/cjs/fetchers/pg-client-import.js.map +1 -0
  37. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  38. package/dist/cjs/fetchers/postgresql.js +3 -1
  39. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  40. package/dist/cjs/fetchers/raw-query.d.ts.map +1 -1
  41. package/dist/cjs/fetchers/raw-query.js +2 -1
  42. package/dist/cjs/fetchers/raw-query.js.map +1 -1
  43. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  44. package/dist/cjs/fetchers/teleport.js +4 -2
  45. package/dist/cjs/fetchers/teleport.js.map +1 -1
  46. package/dist/cjs/fetchers/utils/browser-row-policy.d.ts.map +1 -1
  47. package/dist/cjs/fetchers/utils/browser-row-policy.js +5 -1
  48. package/dist/cjs/fetchers/utils/browser-row-policy.js.map +1 -1
  49. package/dist/cjs/index.d.ts.map +1 -1
  50. package/dist/cjs/index.js +14 -2
  51. package/dist/cjs/index.js.map +1 -1
  52. package/dist/cjs/infinite-scroll.d.ts +89 -4
  53. package/dist/cjs/infinite-scroll.d.ts.map +1 -1
  54. package/dist/cjs/infinite-scroll.js +243 -33
  55. package/dist/cjs/infinite-scroll.js.map +1 -1
  56. package/dist/cjs/list-sort-order.d.ts +14 -0
  57. package/dist/cjs/list-sort-order.d.ts.map +1 -0
  58. package/dist/cjs/list-sort-order.js +36 -0
  59. package/dist/cjs/list-sort-order.js.map +1 -0
  60. package/dist/cjs/loading-state.d.ts +23 -2
  61. package/dist/cjs/loading-state.d.ts.map +1 -1
  62. package/dist/cjs/loading-state.js +50 -15
  63. package/dist/cjs/loading-state.js.map +1 -1
  64. package/dist/cjs/pagination-controls.d.ts +22 -3
  65. package/dist/cjs/pagination-controls.d.ts.map +1 -1
  66. package/dist/cjs/pagination-controls.js +29 -3
  67. package/dist/cjs/pagination-controls.js.map +1 -1
  68. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  69. package/dist/cjs/pagination-plugin.js +336 -164
  70. package/dist/cjs/pagination-plugin.js.map +1 -1
  71. package/dist/cjs/range-control.d.ts +45 -0
  72. package/dist/cjs/range-control.d.ts.map +1 -0
  73. package/dist/cjs/range-control.js +165 -0
  74. package/dist/cjs/range-control.js.map +1 -0
  75. package/dist/cjs/row-order.d.ts +3 -2
  76. package/dist/cjs/row-order.d.ts.map +1 -1
  77. package/dist/cjs/row-order.js +5 -4
  78. package/dist/cjs/row-order.js.map +1 -1
  79. package/dist/cjs/search-input.d.ts +42 -0
  80. package/dist/cjs/search-input.d.ts.map +1 -0
  81. package/dist/cjs/search-input.js +158 -0
  82. package/dist/cjs/search-input.js.map +1 -0
  83. package/dist/cjs/sort-utils.d.ts +53 -1
  84. package/dist/cjs/sort-utils.d.ts.map +1 -1
  85. package/dist/cjs/sort-utils.js +105 -13
  86. package/dist/cjs/sort-utils.js.map +1 -1
  87. package/dist/cjs/transformations/booking-item.d.ts +24 -0
  88. package/dist/cjs/transformations/booking-item.d.ts.map +1 -0
  89. package/dist/cjs/transformations/booking-item.js +27 -0
  90. package/dist/cjs/transformations/booking-item.js.map +1 -0
  91. package/dist/cjs/transformations/index.d.ts +4 -2
  92. package/dist/cjs/transformations/index.d.ts.map +1 -1
  93. package/dist/cjs/transformations/index.js +14 -5
  94. package/dist/cjs/transformations/index.js.map +1 -1
  95. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  96. package/dist/cjs/utils.d.ts +3 -2
  97. package/dist/cjs/utils.d.ts.map +1 -1
  98. package/dist/cjs/utils.js +48 -18
  99. package/dist/cjs/utils.js.map +1 -1
  100. package/dist/esm/cache/ast.d.ts +2 -0
  101. package/dist/esm/cache/ast.d.ts.map +1 -1
  102. package/dist/esm/cache/ast.js +3 -1
  103. package/dist/esm/cache/ast.js.map +1 -1
  104. package/dist/esm/count-effect.d.ts +7 -1
  105. package/dist/esm/count-effect.d.ts.map +1 -1
  106. package/dist/esm/count-effect.js +15 -5
  107. package/dist/esm/count-effect.js.map +1 -1
  108. package/dist/esm/data-source-fetchers.d.ts.map +1 -1
  109. package/dist/esm/data-source-fetchers.js +10 -2
  110. package/dist/esm/data-source-fetchers.js.map +1 -1
  111. package/dist/esm/fetchers/pg-client-import.d.ts +12 -0
  112. package/dist/esm/fetchers/pg-client-import.d.ts.map +1 -0
  113. package/dist/esm/fetchers/pg-client-import.js +12 -0
  114. package/dist/esm/fetchers/pg-client-import.js.map +1 -0
  115. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  116. package/dist/esm/fetchers/postgresql.js +3 -1
  117. package/dist/esm/fetchers/postgresql.js.map +1 -1
  118. package/dist/esm/fetchers/raw-query.d.ts.map +1 -1
  119. package/dist/esm/fetchers/raw-query.js +2 -1
  120. package/dist/esm/fetchers/raw-query.js.map +1 -1
  121. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  122. package/dist/esm/fetchers/teleport.js +4 -2
  123. package/dist/esm/fetchers/teleport.js.map +1 -1
  124. package/dist/esm/fetchers/utils/browser-row-policy.d.ts.map +1 -1
  125. package/dist/esm/fetchers/utils/browser-row-policy.js +6 -2
  126. package/dist/esm/fetchers/utils/browser-row-policy.js.map +1 -1
  127. package/dist/esm/index.d.ts.map +1 -1
  128. package/dist/esm/index.js +14 -2
  129. package/dist/esm/index.js.map +1 -1
  130. package/dist/esm/infinite-scroll.d.ts +89 -4
  131. package/dist/esm/infinite-scroll.d.ts.map +1 -1
  132. package/dist/esm/infinite-scroll.js +237 -32
  133. package/dist/esm/infinite-scroll.js.map +1 -1
  134. package/dist/esm/list-sort-order.d.ts +14 -0
  135. package/dist/esm/list-sort-order.d.ts.map +1 -0
  136. package/dist/esm/list-sort-order.js +32 -0
  137. package/dist/esm/list-sort-order.js.map +1 -0
  138. package/dist/esm/loading-state.d.ts +23 -2
  139. package/dist/esm/loading-state.d.ts.map +1 -1
  140. package/dist/esm/loading-state.js +50 -15
  141. package/dist/esm/loading-state.js.map +1 -1
  142. package/dist/esm/pagination-controls.d.ts +22 -3
  143. package/dist/esm/pagination-controls.d.ts.map +1 -1
  144. package/dist/esm/pagination-controls.js +28 -2
  145. package/dist/esm/pagination-controls.js.map +1 -1
  146. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  147. package/dist/esm/pagination-plugin.js +340 -168
  148. package/dist/esm/pagination-plugin.js.map +1 -1
  149. package/dist/esm/range-control.d.ts +45 -0
  150. package/dist/esm/range-control.d.ts.map +1 -0
  151. package/dist/esm/range-control.js +137 -0
  152. package/dist/esm/range-control.js.map +1 -0
  153. package/dist/esm/row-order.d.ts +3 -2
  154. package/dist/esm/row-order.d.ts.map +1 -1
  155. package/dist/esm/row-order.js +5 -4
  156. package/dist/esm/row-order.js.map +1 -1
  157. package/dist/esm/search-input.d.ts +42 -0
  158. package/dist/esm/search-input.d.ts.map +1 -0
  159. package/dist/esm/search-input.js +129 -0
  160. package/dist/esm/search-input.js.map +1 -0
  161. package/dist/esm/sort-utils.d.ts +53 -1
  162. package/dist/esm/sort-utils.d.ts.map +1 -1
  163. package/dist/esm/sort-utils.js +100 -12
  164. package/dist/esm/sort-utils.js.map +1 -1
  165. package/dist/esm/transformations/booking-item.d.ts +24 -0
  166. package/dist/esm/transformations/booking-item.d.ts.map +1 -0
  167. package/dist/esm/transformations/booking-item.js +23 -0
  168. package/dist/esm/transformations/booking-item.js.map +1 -0
  169. package/dist/esm/transformations/index.d.ts +4 -2
  170. package/dist/esm/transformations/index.d.ts.map +1 -1
  171. package/dist/esm/transformations/index.js +14 -5
  172. package/dist/esm/transformations/index.js.map +1 -1
  173. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  174. package/dist/esm/utils.d.ts +3 -2
  175. package/dist/esm/utils.d.ts.map +1 -1
  176. package/dist/esm/utils.js +49 -19
  177. package/dist/esm/utils.js.map +1 -1
  178. package/package.json +5 -5
  179. package/src/cache/ast.ts +4 -1
  180. package/src/count-effect.ts +31 -8
  181. package/src/data-source-fetchers.ts +33 -14
  182. package/src/fetchers/pg-client-import.ts +16 -0
  183. package/src/fetchers/postgresql.ts +15 -9
  184. package/src/fetchers/raw-query.ts +2 -1
  185. package/src/fetchers/teleport.ts +19 -10
  186. package/src/fetchers/utils/browser-row-policy.ts +79 -5
  187. package/src/index.ts +8 -2
  188. package/src/infinite-scroll.ts +420 -81
  189. package/src/list-sort-order.ts +34 -0
  190. package/src/loading-state.ts +85 -13
  191. package/src/pagination-controls.ts +26 -3
  192. package/src/pagination-plugin.ts +532 -218
  193. package/src/range-control.ts +207 -0
  194. package/src/row-order.ts +5 -4
  195. package/src/search-input.ts +190 -0
  196. package/src/sort-utils.ts +164 -21
  197. package/src/transformations/booking-item.ts +71 -0
  198. package/src/transformations/index.ts +25 -2
  199. package/src/utils.ts +55 -18
@@ -0,0 +1,184 @@
1
+ /* tslint:disable:function-constructor max-classes-per-file no-any */
2
+ import * as types from '@babel/types'
3
+ import generator from '@babel/generator'
4
+ import { generateDataSourceFetcherWithCore } from '../../src/data-source-fetchers'
5
+
6
+ /**
7
+ * Runs what a generated page asks its data module for: the arguments its
8
+ * `getStaticProps` hands `fetchData`, the params a provider computes in the
9
+ * browser, and the products route both reach — so a test can check that the
10
+ * build and the browser order one row set the same way.
11
+ *
12
+ * `__tests__/_helpers` is excluded from the jest test match, so this file is a
13
+ * fixture rather than a suite.
14
+ */
15
+
16
+ const isFetchDataCall = (node: types.Node): node is types.CallExpression =>
17
+ node.type === 'CallExpression' &&
18
+ node.callee.type === 'MemberExpression' &&
19
+ node.callee.property.type === 'Identifier' &&
20
+ node.callee.property.name === 'fetchData' &&
21
+ node.arguments[0]?.type === 'ObjectExpression'
22
+
23
+ // The `Promise.all` entry whose result lands in `propKey`.
24
+ const prefetchOf = (content: types.Node, propKey: string): types.Node | undefined => {
25
+ let entry: types.Node | undefined
26
+ types.traverseFast(content, (node) => {
27
+ if (
28
+ !entry &&
29
+ node.type === 'VariableDeclarator' &&
30
+ node.id.type === 'ArrayPattern' &&
31
+ node.init?.type === 'AwaitExpression' &&
32
+ node.init.argument.type === 'CallExpression' &&
33
+ node.init.argument.arguments[0]?.type === 'ArrayExpression'
34
+ ) {
35
+ const index = node.id.elements.findIndex(
36
+ (element) => element?.type === 'Identifier' && element.name === propKey
37
+ )
38
+ if (index !== -1) {
39
+ entry = (node.init.argument.arguments[0] as types.ArrayExpression).elements[index] as
40
+ | types.Node
41
+ | undefined
42
+ }
43
+ }
44
+ })
45
+ return entry
46
+ }
47
+
48
+ /**
49
+ * What getStaticProps hands fetchData — `{ page: 1, perPage: 20, …, sorts }`,
50
+ * then who it reads for — for the prefetch filling `propKey`, or the first one.
51
+ */
52
+ export const buildTimeArguments = (
53
+ staticPropsChunk: any,
54
+ propKey?: string
55
+ ): Array<Record<string, unknown>> => {
56
+ const scope = propKey ? prefetchOf(staticPropsChunk.content, propKey) : staticPropsChunk.content
57
+ let call: types.CallExpression | undefined
58
+ types.traverseFast(scope as types.Node, (node) => {
59
+ if (!call && isFetchDataCall(node)) {
60
+ call = node
61
+ }
62
+ })
63
+ const args = call!.arguments.map((arg) => generator(arg as types.Node).code)
64
+ return new Function(`return [${args.join(', ')}]`)()
65
+ }
66
+
67
+ // The `nth` memoized `params` a provider of the component sends.
68
+ const browserParamsBody = (chunk: any, nth: number): types.Expression => {
69
+ const bodies: types.Expression[] = []
70
+ types.traverseFast(chunk.content, (node) => {
71
+ if (
72
+ node.type === 'JSXAttribute' &&
73
+ node.name.name === 'params' &&
74
+ node.value?.type === 'JSXExpressionContainer' &&
75
+ node.value.expression.type === 'CallExpression'
76
+ ) {
77
+ const arrow = node.value.expression.arguments[0]
78
+ if (arrow?.type === 'ArrowFunctionExpression') {
79
+ bodies.push(arrow.body as types.Expression)
80
+ }
81
+ }
82
+ })
83
+ return bodies[nth]
84
+ }
85
+
86
+ /** The provider's params as the browser computes them for the given state, as they go on the wire. */
87
+ export const browserParams = (
88
+ chunk: any,
89
+ state: Record<string, unknown>,
90
+ nth: number = 0
91
+ ): Record<string, string> => {
92
+ const names = Object.keys(state)
93
+ const params = new Function(
94
+ ...names,
95
+ `return (${generator(browserParamsBody(chunk, nth)).code})`
96
+ )(...names.map((name) => state[name]))
97
+ // What `new URLSearchParams(params)` puts on the wire.
98
+ return Object.fromEntries(new URLSearchParams(params))
99
+ }
100
+
101
+ /**
102
+ * The products route as the project ships it, over a database that records
103
+ * the statements it is sent.
104
+ */
105
+ export const bootProductsRoute = () => {
106
+ const dataSource = {
107
+ id: 'ds-1',
108
+ name: 'db',
109
+ type: 'teleport',
110
+ config: {
111
+ host: 'teleporthq.secrets.TELEPORT_DB_HOST',
112
+ port: 5432,
113
+ user: 'teleporthq.secrets.TELEPORT_DB_USER',
114
+ password: 'teleporthq.secrets.TELEPORT_DB_PASSWORD',
115
+ database: 'teleporthq.secrets.TELEPORT_DB_NAME',
116
+ selectedTables: { teleport_products: { columns: [{ name: 'id', type: 'uuid' }] } },
117
+ },
118
+ }
119
+ const code = generateDataSourceFetcherWithCore(dataSource as never, 'teleport_products')
120
+ .replace(
121
+ "import { Client, types as pgTypes } from 'pg'",
122
+ "const { Client, types: pgTypes } = require('pg')"
123
+ )
124
+ .replace(/^export \{[^}]*\}\s*$/m, '')
125
+ .replace(/^export default \{[^}]*\}\s*$/m, '')
126
+ .concat('\nmodule.exports = { handler: handler, fetchData: fetchData }')
127
+ expect(code).not.toMatch(/^(export|import) /m)
128
+
129
+ const statements: string[] = []
130
+ class FakeClient {
131
+ public async connect(): Promise<void> {
132
+ return
133
+ }
134
+ public async end(): Promise<void> {
135
+ return
136
+ }
137
+ public async query(sql: string): Promise<any> {
138
+ statements.push(sql)
139
+ return { rows: [], rowCount: 0 }
140
+ }
141
+ }
142
+ const fakeRequire = (name: string): any => {
143
+ if (name === 'pg') {
144
+ return { Client: FakeClient }
145
+ }
146
+ if (name === 'next-auth/jwt') {
147
+ return { getToken: async (): Promise<null> => null }
148
+ }
149
+ throw new Error('unexpected require: ' + name)
150
+ }
151
+ const moduleObj: { exports: any } = { exports: {} }
152
+ new Function('require', 'module', 'exports', 'process', 'console', code)(
153
+ fakeRequire,
154
+ moduleObj,
155
+ moduleObj.exports,
156
+ {
157
+ env: {
158
+ NEXTAUTH_SECRET: 'secret',
159
+ TELEPORT_DB_HOST: 'h',
160
+ TELEPORT_DB_USER: 'u',
161
+ TELEPORT_DB_PASSWORD: 'p',
162
+ TELEPORT_DB_NAME: 'd',
163
+ },
164
+ },
165
+ { error: (): void => undefined, warn: (): void => undefined, log: (): void => undefined }
166
+ )
167
+ const lastSelect = (): string =>
168
+ statements.filter((sql) => /^\s*SELECT \* FROM/i.test(sql)).pop() || ''
169
+ return {
170
+ atBuild: async (args: Array<Record<string, unknown>>) => {
171
+ await moduleObj.exports.fetchData(...args)
172
+ return lastSelect()
173
+ },
174
+ inBrowser: async (query: Record<string, string>) => {
175
+ const res = { status: () => res, json: () => res, setHeader: () => res }
176
+ await moduleObj.exports.handler({ method: 'GET', query, headers: {} }, res)
177
+ return lastSelect()
178
+ },
179
+ }
180
+ }
181
+
182
+ export const orderBy = (sql: string): string => (sql.match(/ ORDER BY .*? LIMIT/) || [''])[0]
183
+ export const rowWindow = (sql: string): string =>
184
+ (sql.match(/ LIMIT \d+( OFFSET \d+)?$/) || [''])[0]
@@ -1,6 +1,11 @@
1
1
  import * as types from '@babel/types'
2
2
  import { ChunkType, FileType, ChunkDefinition } from '@teleporthq/teleport-types'
3
- import { PAGINATION_CONTROL_ATTR } from '../../src/pagination-controls'
3
+ import {
4
+ PAGINATION_CONTROL_ATTR,
5
+ PAGINATION_FOR_ATTR,
6
+ RANGE_TEMPLATE_ATTR,
7
+ } from '../../src/pagination-controls'
8
+ import { SEARCH_FOR_ATTR } from '../../src/search-input'
4
9
 
5
10
  /**
6
11
  * A component shell carrying a `DataProvider` and the pagination controls a
@@ -20,9 +25,36 @@ interface ControlOptions {
20
25
  legacyOnly?: boolean
21
26
  /** Number of DataProvider + widget pairs to emit (order-based zip coverage). */
22
27
  mappers?: number
28
+ /** The element the providers render in: a table's body, a list. */
29
+ rootTag?: string
30
+ /**
31
+ * The `data-tq-range-template` of a `range` control in the widget. Defaults
32
+ * to the "Showing" wording; `null` authors the control with no template.
33
+ */
34
+ rangeTemplate?: string | null
35
+ /**
36
+ * A `range` text OUTSIDE every widget, placed BEFORE the first provider (a
37
+ * toolbar above the list), naming its mapper with `data-tq-pagination-for`.
38
+ */
39
+ standaloneRange?: { forNodeId: string; template?: string }
40
+ /**
41
+ * Search inputs OUTSIDE every widget, placed BEFORE the first provider, in
42
+ * this order: a page's own box naming its mapper with `data-tq-search-for`,
43
+ * or the platform's (`search-input` class) paired by order.
44
+ */
45
+ standaloneSearch?: Array<{ forNodeId?: string; name: string }>
23
46
  }
24
47
 
25
- const control = (marker: string, children: types.JSXElement['children'] = []): types.JSXElement =>
48
+ // ASCII hyphen on purpose: the generator prints string literals through jsesc,
49
+ // which would turn an en dash into `–` and make every assertion unreadable.
50
+ export const RANGE_DRAWN_TEXT = 'Showing 1-8 of 12'
51
+ export const RANGE_TEMPLATE = 'Showing {from}–{to} of {total}'
52
+
53
+ const control = (
54
+ marker: string,
55
+ children: types.JSXElement['children'] = [],
56
+ extraAttrs: Array<[string, string]> = []
57
+ ): types.JSXElement =>
26
58
  types.jsxElement(
27
59
  types.jsxOpeningElement(
28
60
  types.jsxIdentifier('div'),
@@ -35,6 +67,9 @@ const control = (marker: string, children: types.JSXElement['children'] = []): t
35
67
  types.jsxIdentifier(PAGINATION_CONTROL_ATTR),
36
68
  types.stringLiteral(marker)
37
69
  ),
70
+ ...extraAttrs.map(([name, value]) =>
71
+ types.jsxAttribute(types.jsxIdentifier(name), types.stringLiteral(value))
72
+ ),
38
73
  ],
39
74
  false
40
75
  ),
@@ -43,6 +78,40 @@ const control = (marker: string, children: types.JSXElement['children'] = []): t
43
78
  false
44
79
  )
45
80
 
81
+ const rangeControl = (
82
+ template: string | null | undefined,
83
+ forNodeId?: string
84
+ ): types.JSXElement => {
85
+ const attrs: Array<[string, string]> = []
86
+ if (template !== null) {
87
+ attrs.push([RANGE_TEMPLATE_ATTR, template ?? RANGE_TEMPLATE])
88
+ }
89
+ if (forNodeId) {
90
+ attrs.push([PAGINATION_FOR_ATTR, forNodeId])
91
+ }
92
+ return control('range', [types.jsxText(RANGE_DRAWN_TEXT)], attrs)
93
+ }
94
+
95
+ const searchInput = (name: string, forNodeId?: string): types.JSXElement =>
96
+ types.jsxElement(
97
+ types.jsxOpeningElement(
98
+ types.jsxIdentifier('input'),
99
+ [
100
+ types.jsxAttribute(types.jsxIdentifier('name'), types.stringLiteral(name)),
101
+ forNodeId
102
+ ? types.jsxAttribute(types.jsxIdentifier(SEARCH_FOR_ATTR), types.stringLiteral(forNodeId))
103
+ : types.jsxAttribute(
104
+ types.jsxIdentifier('className'),
105
+ types.stringLiteral('products-list-search-input')
106
+ ),
107
+ ],
108
+ true
109
+ ),
110
+ null,
111
+ [],
112
+ true
113
+ )
114
+
46
115
  const legacyControl = (direction: 'previous' | 'next'): types.JSXElement =>
47
116
  types.jsxElement(
48
117
  types.jsxOpeningElement(
@@ -72,6 +141,10 @@ const buildWidget = (options: ControlOptions): types.JSXElement => {
72
141
  children.push(legacyControl(marker))
73
142
  continue
74
143
  }
144
+ if (marker === 'range') {
145
+ children.push(rangeControl(options.rangeTemplate))
146
+ continue
147
+ }
75
148
  children.push(control(marker))
76
149
  }
77
150
 
@@ -148,6 +221,12 @@ const buildDataProvider = (name: string, rowIdentifier: string): types.JSXElemen
148
221
 
149
222
  export const makeWidgetComponentChunk = (options: ControlOptions): ChunkDefinition => {
150
223
  const sections: types.JSXElement[] = []
224
+ if (options.standaloneRange) {
225
+ sections.push(rangeControl(options.standaloneRange.template, options.standaloneRange.forNodeId))
226
+ }
227
+ for (const search of options.standaloneSearch ?? []) {
228
+ sections.push(searchInput(search.name, search.forNodeId))
229
+ }
151
230
  const mappers = options.mappers ?? 1
152
231
  for (let i = 0; i < mappers; i += 1) {
153
232
  const identifier = `items${i === 0 ? '' : i}`
@@ -155,9 +234,10 @@ export const makeWidgetComponentChunk = (options: ControlOptions): ChunkDefiniti
155
234
  sections.push(buildWidget(options))
156
235
  }
157
236
 
237
+ const rootTag = options.rootTag ?? 'div'
158
238
  const root = types.jsxElement(
159
- types.jsxOpeningElement(types.jsxIdentifier('div'), [], false),
160
- types.jsxClosingElement(types.jsxIdentifier('div')),
239
+ types.jsxOpeningElement(types.jsxIdentifier(rootTag), [], false),
240
+ types.jsxClosingElement(types.jsxIdentifier(rootTag)),
161
241
  sections,
162
242
  false
163
243
  )
@@ -189,6 +269,16 @@ export interface RepeaterOptions {
189
269
  perPage?: number
190
270
  /** The list's filters, as the inspector authors them (conditions and groups). */
191
271
  filters?: unknown[]
272
+ /** The mapper's editor node id, as the UIDL carries it on the repeater. */
273
+ nodeId?: string
274
+ /** The repeater's sort and direction as the UIDL carries them: a static column, or an expression. */
275
+ sort?: unknown
276
+ sortDirection?: unknown
277
+ /** The repeater's cache config. */
278
+ cache?: unknown
279
+ /** The repeater's `source`, for a list bound inside the payload (with `dataSourceType`). */
280
+ source?: string
281
+ dataSourceType?: string
192
282
  }
193
283
 
194
284
  export const makeUidlNode = (options: RepeaterOptions, identifier: string = 'items'): any => ({
@@ -198,7 +288,7 @@ export const makeUidlNode = (options: RepeaterOptions, identifier: string = 'ite
198
288
  resourceDefinition: {
199
289
  dataSourceId: 'ds1',
200
290
  tableName: 'products',
201
- dataSourceType: 'postgresql',
291
+ dataSourceType: options.dataSourceType ?? 'postgresql',
202
292
  },
203
293
  resource: {
204
294
  params: {
@@ -218,6 +308,11 @@ export const makeUidlNode = (options: RepeaterOptions, identifier: string = 'ite
218
308
  ...(options.paginationMode ? { paginationMode: options.paginationMode } : {}),
219
309
  ...(options.infiniteScroll ? { infiniteScroll: true } : {}),
220
310
  ...(options.infiniteScrollLoadMore ? { infiniteScrollLoadMore: true } : {}),
311
+ ...(options.nodeId ? { nodeId: options.nodeId } : {}),
312
+ ...(options.sort ? { sort: options.sort } : {}),
313
+ ...(options.sortDirection ? { sortDirection: options.sortDirection } : {}),
314
+ ...(options.cache ? { cache: options.cache } : {}),
315
+ ...(options.source ? { source: options.source } : {}),
221
316
  nodes: { list: { type: 'element', content: { elementType: 'div' } } },
222
317
  },
223
318
  },
@@ -235,3 +330,57 @@ export const makeMultiMapperUidl = (repeaters: RepeaterOptions[]): any => ({
235
330
  ),
236
331
  },
237
332
  })
333
+
334
+ /**
335
+ * The page's `getStaticProps` as the main data-source plugin leaves it: a
336
+ * `Promise.all` of prefetches and a `props` object, which the pagination plugin
337
+ * extends with each paginated list's first page and its count. Its presence is
338
+ * what makes the component a page.
339
+ */
340
+ export const makeGetStaticPropsChunk = (): ChunkDefinition => ({
341
+ name: 'getStaticProps',
342
+ type: ChunkType.AST,
343
+ fileType: FileType.JS,
344
+ linkAfter: ['jsx-component'],
345
+ meta: {},
346
+ content: types.exportNamedDeclaration(
347
+ types.functionDeclaration(
348
+ types.identifier('getStaticProps'),
349
+ [types.identifier('context')],
350
+ types.blockStatement([
351
+ types.tryStatement(
352
+ types.blockStatement([
353
+ types.variableDeclaration('const', [
354
+ types.variableDeclarator(
355
+ types.arrayPattern([]),
356
+ types.awaitExpression(
357
+ types.callExpression(
358
+ types.memberExpression(types.identifier('Promise'), types.identifier('all')),
359
+ [types.arrayExpression([])]
360
+ )
361
+ )
362
+ ),
363
+ ]),
364
+ types.returnStatement(
365
+ types.objectExpression([
366
+ types.objectProperty(types.identifier('props'), types.objectExpression([])),
367
+ ])
368
+ ),
369
+ ]),
370
+ types.catchClause(
371
+ types.identifier('error'),
372
+ types.blockStatement([
373
+ types.returnStatement(
374
+ types.objectExpression([
375
+ types.objectProperty(types.identifier('props'), types.objectExpression([])),
376
+ ])
377
+ ),
378
+ ])
379
+ )
380
+ ),
381
+ ]),
382
+ false,
383
+ true
384
+ )
385
+ ),
386
+ })
@@ -0,0 +1,120 @@
1
+ import { generateBookingItemTransformationCode } from '../src/transformations/booking-item'
2
+ import { generateSharedTransformationCode } from '../src/transformations/shared-utils'
3
+ import {
4
+ detectTransformationType,
5
+ getTransformExpression,
6
+ getTransformWrapperCode,
7
+ } from '../src/transformations'
8
+
9
+ // A booking page binds an item's columns as stored; the transform only puts
10
+ // its texts in the page's language. These evaluate the EMITTED code, the way
11
+ // the runtime does.
12
+
13
+ type Build = (record: unknown, options?: Record<string, unknown>) => Record<string, unknown>
14
+
15
+ const builderFor = (code: string): Build =>
16
+ new Function(
17
+ generateSharedTransformationCode() + '\n' + code + '\nreturn buildBookingItem;'
18
+ )() as Build
19
+
20
+ const buildBookingItem = builderFor(generateBookingItemTransformationCode())
21
+
22
+ const buildWithCategories = builderFor(
23
+ generateBookingItemTransformationCode({
24
+ categories: [
25
+ {
26
+ id: 'rooms',
27
+ name: 'Rooms',
28
+ slug: 'rooms',
29
+ parentId: null,
30
+ order: 0,
31
+ translations: { es: { name: 'Habitaciones' } },
32
+ children: [
33
+ {
34
+ id: 'suites',
35
+ name: 'Suites',
36
+ slug: 'suites',
37
+ parentId: 'rooms',
38
+ order: 0,
39
+ children: [],
40
+ },
41
+ ],
42
+ },
43
+ ],
44
+ })
45
+ )
46
+
47
+ const item = {
48
+ id: 'i1',
49
+ slug: 'double-room',
50
+ name: 'Double room',
51
+ es_name: 'Habitación doble',
52
+ description: '<p>Sea view.</p>',
53
+ es_description: '<p>Vista al mar.</p>',
54
+ location: 'Main street',
55
+ es_location: '',
56
+ price: '120.00',
57
+ booking_unit: 'night',
58
+ }
59
+
60
+ describe('the booking item transform', () => {
61
+ it('is applied to the booking items table only', () => {
62
+ expect(detectTransformationType('teleport_booking_items')).toBe('booking-item')
63
+ expect(detectTransformationType('public.TELEPORT_BOOKING_ITEMS')).toBe('booking-item')
64
+ expect(detectTransformationType('booking_items')).toBeNull()
65
+ expect(getTransformExpression('teleport_booking_items')).toContain('transformRecords')
66
+ expect(getTransformWrapperCode('teleport_booking_items')).toContain('transformBookingItems')
67
+ })
68
+
69
+ it('shows the texts in the page’s language, every column kept as stored', () => {
70
+ const row = buildBookingItem(item, { currentLanguage: 'es', mainLanguage: 'en' })
71
+ expect(row).toMatchObject({
72
+ name: 'Habitación doble',
73
+ description: '<p>Vista al mar.</p>',
74
+ // Not translated yet: the main language's.
75
+ location: 'Main street',
76
+ slug: 'double-room',
77
+ price: '120.00',
78
+ booking_unit: 'night',
79
+ es_name: 'Habitación doble',
80
+ })
81
+ })
82
+
83
+ it('names its category the way the taxonomy names its first one in the page’s language', () => {
84
+ const filed = { ...item, category: 'Rooms', category_ids: '["rooms","suites"]' }
85
+ expect(buildWithCategories(filed, { currentLanguage: 'es', mainLanguage: 'en' }).category).toBe(
86
+ 'Habitaciones'
87
+ )
88
+ expect(buildWithCategories(filed, { currentLanguage: 'en', mainLanguage: 'en' }).category).toBe(
89
+ 'Rooms'
90
+ )
91
+ // No translation: the main name; a nested category resolves too.
92
+ expect(
93
+ buildWithCategories(
94
+ { ...filed, category: 'Suites', category_ids: '["suites"]' },
95
+ { currentLanguage: 'es', mainLanguage: 'en' }
96
+ ).category
97
+ ).toBe('Suites')
98
+ })
99
+
100
+ it('keeps the stored category while none of its ids is a category', () => {
101
+ const legacy = { ...item, category: 'Rooms', category_ids: '["gone"]' }
102
+ expect(
103
+ buildWithCategories(legacy, { currentLanguage: 'es', mainLanguage: 'en' }).category
104
+ ).toBe('Rooms')
105
+ expect(buildBookingItem(legacy, { currentLanguage: 'es', mainLanguage: 'en' }).category).toBe(
106
+ 'Rooms'
107
+ )
108
+ // A row without category columns gains none.
109
+ expect(
110
+ 'category' in buildWithCategories(item, { currentLanguage: 'es', mainLanguage: 'en' })
111
+ ).toBe(false)
112
+ })
113
+
114
+ it('keeps the main language on its own pages and without a localization', () => {
115
+ expect(buildBookingItem(item, { currentLanguage: 'en', mainLanguage: 'en' }).name).toBe(
116
+ 'Double room'
117
+ )
118
+ expect(buildBookingItem(item, {}).name).toBe('Double room')
119
+ })
120
+ })