@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,55 @@
1
+ import { generateBlogPostTransformationCode } from '../src/transformations/blog-post'
2
+ import { generateSharedTransformationCode } from '../src/transformations/shared-utils'
3
+
4
+ /**
5
+ * A post that stores no reading time gets one from its body — its words at
6
+ * about 200 a minute, at least one, as the editor's post form computes it —
7
+ * so a card or post page never prints "min read" with no number before it.
8
+ * The SAME cases as teleport-gui `packages/renderer/src/utils/__tests__/
9
+ * blog-reading-time.test.ts`: the canvas and the site must agree.
10
+ */
11
+
12
+ const buildPost = (headingAnchors = false): ((record: unknown) => Record<string, unknown>) => {
13
+ const code =
14
+ generateSharedTransformationCode() +
15
+ '\n' +
16
+ generateBlogPostTransformationCode({ headingAnchors })
17
+ return new Function(code + '\nreturn buildBlogPost;')() as (
18
+ record: unknown
19
+ ) => Record<string, unknown>
20
+ }
21
+
22
+ const words = (count: number) => `<p>${Array.from({ length: count }, () => 'word').join(' ')}</p>`
23
+ const POST = { id: 'p1', title: 'Connect a domain', slug: 'connect-a-domain', status: 'published' }
24
+
25
+ describe('a post’s reading time', () => {
26
+ it('is the one the post stores', () => {
27
+ expect(
28
+ buildPost()({ ...POST, reading_time_minutes: 7, content: words(401) }).readingTimeMinutes
29
+ ).toBe(7)
30
+ })
31
+
32
+ it('comes from the body when the post stores none', () => {
33
+ expect(buildPost()({ ...POST, content: words(401) }).readingTimeMinutes).toBe(3)
34
+ expect(
35
+ buildPost()({ ...POST, reading_time_minutes: null, content: words(12) }).readingTimeMinutes
36
+ ).toBe(1)
37
+ expect(
38
+ buildPost()({
39
+ ...POST,
40
+ content: '<h2>Steps</h2><ul><li>Open the panel</li><li>Click Save</li></ul>',
41
+ }).readingTimeMinutes
42
+ ).toBe(1)
43
+ })
44
+
45
+ it('counts the words, not the heading anchors a site adds', () => {
46
+ const content = `<h2>One</h2>${words(399)}`
47
+ expect(buildPost(true)({ ...POST, content }).readingTimeMinutes).toBe(2)
48
+ expect(buildPost(false)({ ...POST, content }).readingTimeMinutes).toBe(2)
49
+ })
50
+
51
+ it('is none for a post with no body', () => {
52
+ expect(buildPost()({ ...POST, content: '' }).readingTimeMinutes).toBeNull()
53
+ expect(buildPost()({ ...POST, content: '<p> </p>' }).readingTimeMinutes).toBeNull()
54
+ })
55
+ })
@@ -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
+ })
@@ -67,7 +67,10 @@ function bootTableRoute(tableName: string, options: BootOptions = {}) {
67
67
  options.cache
68
68
  )
69
69
  const code = esm
70
- .replace("import { Client } from 'pg'", "const { Client } = require('pg')")
70
+ .replace(
71
+ "import { Client, types as pgTypes } from 'pg'",
72
+ "const { Client, types: pgTypes } = require('pg')"
73
+ )
71
74
  .replace(
72
75
  "import { tqWithCache } from '../tq-cache/server'",
73
76
  "const { tqWithCache } = require('tq-cache')"
@@ -233,7 +236,11 @@ describe('blog posts: a visitor reads the published posts, never an author email
233
236
  it('serves the store everything: the admin, a server segment and getStaticProps', async () => {
234
237
  const admin = await posts.run({ session: { role: 'admin' } })
235
238
  expect(mainQuery(admin)).not.toContain("status = 'published'")
236
- expect(JSON.stringify(admin.body.data)).toContain('ana@example.com')
239
+ // The post view model carries no email for anyone — a page's data must not
240
+ // ship it (transformations/blog-post.ts); the admin form reads raw rows.
241
+ expect(JSON.stringify(admin.body.data)).not.toContain('ana@example.com')
242
+ const adminRaw = await posts.run({ session: { role: 'admin' }, query: { rawRows: 'true' } })
243
+ expect(JSON.stringify(adminRaw.body.data)).toContain('ana@example.com')
237
244
 
238
245
  const server = await posts.run({ headers: internal })
239
246
  expect(mainQuery(server)).not.toContain("status = 'published'")
@@ -413,7 +420,10 @@ describe('raw-query state routes bind {{Current User.*}} to the session', () =>
413
420
  paramFields
414
421
  )
415
422
  const code = esm
416
- .replace("import { Client } from 'pg'", "const { Client } = require('pg')")
423
+ .replace(
424
+ "import { Client, types as pgTypes } from 'pg'",
425
+ "const { Client, types: pgTypes } = require('pg')"
426
+ )
417
427
  .replace(/^export default async function handler/m, 'async function handler')
418
428
  .concat('\nmodule.exports = { handler: handler }')
419
429
  const queries: Array<{ sql: string; params: unknown[] }> = []
@@ -0,0 +1,108 @@
1
+ import { ContentTables, TableAccess } from '@teleporthq/teleport-shared'
2
+ import {
3
+ BROWSER_READABLE_TABLES,
4
+ BROWSER_ROW_POLICIES as DATA_API_ROW_POLICIES,
5
+ } from '../../teleport-plugin-next-workflows/src/data-api-route-generator'
6
+ import {
7
+ BROWSER_ROW_POLICIES,
8
+ isViewDependentTable,
9
+ } from '../src/fetchers/utils/browser-row-policy'
10
+ import {
11
+ buildProductTransformOptions,
12
+ contentSettingsFor,
13
+ detectTransformationType,
14
+ getTransformWrapperCode,
15
+ getTransformationCode,
16
+ } from '../src/transformations'
17
+ import { generateBlogContextFileContent } from '../../teleport-project-generator-next/src/blog/blog-context-generator'
18
+
19
+ /**
20
+ * Every content preset in the registry is wired through every generator that
21
+ * tells a content table apart from a custom one. A preset registered in
22
+ * teleport-shared but unknown to one of these would publish its drafts, or
23
+ * serve its rows raw — so each check runs per preset, and a new one fails
24
+ * here before it fails on a site.
25
+ */
26
+ describe('content preset wiring in the generators', () => {
27
+ it.each(ContentTables.CONTENT_TABLES.map((tables) => [tables.key, tables]))(
28
+ 'the "%s" preset is recognised everywhere',
29
+ (_key, tables: ContentTables.ContentTables) => {
30
+ // Visitors are served published rows only, through both route families.
31
+ expect(BROWSER_ROW_POLICIES[tables.posts]).toBe(
32
+ ContentTables.CONTENT_POSTS_VISITOR_ROW_POLICY
33
+ )
34
+ expect(DATA_API_ROW_POLICIES[tables.posts]).toBe(
35
+ ContentTables.CONTENT_POSTS_VISITOR_ROW_POLICY
36
+ )
37
+ expect(BROWSER_READABLE_TABLES).toContain(tables.posts)
38
+ expect(isViewDependentTable(tables.posts, TableAccess.PROTECTED_TABLES)).toBe(true)
39
+
40
+ // The rows go through the post view model, whose SQL names THIS preset's tables.
41
+ expect(detectTransformationType(tables.posts)).toBe('blog-post')
42
+ expect(detectTransformationType(`public.${tables.posts}`)).toBe('blog-post')
43
+ const code = getTransformationCode(tables.posts)
44
+ expect(code).toContain(`SELECT * FROM ${tables.posts} WHERE id = ANY($1)`)
45
+ expect(code).toContain(`SELECT p.* FROM ${tables.posts} p,`)
46
+ if (tables.comments) {
47
+ expect(code).toContain(`FROM ${tables.comments} c`)
48
+ // A browser never lists comments: the table carries the commenters' emails.
49
+ expect(TableAccess.CUSTOMER_RECORD_TABLES).toContain(tables.comments)
50
+ expect(BROWSER_READABLE_TABLES).not.toContain(tables.comments)
51
+ }
52
+ }
53
+ )
54
+
55
+ it('leaves a custom table that only sounds like posts alone', () => {
56
+ expect(detectTransformationType('blog_posts')).toBeNull()
57
+ expect(BROWSER_ROW_POLICIES.blog_posts).toBeUndefined()
58
+ expect(BROWSER_READABLE_TABLES).not.toContain('blog_posts')
59
+ })
60
+
61
+ it('bakes each preset\u2019s own settings into its transform, never another\u2019s', () => {
62
+ const blogTaxonomy = [{ id: 'b1', name: 'Blog cat', slug: 'blog-cat' }]
63
+ const helpTaxonomy = [{ id: 'h1', name: 'Help cat', slug: 'help-cat' }]
64
+ const options = buildProductTransformOptions({
65
+ blogSettings: { categories: blogTaxonomy as never, headingAnchors: false, comments: true },
66
+ helpCenterSettings: {
67
+ categories: helpTaxonomy as never,
68
+ headingAnchors: true,
69
+ comments: false,
70
+ },
71
+ })
72
+ expect(contentSettingsFor(options, 'teleport_blog_posts')).toEqual({
73
+ categories: blogTaxonomy,
74
+ headingAnchors: false,
75
+ comments: true,
76
+ })
77
+ expect(contentSettingsFor(options, 'teleport_help_articles')).toEqual({
78
+ categories: helpTaxonomy,
79
+ headingAnchors: true,
80
+ comments: false,
81
+ })
82
+ expect(contentSettingsFor(options, 'teleport_products')).toEqual({})
83
+ // The Blog's legacy fields still answer for a caller that set only them.
84
+ expect(contentSettingsFor({ blogComments: true }, 'teleport_blog_posts').comments).toBe(true)
85
+
86
+ // The article transform carries the Help Center's taxonomy and anchors, not the Blog's.
87
+ const helpCode = getTransformationCode('teleport_help_articles', options)
88
+ expect(helpCode).toContain('Help cat')
89
+ expect(helpCode).not.toContain('Blog cat')
90
+ expect(helpCode).toContain('var BLOG_HEADING_ANCHORS = true')
91
+ // Comments are a Blog feature: the article wrapper never queries a comments table.
92
+ expect(getTransformWrapperCode('teleport_help_articles', options)).not.toContain(
93
+ 'getBlogCommentsMap('
94
+ )
95
+ expect(getTransformWrapperCode('teleport_blog_posts', options)).toContain('getBlogCommentsMap(')
96
+ })
97
+
98
+ it('exposes both taxonomies from the one generated context module', () => {
99
+ const code = generateBlogContextFileContent(
100
+ { categories: [{ id: 'b1', name: 'Blog cat', slug: 'blog-cat' }] as never },
101
+ { categories: [{ id: 'h1', name: 'Help cat', slug: 'help-cat' }] as never }
102
+ )
103
+ expect(code).toContain('const BLOG_CATEGORIES = [{"id":"b1"')
104
+ expect(code).toContain('const HELP_CATEGORIES = [{"id":"h1"')
105
+ expect(code).toContain('export const useBlogCategories = ()')
106
+ expect(code).toContain('export const useHelpCategories = ()')
107
+ })
108
+ })
@@ -335,7 +335,7 @@ describe('generateDataSourceFetcher', () => {
335
335
  const dataSource = createPostgreSQLDataSource('ds-1')
336
336
  const code = generateDataSourceFetcher(dataSource, 'users')
337
337
 
338
- expect(code).toContain('import { Client } from')
338
+ expect(code).toContain('import { Client, types as pgTypes } from')
339
339
  expect(code).toContain('export default async function handler')
340
340
  expect(code).toContain('SELECT * FROM "users"')
341
341
  expect(code).toContain('localhost')
@@ -0,0 +1,183 @@
1
+ import * as types from '@babel/types'
2
+ import generator from '@babel/generator'
3
+ import {
4
+ ChunkType,
5
+ FileType,
6
+ ComponentStructure,
7
+ ChunkDefinition,
8
+ UIDLDataCacheConfig,
9
+ } from '@teleporthq/teleport-types'
10
+ import { createNextArrayMapperPaginationPlugin } from '../src/pagination-plugin'
11
+ import { readFixedSort } from '../src/sort-utils'
12
+
13
+ /**
14
+ * A list whose sort the editor FIXED (a blog listing's newest first: a static
15
+ * column, a static direction) is a static sort. Read as dynamic, it made the
16
+ * list skip the server-side first page — `initialData` is left out for a
17
+ * dynamic sort, since that prefetch ran without one — so the pre-rendered
18
+ * HTML of every blog and help center listing carried no posts, and the
19
+ * browser fetched page 1 a second time. Only a sort bound to state is dynamic.
20
+ */
21
+
22
+ const CACHE: UIDLDataCacheConfig = {
23
+ enabled: true,
24
+ ttlSeconds: 60,
25
+ client: true,
26
+ server: true,
27
+ versionScope: 'ds1:posts',
28
+ }
29
+
30
+ const element = (name: string, attributes: types.JSXAttribute[] = []) =>
31
+ types.jsxElement(
32
+ types.jsxOpeningElement(types.jsxIdentifier(name), attributes, true),
33
+ null,
34
+ [],
35
+ true
36
+ )
37
+
38
+ const makeDataProviderJSX = (): types.JSXElement =>
39
+ element('DataProvider', [
40
+ types.jsxAttribute(
41
+ types.jsxIdentifier('name'),
42
+ types.jsxExpressionContainer(types.stringLiteral('items'))
43
+ ),
44
+ types.jsxAttribute(
45
+ types.jsxIdentifier('renderSuccess'),
46
+ types.jsxExpressionContainer(
47
+ types.arrowFunctionExpression(
48
+ [types.identifier('items')],
49
+ element('Repeater', [
50
+ types.jsxAttribute(
51
+ types.jsxIdentifier('renderItem'),
52
+ types.jsxExpressionContainer(
53
+ types.arrowFunctionExpression([types.identifier('post')], element('div'))
54
+ )
55
+ ),
56
+ ])
57
+ )
58
+ )
59
+ ),
60
+ ])
61
+
62
+ const makeComponentChunk = (): ChunkDefinition => ({
63
+ name: 'jsx-component',
64
+ type: ChunkType.AST,
65
+ fileType: FileType.JS,
66
+ linkAfter: [],
67
+ content: types.variableDeclaration('const', [
68
+ types.variableDeclarator(
69
+ types.identifier('PostsList'),
70
+ types.arrowFunctionExpression(
71
+ [types.identifier('props')],
72
+ types.blockStatement([types.returnStatement(makeDataProviderJSX())])
73
+ )
74
+ ),
75
+ ]),
76
+ meta: {},
77
+ })
78
+
79
+ type SortKind = 'fixed' | 'state' | 'none'
80
+
81
+ const SORTS: Record<SortKind, Record<string, unknown>> = {
82
+ fixed: {
83
+ sort: { type: 'static', content: 'created_at' },
84
+ sortDirection: { type: 'static', content: 'desc' },
85
+ },
86
+ state: {
87
+ sort: { type: 'expr', content: 'sortBy.split("-")[0]' },
88
+ sortDirection: { type: 'expr', content: 'sortBy.split("-")[1]' },
89
+ },
90
+ none: {},
91
+ }
92
+
93
+ const runPlugin = async (sort: SortKind, cache?: UIDLDataCacheConfig): Promise<string> => {
94
+ const chunk = makeComponentChunk()
95
+ const structure: ComponentStructure = {
96
+ uidl: {
97
+ name: 'PostsList',
98
+ node: {
99
+ type: 'data-source-list',
100
+ content: {
101
+ renderPropIdentifier: 'items',
102
+ resourceDefinition: {
103
+ dataSourceId: 'ds1',
104
+ tableName: 'posts',
105
+ dataSourceType: 'postgresql',
106
+ },
107
+ resource: { params: { queryColumns: { content: ['title'] } } },
108
+ nodes: {
109
+ success: {
110
+ type: 'cms-list-repeater',
111
+ content: {
112
+ renderPropIdentifier: 'post',
113
+ paginated: true,
114
+ perPage: 20,
115
+ searchEnabled: true,
116
+ searchDebounce: 300,
117
+ nodes: { list: { type: 'element', content: { elementType: 'div' } } },
118
+ ...(cache ? { cache } : {}),
119
+ ...SORTS[sort],
120
+ },
121
+ },
122
+ },
123
+ },
124
+ },
125
+ },
126
+ chunks: [chunk],
127
+ dependencies: {},
128
+ options: {
129
+ dataSources: {
130
+ ds1: {
131
+ id: 'ds1',
132
+ name: 'Content',
133
+ type: 'postgresql',
134
+ config: { host: 'h', database: 'd', user: 'u', password: 'p' },
135
+ },
136
+ },
137
+ extractedResources: {},
138
+ },
139
+ } as never
140
+ await createNextArrayMapperPaginationPlugin()(structure)
141
+ return generator(chunk.content as types.Node).code.replace(/\s+/g, ' ')
142
+ }
143
+
144
+ describe('a list sort the editor fixed', () => {
145
+ it('keeps the server-side first page and asks for the sort itself', async () => {
146
+ const code = await runPlugin('fixed')
147
+ expect(code).toContain('initialData=')
148
+ expect(code).toContain('sorts: JSON.stringify([{ field: "created_at", order: "desc" }])')
149
+ })
150
+
151
+ it('may peek at the browser cache on mount, as an unsorted list may', async () => {
152
+ expect(await runPlugin('fixed', CACHE)).toContain('ds_0_cached')
153
+ expect(await runPlugin('none', CACHE)).toContain('ds_0_cached')
154
+ })
155
+
156
+ it('leaves a sort bound to state dynamic: no first page from the server, no peek', async () => {
157
+ const code = await runPlugin('state', CACHE)
158
+ expect(code).not.toContain('initialData=')
159
+ expect(code).not.toContain('ds_0_cached')
160
+ expect(code).toContain('field: sortBy.split("-")?.[0]')
161
+ })
162
+ })
163
+
164
+ describe('readFixedSort', () => {
165
+ it('reads a static column and direction, ascending by default', () => {
166
+ expect(
167
+ readFixedSort({ type: 'static', content: 'created_at' }, { type: 'static', content: 'desc' })
168
+ ).toEqual({ field: 'created_at', order: 'desc' })
169
+ expect(readFixedSort({ type: 'static', content: 'title' }, undefined)).toEqual({
170
+ field: 'title',
171
+ order: 'asc',
172
+ })
173
+ })
174
+
175
+ it('refuses anything bound to state, and an empty column', () => {
176
+ expect(readFixedSort({ type: 'expr', content: 'sortBy' }, undefined)).toBeUndefined()
177
+ expect(
178
+ readFixedSort({ type: 'static', content: 'title' }, { type: 'expr', content: 'direction' })
179
+ ).toBeUndefined()
180
+ expect(readFixedSort({ type: 'static', content: ' ' }, undefined)).toBeUndefined()
181
+ expect(readFixedSort(undefined, undefined)).toBeUndefined()
182
+ })
183
+ })