@teleporthq/teleport-plugin-next-data-source 0.43.66 → 0.43.67

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 (179) hide show
  1. package/__tests__/array-mapper-cache-wiring.test.ts +11 -0
  2. package/__tests__/data-source-request-locale.test.ts +313 -0
  3. package/__tests__/gated-data-provider.test.ts +88 -0
  4. package/__tests__/localized-columns.test.ts +217 -0
  5. package/__tests__/numbered-pagination.test.ts +15 -0
  6. package/__tests__/product-filter-fields.ts +230 -0
  7. package/__tests__/product-filter-sql.fixture.json +7 -0
  8. package/__tests__/refetch-loading-state.test.ts +23 -0
  9. package/__tests__/search-clear-button.test.ts +122 -0
  10. package/dist/cjs/content-localization.d.ts +37 -0
  11. package/dist/cjs/content-localization.d.ts.map +1 -0
  12. package/dist/cjs/content-localization.js +29 -0
  13. package/dist/cjs/content-localization.js.map +1 -0
  14. package/dist/cjs/count-effect.d.ts +11 -1
  15. package/dist/cjs/count-effect.d.ts.map +1 -1
  16. package/dist/cjs/count-effect.js +30 -2
  17. package/dist/cjs/count-effect.js.map +1 -1
  18. package/dist/cjs/fetchers/airtable.d.ts.map +1 -1
  19. package/dist/cjs/fetchers/airtable.js +1 -1
  20. package/dist/cjs/fetchers/airtable.js.map +1 -1
  21. package/dist/cjs/fetchers/clickhouse.d.ts.map +1 -1
  22. package/dist/cjs/fetchers/clickhouse.js +1 -1
  23. package/dist/cjs/fetchers/clickhouse.js.map +1 -1
  24. package/dist/cjs/fetchers/firestore.d.ts.map +1 -1
  25. package/dist/cjs/fetchers/firestore.js +1 -1
  26. package/dist/cjs/fetchers/firestore.js.map +1 -1
  27. package/dist/cjs/fetchers/mariadb.d.ts.map +1 -1
  28. package/dist/cjs/fetchers/mariadb.js +1 -1
  29. package/dist/cjs/fetchers/mariadb.js.map +1 -1
  30. package/dist/cjs/fetchers/mongodb.d.ts.map +1 -1
  31. package/dist/cjs/fetchers/mongodb.js +1 -1
  32. package/dist/cjs/fetchers/mongodb.js.map +1 -1
  33. package/dist/cjs/fetchers/mysql.d.ts.map +1 -1
  34. package/dist/cjs/fetchers/mysql.js +1 -1
  35. package/dist/cjs/fetchers/mysql.js.map +1 -1
  36. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  37. package/dist/cjs/fetchers/postgresql.js +2 -1
  38. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  39. package/dist/cjs/fetchers/redshift.d.ts.map +1 -1
  40. package/dist/cjs/fetchers/redshift.js +1 -1
  41. package/dist/cjs/fetchers/redshift.js.map +1 -1
  42. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  43. package/dist/cjs/fetchers/teleport.js +10 -3
  44. package/dist/cjs/fetchers/teleport.js.map +1 -1
  45. package/dist/cjs/fetchers/turso.d.ts.map +1 -1
  46. package/dist/cjs/fetchers/turso.js +1 -1
  47. package/dist/cjs/fetchers/turso.js.map +1 -1
  48. package/dist/cjs/index.d.ts +5 -0
  49. package/dist/cjs/index.d.ts.map +1 -1
  50. package/dist/cjs/index.js +60 -8
  51. package/dist/cjs/index.js.map +1 -1
  52. package/dist/cjs/loading-state.d.ts +16 -1
  53. package/dist/cjs/loading-state.d.ts.map +1 -1
  54. package/dist/cjs/loading-state.js +69 -3
  55. package/dist/cjs/loading-state.js.map +1 -1
  56. package/dist/cjs/localized-columns.d.ts +56 -0
  57. package/dist/cjs/localized-columns.d.ts.map +1 -0
  58. package/dist/cjs/localized-columns.js +124 -0
  59. package/dist/cjs/localized-columns.js.map +1 -0
  60. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  61. package/dist/cjs/pagination-plugin.js +123 -45
  62. package/dist/cjs/pagination-plugin.js.map +1 -1
  63. package/dist/cjs/product-filter-fields.d.ts +87 -0
  64. package/dist/cjs/product-filter-fields.d.ts.map +1 -0
  65. package/dist/cjs/product-filter-fields.js +161 -0
  66. package/dist/cjs/product-filter-fields.js.map +1 -0
  67. package/dist/cjs/request-locale.d.ts +49 -0
  68. package/dist/cjs/request-locale.d.ts.map +1 -0
  69. package/dist/cjs/request-locale.js +130 -0
  70. package/dist/cjs/request-locale.js.map +1 -0
  71. package/dist/cjs/search-clear.d.ts +31 -0
  72. package/dist/cjs/search-clear.d.ts.map +1 -0
  73. package/dist/cjs/search-clear.js +103 -0
  74. package/dist/cjs/search-clear.js.map +1 -0
  75. package/dist/cjs/transformations/index.d.ts +16 -2
  76. package/dist/cjs/transformations/index.d.ts.map +1 -1
  77. package/dist/cjs/transformations/index.js +14 -2
  78. package/dist/cjs/transformations/index.js.map +1 -1
  79. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  80. package/dist/cjs/utils.d.ts +11 -1
  81. package/dist/cjs/utils.d.ts.map +1 -1
  82. package/dist/cjs/utils.js +59 -15
  83. package/dist/cjs/utils.js.map +1 -1
  84. package/dist/esm/content-localization.d.ts +37 -0
  85. package/dist/esm/content-localization.d.ts.map +1 -0
  86. package/dist/esm/content-localization.js +24 -0
  87. package/dist/esm/content-localization.js.map +1 -0
  88. package/dist/esm/count-effect.d.ts +11 -1
  89. package/dist/esm/count-effect.d.ts.map +1 -1
  90. package/dist/esm/count-effect.js +28 -1
  91. package/dist/esm/count-effect.js.map +1 -1
  92. package/dist/esm/fetchers/airtable.d.ts.map +1 -1
  93. package/dist/esm/fetchers/airtable.js +1 -1
  94. package/dist/esm/fetchers/airtable.js.map +1 -1
  95. package/dist/esm/fetchers/clickhouse.d.ts.map +1 -1
  96. package/dist/esm/fetchers/clickhouse.js +1 -1
  97. package/dist/esm/fetchers/clickhouse.js.map +1 -1
  98. package/dist/esm/fetchers/firestore.d.ts.map +1 -1
  99. package/dist/esm/fetchers/firestore.js +1 -1
  100. package/dist/esm/fetchers/firestore.js.map +1 -1
  101. package/dist/esm/fetchers/mariadb.d.ts.map +1 -1
  102. package/dist/esm/fetchers/mariadb.js +1 -1
  103. package/dist/esm/fetchers/mariadb.js.map +1 -1
  104. package/dist/esm/fetchers/mongodb.d.ts.map +1 -1
  105. package/dist/esm/fetchers/mongodb.js +1 -1
  106. package/dist/esm/fetchers/mongodb.js.map +1 -1
  107. package/dist/esm/fetchers/mysql.d.ts.map +1 -1
  108. package/dist/esm/fetchers/mysql.js +1 -1
  109. package/dist/esm/fetchers/mysql.js.map +1 -1
  110. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  111. package/dist/esm/fetchers/postgresql.js +2 -1
  112. package/dist/esm/fetchers/postgresql.js.map +1 -1
  113. package/dist/esm/fetchers/redshift.d.ts.map +1 -1
  114. package/dist/esm/fetchers/redshift.js +1 -1
  115. package/dist/esm/fetchers/redshift.js.map +1 -1
  116. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  117. package/dist/esm/fetchers/teleport.js +10 -3
  118. package/dist/esm/fetchers/teleport.js.map +1 -1
  119. package/dist/esm/fetchers/turso.d.ts.map +1 -1
  120. package/dist/esm/fetchers/turso.js +1 -1
  121. package/dist/esm/fetchers/turso.js.map +1 -1
  122. package/dist/esm/index.d.ts +5 -0
  123. package/dist/esm/index.d.ts.map +1 -1
  124. package/dist/esm/index.js +55 -7
  125. package/dist/esm/index.js.map +1 -1
  126. package/dist/esm/loading-state.d.ts +16 -1
  127. package/dist/esm/loading-state.d.ts.map +1 -1
  128. package/dist/esm/loading-state.js +69 -3
  129. package/dist/esm/loading-state.js.map +1 -1
  130. package/dist/esm/localized-columns.d.ts +56 -0
  131. package/dist/esm/localized-columns.d.ts.map +1 -0
  132. package/dist/esm/localized-columns.js +119 -0
  133. package/dist/esm/localized-columns.js.map +1 -0
  134. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  135. package/dist/esm/pagination-plugin.js +124 -46
  136. package/dist/esm/pagination-plugin.js.map +1 -1
  137. package/dist/esm/product-filter-fields.d.ts +87 -0
  138. package/dist/esm/product-filter-fields.d.ts.map +1 -0
  139. package/dist/esm/product-filter-fields.js +154 -0
  140. package/dist/esm/product-filter-fields.js.map +1 -0
  141. package/dist/esm/request-locale.d.ts +49 -0
  142. package/dist/esm/request-locale.d.ts.map +1 -0
  143. package/dist/esm/request-locale.js +98 -0
  144. package/dist/esm/request-locale.js.map +1 -0
  145. package/dist/esm/search-clear.d.ts +31 -0
  146. package/dist/esm/search-clear.d.ts.map +1 -0
  147. package/dist/esm/search-clear.js +74 -0
  148. package/dist/esm/search-clear.js.map +1 -0
  149. package/dist/esm/transformations/index.d.ts +16 -2
  150. package/dist/esm/transformations/index.d.ts.map +1 -1
  151. package/dist/esm/transformations/index.js +14 -2
  152. package/dist/esm/transformations/index.js.map +1 -1
  153. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  154. package/dist/esm/utils.d.ts +11 -1
  155. package/dist/esm/utils.d.ts.map +1 -1
  156. package/dist/esm/utils.js +59 -15
  157. package/dist/esm/utils.js.map +1 -1
  158. package/package.json +5 -5
  159. package/src/content-localization.ts +53 -0
  160. package/src/count-effect.ts +53 -2
  161. package/src/fetchers/airtable.ts +4 -2
  162. package/src/fetchers/clickhouse.ts +4 -2
  163. package/src/fetchers/firestore.ts +4 -2
  164. package/src/fetchers/mariadb.ts +4 -2
  165. package/src/fetchers/mongodb.ts +4 -2
  166. package/src/fetchers/mysql.ts +4 -2
  167. package/src/fetchers/postgresql.ts +16 -2
  168. package/src/fetchers/redshift.ts +4 -2
  169. package/src/fetchers/teleport.ts +48 -9
  170. package/src/fetchers/turso.ts +4 -2
  171. package/src/index.ts +79 -11
  172. package/src/loading-state.ts +134 -3
  173. package/src/localized-columns.ts +133 -0
  174. package/src/pagination-plugin.ts +177 -53
  175. package/src/product-filter-fields.ts +173 -0
  176. package/src/request-locale.ts +113 -0
  177. package/src/search-clear.ts +102 -0
  178. package/src/transformations/index.ts +30 -5
  179. package/src/utils.ts +83 -15
@@ -0,0 +1,230 @@
1
+ import { parse } from '@babel/parser'
2
+ import { readFileSync } from 'fs'
3
+ import { join } from 'path'
4
+ import {
5
+ PRODUCT_ON_SALE_FILTER_FIELD,
6
+ PRODUCT_RATING_FILTER_FIELD,
7
+ PRODUCT_VARIANT_AXIS_FILTER_PREFIX,
8
+ PRODUCT_VARIANT_AXIS_KEY_PATTERN,
9
+ buildProductOnSaleSql,
10
+ buildProductRatingBucketSql,
11
+ buildProductVariantAxisExistsSql,
12
+ generateProductFilterClauseHelper,
13
+ } from '../src/product-filter-fields'
14
+ import { buildProductEffectivePriceSql } from '../src/product-price-sort'
15
+ import { generateTeleportFetcher } from '../src/fetchers/teleport'
16
+ import { generatePostgreSQLFetcher } from '../src/fetchers/postgresql'
17
+ import { generateMySQLFetcher } from '../src/fetchers/mysql'
18
+ import { generateDataSourceFetcher } from '../src/data-source-fetchers'
19
+ import { generateFilterTreeHelpersCode } from '../src/utils'
20
+
21
+ /**
22
+ * The storefront's product filters on things that are not a column: the price
23
+ * paid today, the star bucket, the on-sale flag and a variant axis. They are
24
+ * answered in SQL — the list is paginated — so the emitted helper is pinned
25
+ * here the same way the discount-aware sort is.
26
+ */
27
+
28
+ const PG_CONFIG = { connectionString: 'postgres://user:pw@host:5432/db' }
29
+
30
+ /** Runs the emitted helper as real JS and returns the clause + bound params. */
31
+ function runHelper(
32
+ tableName: string,
33
+ field: string,
34
+ operand: string,
35
+ value: unknown
36
+ ): { clause: string | null; params: unknown[] } {
37
+ const params: unknown[] = []
38
+ let index = 1
39
+ const pushParam = (param: unknown) => {
40
+ params.push(param)
41
+ return '$' + index++
42
+ }
43
+ // tslint:disable-next-line:function-constructor
44
+ const factory = new Function(
45
+ `${generateProductFilterClauseHelper(tableName)}\nreturn productFilterClause`
46
+ )
47
+ const clause = factory()(field, operand, value, pushParam)
48
+ return { clause, params }
49
+ }
50
+
51
+ /** Runs the emitted `normalizeInOperand` as real JS. */
52
+ function runNormalize(operand: string, value: unknown): unknown {
53
+ // tslint:disable-next-line:function-constructor
54
+ const factory = new Function(`${generateFilterTreeHelpersCode()}\nreturn normalizeInOperand`)
55
+ return factory()(operand, value)
56
+ }
57
+
58
+ describe('generateProductFilterClauseHelper', () => {
59
+ it('is the identity for every table but the products table', () => {
60
+ const helper = generateProductFilterClauseHelper('teleport_orders')
61
+ expect(helper).toContain('return null')
62
+ expect(helper).not.toContain('jsonb_array_elements')
63
+ expect(runHelper('teleport_orders', 'rating', '=', '4').clause).toBeNull()
64
+ })
65
+
66
+ it('emits the SQL as double-quoted literals, carrying no template syntax', () => {
67
+ const helper = generateProductFilterClauseHelper('teleport_products')
68
+ expect(helper).not.toContain('`')
69
+ expect(helper).not.toContain('${')
70
+ expect(() => parse(helper, { sourceType: 'script' })).not.toThrow()
71
+ })
72
+
73
+ it('leaves a plain column to the ordinary branches', () => {
74
+ expect(runHelper('teleport_products', 'brand', '=', 'Acme').clause).toBeNull()
75
+ expect(runHelper('teleport_products', 'brand', '=', ['Acme', 'Globex']).clause).toBeNull()
76
+ })
77
+
78
+ it('filters price on what customers pay today, like the price sort', () => {
79
+ const { clause, params } = runHelper('teleport_products', 'price', '<=', '50')
80
+ expect(clause).toBe(`(${buildProductEffectivePriceSql()}) <= $1`)
81
+ expect(params).toEqual(['50'])
82
+ })
83
+
84
+ it('answers a star bucket over approved reviews only', () => {
85
+ const sql = buildProductRatingBucketSql()
86
+ expect(sql).toContain("r.status = 'approved'")
87
+ expect(sql).toContain('ROUND(AVG(r.rating))')
88
+ expect(sql).toContain('r.product_id = teleport_products.id')
89
+
90
+ const { clause, params } = runHelper('teleport_products', PRODUCT_RATING_FILTER_FIELD, '=', [
91
+ '4',
92
+ '5',
93
+ ])
94
+ expect(clause).toBe(`${sql}::text = ANY($1::text[])`)
95
+ expect(params).toEqual([['4', '5']])
96
+ })
97
+
98
+ it('reads the on-sale flag off the effective price', () => {
99
+ const sql = buildProductOnSaleSql()
100
+ expect(sql).toContain(`< "price"::numeric THEN 'true' ELSE 'false'`)
101
+ const { clause, params } = runHelper(
102
+ 'teleport_products',
103
+ PRODUCT_ON_SALE_FILTER_FIELD,
104
+ '=',
105
+ 'true'
106
+ )
107
+ expect(clause).toBe(`${sql} = $1`)
108
+ expect(params).toEqual(['true'])
109
+ })
110
+
111
+ it('binds the axis key and the values, never interpolating them', () => {
112
+ const { clause, params } = runHelper(
113
+ 'teleport_products',
114
+ `${PRODUCT_VARIANT_AXIS_FILTER_PREFIX}size`,
115
+ '=',
116
+ ['xl', 'l']
117
+ )
118
+ expect(clause).toBe(buildProductVariantAxisExistsSql('$1', '$2'))
119
+ expect(clause).toContain("ax->>'key' = $1")
120
+ expect(clause).toContain("v->>'value' = ANY($2::text[])")
121
+ expect(params).toEqual(['size', ['xl', 'l']])
122
+ })
123
+
124
+ it('casts the JSON column only when it already looks like an array', () => {
125
+ const sql = buildProductVariantAxisExistsSql('$1', '$2')
126
+ expect(sql).toContain(`btrim("variant_options") NOT LIKE '[%'`)
127
+ expect(sql).toContain(`jsonb_typeof(ax->'values') = 'array'`)
128
+ })
129
+
130
+ it('refuses an axis key the editor could never have written', () => {
131
+ expect(new RegExp(PRODUCT_VARIANT_AXIS_KEY_PATTERN).test('size')).toBe(true)
132
+ expect(new RegExp(PRODUCT_VARIANT_AXIS_KEY_PATTERN).test('shoe-size_2')).toBe(true)
133
+ expect(new RegExp(PRODUCT_VARIANT_AXIS_KEY_PATTERN).test(`x' OR 1=1 --`)).toBe(false)
134
+ const { clause, params } = runHelper(
135
+ 'teleport_products',
136
+ `${PRODUCT_VARIANT_AXIS_FILTER_PREFIX}x' OR 1=1 --`,
137
+ '=',
138
+ ['a']
139
+ )
140
+ expect(clause).toBe('FALSE')
141
+ expect(params).toEqual([])
142
+ })
143
+
144
+ it('never lets a virtual field fall into a column branch', () => {
145
+ // A `>` on the on-sale flag has no meaning; the fallthrough would emit
146
+ // `on_sale > $1` and fail the whole query.
147
+ expect(runHelper('teleport_products', PRODUCT_ON_SALE_FILTER_FIELD, '>', 'x').clause).toBe(
148
+ 'FALSE'
149
+ )
150
+ expect(
151
+ runHelper('teleport_products', `${PRODUCT_VARIANT_AXIS_FILTER_PREFIX}size`, '>', ['xl'])
152
+ .clause
153
+ ).toBe('FALSE')
154
+ })
155
+ })
156
+
157
+ describe('normalizeInOperand', () => {
158
+ it('turns "in" into "=" over an array, so every fetcher answers IN (...)', () => {
159
+ expect(runNormalize('in', 'a, b,,c')).toEqual({ operand: '=', value: ['a', 'b', 'c'] })
160
+ expect(runNormalize('in', ['a', 'b,c', 4])).toEqual({ operand: '=', value: ['a', 'b', 'c', 4] })
161
+ })
162
+
163
+ it('reads an empty set as "no filter"', () => {
164
+ expect(runNormalize('in', '')).toBeNull()
165
+ expect(runNormalize('in', [])).toBeNull()
166
+ expect(runNormalize('in', ' , ')).toBeNull()
167
+ })
168
+
169
+ it('passes every other operand through untouched', () => {
170
+ expect(runNormalize('=', 'a,b')).toEqual({ operand: '=', value: 'a,b' })
171
+ expect(runNormalize('array_overlap', 'a,b')).toEqual({ operand: 'array_overlap', value: 'a,b' })
172
+ })
173
+ })
174
+
175
+ describe('the generated routes', () => {
176
+ it('answer the virtual fields before any raw-field branch, in the fetcher a store uses', () => {
177
+ // Through the type dispatch, not one fetcher's export: `teleport_products`
178
+ // is a Teleport-hosted table, and the discount sort once shipped into the
179
+ // wrong fetcher with every test green.
180
+ const route = generateDataSourceFetcher({ type: 'teleport', config: {} }, 'teleport_products')
181
+ expect(route).toContain('function productFilterClause(field, operand, value, pushParam)')
182
+ const conditionAt = route.indexOf('const buildCondition = (condition) =>')
183
+ const hookAt = route.indexOf('const productClause = productFilterClause(', conditionAt)
184
+ const arrayBranchAt = route.indexOf('if (Array.isArray(value))', conditionAt)
185
+ expect(conditionAt).toBeGreaterThan(-1)
186
+ expect(hookAt).toBeGreaterThan(conditionAt)
187
+ expect(arrayBranchAt).toBeGreaterThan(hookAt)
188
+ expect(route).toContain('normalizeInOperand(condition.operand, condition.destination)')
189
+ expect(() => parse(route, { sourceType: 'module', plugins: ['jsx'] })).not.toThrow()
190
+ })
191
+
192
+ it('embed the helper in the Postgres route too, and parse', () => {
193
+ const route = generatePostgreSQLFetcher(PG_CONFIG, 'teleport_products')
194
+ expect(route).toContain('const productClause = productFilterClause(')
195
+ expect(() => parse(route, { sourceType: 'module', plugins: ['jsx'] })).not.toThrow()
196
+ })
197
+
198
+ it('give every other SQL fetcher the "in" normalisation, and nothing else', () => {
199
+ const route = generateMySQLFetcher({ host: 'h' }, 'orders')
200
+ expect(route).toContain('normalizeInOperand(condition.operand, condition.destination)')
201
+ expect(route).not.toContain('productFilterClause')
202
+ expect(() => parse(route, { sourceType: 'module', plugins: ['jsx'] })).not.toThrow()
203
+ })
204
+
205
+ it('keep a non-products Teleport table on the ordinary branches', () => {
206
+ const route = generateTeleportFetcher({}, 'teleport_orders')
207
+ expect(route).toContain('function productFilterClause() {')
208
+ expect(route).not.toContain('jsonb_array_elements(CASE')
209
+ expect(() => parse(route, { sourceType: 'module', plugins: ['jsx'] })).not.toThrow()
210
+ })
211
+ })
212
+
213
+ describe('the SQL mirrored into database-provider-worker', () => {
214
+ // `database-provider-worker/src/lib/__tests__/product-filter-sql.fixture.json`
215
+ // is a copy of this file. Both repos pin their builders to it, so the canvas
216
+ // and the generated store can never disagree about what a filter means.
217
+ const fixture = JSON.parse(
218
+ readFileSync(join(__dirname, 'product-filter-sql.fixture.json'), 'utf8')
219
+ ) as Record<string, string>
220
+
221
+ it('matches the shared fixture byte for byte', () => {
222
+ expect(buildProductEffectivePriceSql()).toBe(fixture.effectivePrice)
223
+ expect(buildProductRatingBucketSql()).toBe(fixture.ratingBucket)
224
+ expect(buildProductOnSaleSql()).toBe(fixture.onSale)
225
+ expect(buildProductVariantAxisExistsSql('__KEY__', '__VALUES__')).toBe(
226
+ fixture.variantAxisExists
227
+ )
228
+ expect(PRODUCT_VARIANT_AXIS_KEY_PATTERN).toBe(fixture.axisKeyPattern)
229
+ })
230
+ })
@@ -0,0 +1,7 @@
1
+ {
2
+ "effectivePrice": "COALESCE((\n SELECT ROUND(GREATEST(0,\n CASE\n WHEN entry.kind = 'percentage'\n THEN \"price\"::numeric * (1 - LEAST(entry.amount, 100) / 100)\n ELSE \"price\"::numeric - entry.amount\n END\n ), 2)\n FROM (\n SELECT\n d.value->>'type' AS kind,\n CASE\n WHEN d.value->>'value' ~ '^[0-9]+(\\.[0-9]+)?$' THEN (d.value->>'value')::numeric\n ELSE 0\n END AS amount,\n d.value->>'startsAt' AS starts_at,\n d.value->>'endsAt' AS ends_at,\n d.ord AS ord\n FROM jsonb_array_elements(CASE\n WHEN \"discounts\" IS NULL OR btrim(\"discounts\") NOT LIKE '[%' THEN '[]'::jsonb\n ELSE \"discounts\"::jsonb\n END) WITH ORDINALITY AS d(value, ord)\n ) AS entry\n WHERE entry.amount > 0\n AND entry.kind IN ('percentage', 'fixed')\n AND (entry.starts_at IS NULL OR entry.starts_at <= to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"'))\n AND (entry.ends_at IS NULL OR entry.ends_at > to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"'))\n ORDER BY entry.ord\n LIMIT 1\n ), \"price\"::numeric)",
3
+ "ratingBucket": "(SELECT ROUND(AVG(r.rating)) FROM teleport_product_reviews r WHERE r.product_id = teleport_products.id AND r.status = 'approved' AND r.rating IS NOT NULL)",
4
+ "onSale": "(CASE WHEN COALESCE((\n SELECT ROUND(GREATEST(0,\n CASE\n WHEN entry.kind = 'percentage'\n THEN \"price\"::numeric * (1 - LEAST(entry.amount, 100) / 100)\n ELSE \"price\"::numeric - entry.amount\n END\n ), 2)\n FROM (\n SELECT\n d.value->>'type' AS kind,\n CASE\n WHEN d.value->>'value' ~ '^[0-9]+(\\.[0-9]+)?$' THEN (d.value->>'value')::numeric\n ELSE 0\n END AS amount,\n d.value->>'startsAt' AS starts_at,\n d.value->>'endsAt' AS ends_at,\n d.ord AS ord\n FROM jsonb_array_elements(CASE\n WHEN \"discounts\" IS NULL OR btrim(\"discounts\") NOT LIKE '[%' THEN '[]'::jsonb\n ELSE \"discounts\"::jsonb\n END) WITH ORDINALITY AS d(value, ord)\n ) AS entry\n WHERE entry.amount > 0\n AND entry.kind IN ('percentage', 'fixed')\n AND (entry.starts_at IS NULL OR entry.starts_at <= to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"'))\n AND (entry.ends_at IS NULL OR entry.ends_at > to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"'))\n ORDER BY entry.ord\n LIMIT 1\n ), \"price\"::numeric) < \"price\"::numeric THEN 'true' ELSE 'false' END)",
5
+ "variantAxisExists": "EXISTS (SELECT 1 FROM jsonb_array_elements(CASE\n WHEN \"variant_options\" IS NULL OR btrim(\"variant_options\") NOT LIKE '[%' THEN '[]'::jsonb\n ELSE \"variant_options\"::jsonb\n END) AS ax, jsonb_array_elements(CASE WHEN jsonb_typeof(ax->'values') = 'array' THEN ax->'values' ELSE '[]'::jsonb END) AS v WHERE ax->>'key' = __KEY__ AND v->>'value' = ANY(__VALUES__::text[]))",
6
+ "axisKeyPattern": "^[a-z0-9][a-z0-9_-]{0,63}$"
7
+ }
@@ -178,6 +178,28 @@ describe('pagination plugin — refetch loading state', () => {
178
178
  expect(code).toContain('ds_0_fetchesInFlight.current = 0')
179
179
  })
180
180
 
181
+ it('hands an overtaken response the newest rows, so a slow older request never paints over a newer one', async () => {
182
+ const code = await runPlugin({})
183
+
184
+ expect(code).toContain('const ds_0_fetchSeq = useRef(0)')
185
+ expect(code).toContain('const ds_0_latestData = useRef(undefined)')
186
+ // Numbered when it STARTS, before the flag goes up.
187
+ expect(code).toMatch(
188
+ /const __tqSeq = \+\+ds_0_fetchSeq\.current;?\s*ds_0_fetchesInFlight\.current \+= 1/
189
+ )
190
+ // Checked when it settles: overtaken → the newest settled rows (or its own
191
+ // while the successor is still in flight); newest → recorded and returned.
192
+ expect(code).toContain('if (__tqSeq !== ds_0_fetchSeq.current)')
193
+ expect(code).toContain(
194
+ 'return ds_0_latestData.current !== undefined ? ds_0_latestData.current : __tqRows'
195
+ )
196
+ expect(code).toContain('ds_0_latestData.current = __tqRows')
197
+ // The guard sits on the chain BEFORE the settle handler.
198
+ expect(code.indexOf('__tqSeq !== ds_0_fetchSeq.current')).toBeLessThan(
199
+ code.indexOf('.finally(() => {')
200
+ )
201
+ })
202
+
181
203
  it('keeps fetchData referentially stable so the provider does not refetch in a loop', async () => {
182
204
  const code = await runPlugin({})
183
205
 
@@ -249,6 +271,7 @@ describe('pagination plugin — refetch loading state', () => {
249
271
  const code = generator(chunk.content as types.Node).code
250
272
  expect(code).not.toContain('ds_0_isFetching')
251
273
  expect(code).not.toContain('ds_0_fetchesInFlight')
274
+ expect(code).not.toContain('ds_0_fetchSeq')
252
275
  })
253
276
 
254
277
  it('leaves a provider without a designed loading state untouched', async () => {
@@ -0,0 +1,122 @@
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
+ import { SEARCH_CLEAR_ATTR } from '../src/search-clear'
11
+
12
+ // tslint:disable:no-any
13
+
14
+ const element = (
15
+ tag: string,
16
+ attrs: Array<[string, string]>,
17
+ children: types.JSXElement['children'] = []
18
+ ): types.JSXElement =>
19
+ types.jsxElement(
20
+ types.jsxOpeningElement(
21
+ types.jsxIdentifier(tag),
22
+ attrs.map(([name, value]) =>
23
+ types.jsxAttribute(types.jsxIdentifier(name), types.stringLiteral(value))
24
+ ),
25
+ false
26
+ ),
27
+ types.jsxClosingElement(types.jsxIdentifier(tag)),
28
+ children,
29
+ false
30
+ )
31
+
32
+ const searchInput = (): types.JSXElement => element('input', [['className', 'search-input']])
33
+ const clearButton = (): types.JSXElement =>
34
+ element('button', [
35
+ ['type', 'button'],
36
+ [SEARCH_CLEAR_ATTR, 'true'],
37
+ ])
38
+
39
+ /**
40
+ * A component whose JSX is the field the editor builds — `<div><input
41
+ * class=search-input/><button data-tq-search-clear/></div>` — plus, when
42
+ * asked, a stray marked button somewhere else on the page.
43
+ */
44
+ const makeComponentChunk = (withStrayButton: boolean): ChunkDefinition => {
45
+ const field = element('div', [['data-tq-search-field', 'true']], [searchInput(), clearButton()])
46
+ const root = element(
47
+ 'div',
48
+ [],
49
+ withStrayButton ? [field, element('div', [], [clearButton()])] : [field]
50
+ )
51
+ const body = types.blockStatement([types.returnStatement(root)])
52
+ const arrow = types.arrowFunctionExpression([types.identifier('props')], body)
53
+ const declaration = types.variableDeclaration('const', [
54
+ types.variableDeclarator(types.identifier('TestComponent'), arrow),
55
+ ])
56
+ return {
57
+ name: 'jsx-component',
58
+ type: ChunkType.AST,
59
+ fileType: FileType.JS,
60
+ linkAfter: [],
61
+ content: declaration,
62
+ meta: {},
63
+ }
64
+ }
65
+
66
+ const makeUidlNode = (): any => ({
67
+ type: 'data-source-list',
68
+ content: {
69
+ renderPropIdentifier: 'items',
70
+ resourceDefinition: {
71
+ dataSourceId: 'ds1',
72
+ tableName: 'products',
73
+ dataSourceType: 'postgresql',
74
+ },
75
+ resource: { params: { queryColumns: { content: ['name'] } } },
76
+ nodes: {
77
+ success: {
78
+ type: 'cms-list-repeater',
79
+ content: {
80
+ renderPropIdentifier: 'product',
81
+ paginated: true,
82
+ perPage: 20,
83
+ searchEnabled: true,
84
+ searchDebounce: 300,
85
+ nodes: { list: { type: 'element', content: { elementType: 'div' } } },
86
+ },
87
+ },
88
+ },
89
+ },
90
+ })
91
+
92
+ const runPlugin = async (withStrayButton = false): Promise<string> => {
93
+ const chunk = makeComponentChunk(withStrayButton)
94
+ const structure: ComponentStructure = {
95
+ uidl: { name: 'TestComponent', node: makeUidlNode() },
96
+ chunks: [chunk],
97
+ dependencies: {},
98
+ options: { dataSources: {}, extractedResources: {} },
99
+ } as never
100
+ await createNextArrayMapperPaginationPlugin()(structure)
101
+ return generator(chunk.content as types.Node).code
102
+ }
103
+
104
+ const collapse = (code: string): string => code.replace(/\s+/g, ' ')
105
+
106
+ describe('pagination plugin — search clear button', () => {
107
+ it('empties the query on click and renders only while something is typed', async () => {
108
+ const flat = collapse(await runPlugin())
109
+ expect(flat).toContain('onClick={() => setDs_0_searchQuery("")}')
110
+ expect(flat).toContain('{ds_0_searchQuery ? <button type="button" data-tq-search-clear="true"')
111
+ expect(flat).toContain(': null}')
112
+ // The input beside it is the one the button clears.
113
+ expect(flat).toContain('value={ds_0_searchQuery}')
114
+ })
115
+
116
+ it('leaves a marked button that shares no parent with a search input alone', async () => {
117
+ const flat = collapse(await runPlugin(true))
118
+ expect(flat.match(/setDs_0_searchQuery\(""\)/g)).toHaveLength(1)
119
+ // The stray one keeps its markup, unwired and unconditional.
120
+ expect(flat).toContain('<div><button type="button" data-tq-search-clear="true"></button></div>')
121
+ })
122
+ })
@@ -0,0 +1,37 @@
1
+ import type { GeneratorOptions } from '@teleporthq/teleport-types';
2
+ /**
3
+ * The languages a project's DATABASE content is stored in.
4
+ *
5
+ * Translatable columns of the platform tables (`teleport_products.name`,
6
+ * `teleport_blog_posts.title`, …) hold the MAIN language, and every other
7
+ * language of the project owns a `<locale>_<column>` copy (`es_name`) — the
8
+ * editor creates those columns when a language is added. The generated
9
+ * fetcher resolves a row against the locale the visitor is reading in, so it
10
+ * needs to know which locale is the main one (never prefixed) and which
11
+ * locales may have a prefixed copy.
12
+ */
13
+ export interface ContentLocalization {
14
+ /** The locale whose values live in the base columns, never in a prefixed copy. */
15
+ mainLocale: string;
16
+ /** Every other locale of the project — each may own a `<locale>_<column>` copy. */
17
+ secondaryLocales: string[];
18
+ }
19
+ /**
20
+ * The options both a generator pass (`GeneratorOptions`) and a project plugin
21
+ * (the `ProjectUIDL` itself) can hand over — the two carry the same
22
+ * `internationalization` shape.
23
+ */
24
+ export type LocalizedProjectOptions = Pick<GeneratorOptions, 'internationalization'>;
25
+ /**
26
+ * Derives the content localization from the project's i18n settings, or
27
+ * `undefined` for a single-language project — in which case nothing about a
28
+ * generated fetch mentions a locale, exactly as before languages existed.
29
+ */
30
+ export declare const resolveContentLocalization: (options: LocalizedProjectOptions) => ContentLocalization | undefined;
31
+ /**
32
+ * True when the project has a language setup at all — the same condition under
33
+ * which the Next generator emits its i18n routing (`skipI18n`), and therefore
34
+ * the condition under which a fetch has to say which language it is for.
35
+ */
36
+ export declare const isLocalizedProject: (options: LocalizedProjectOptions) => boolean;
37
+ //# sourceMappingURL=content-localization.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-localization.d.ts","sourceRoot":"","sources":["../../src/content-localization.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAElE;;;;;;;;;;GAUG;AACH,MAAM,WAAW,mBAAmB;IAClC,kFAAkF;IAClF,UAAU,EAAE,MAAM,CAAA;IAClB,mFAAmF;IACnF,gBAAgB,EAAE,MAAM,EAAE,CAAA;CAC3B;AAED;;;;GAIG;AACH,MAAM,MAAM,uBAAuB,GAAG,IAAI,CAAC,gBAAgB,EAAE,sBAAsB,CAAC,CAAA;AAEpF;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,YAC5B,uBAAuB,KAC/B,mBAAmB,GAAG,SAUxB,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,YAAa,uBAAuB,KAAG,OACnB,CAAA"}
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isLocalizedProject = exports.resolveContentLocalization = void 0;
4
+ /**
5
+ * Derives the content localization from the project's i18n settings, or
6
+ * `undefined` for a single-language project — in which case nothing about a
7
+ * generated fetch mentions a locale, exactly as before languages existed.
8
+ */
9
+ var resolveContentLocalization = function (options) {
10
+ var _a;
11
+ var internationalization = options.internationalization;
12
+ var mainLocale = (_a = internationalization === null || internationalization === void 0 ? void 0 : internationalization.main) === null || _a === void 0 ? void 0 : _a.locale;
13
+ if (!mainLocale) {
14
+ return undefined;
15
+ }
16
+ var secondaryLocales = Object.keys(internationalization.languages || {}).filter(function (locale) { return locale !== mainLocale; });
17
+ return { mainLocale: mainLocale, secondaryLocales: secondaryLocales };
18
+ };
19
+ exports.resolveContentLocalization = resolveContentLocalization;
20
+ /**
21
+ * True when the project has a language setup at all — the same condition under
22
+ * which the Next generator emits its i18n routing (`skipI18n`), and therefore
23
+ * the condition under which a fetch has to say which language it is for.
24
+ */
25
+ var isLocalizedProject = function (options) {
26
+ return (0, exports.resolveContentLocalization)(options) !== undefined;
27
+ };
28
+ exports.isLocalizedProject = isLocalizedProject;
29
+ //# sourceMappingURL=content-localization.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-localization.js","sourceRoot":"","sources":["../../src/content-localization.ts"],"names":[],"mappings":";;;AA2BA;;;;GAIG;AACI,IAAM,0BAA0B,GAAG,UACxC,OAAgC;;IAEhC,IAAM,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAA;IACzD,IAAM,UAAU,GAAG,MAAA,oBAAoB,aAApB,oBAAoB,uBAApB,oBAAoB,CAAE,IAAI,0CAAE,MAAM,CAAA;IACrD,IAAI,CAAC,UAAU,EAAE;QACf,OAAO,SAAS,CAAA;KACjB;IACD,IAAM,gBAAgB,GAAG,MAAM,CAAC,IAAI,CAAC,oBAAoB,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,MAAM,CAC/E,UAAC,MAAM,IAAK,OAAA,MAAM,KAAK,UAAU,EAArB,CAAqB,CAClC,CAAA;IACD,OAAO,EAAE,UAAU,YAAA,EAAE,gBAAgB,kBAAA,EAAE,CAAA;AACzC,CAAC,CAAA;AAZY,QAAA,0BAA0B,8BAYtC;AAED;;;;GAIG;AACI,IAAM,kBAAkB,GAAG,UAAC,OAAgC;IACjE,OAAA,IAAA,kCAA0B,EAAC,OAAO,CAAC,KAAK,SAAS;AAAjD,CAAiD,CAAA;AADtC,QAAA,kBAAkB,sBACoB"}
@@ -27,8 +27,18 @@ interface CountEffectOptions {
27
27
  urlParams: types.ObjectProperty[];
28
28
  /** Dependencies that should re-run the count. */
29
29
  deps: types.Expression[];
30
+ /**
31
+ * Name of the `useRef(0)` counting the count requests STARTED. Each request
32
+ * notes its sequence number and a response overtaken by a newer request is
33
+ * dropped: two filter changes in quick succession start two counts, and the
34
+ * older, slower one landing last painted a page strip for a result set the
35
+ * list no longer shows.
36
+ */
37
+ countSeqRefVar: string;
30
38
  }
31
- export declare const buildCountFetchStatement: (options: Pick<CountEffectOptions, 'fileName' | 'perPage' | 'setMaxPagesVar' | 'urlParams'>) => types.Statement;
39
+ /** `const ds_N_countSeq = useRef(0)` */
40
+ export declare const buildCountSeqDeclaration: (countSeqRefVar: string) => types.Statement;
41
+ export declare const buildCountFetchStatement: (options: Pick<CountEffectOptions, 'fileName' | 'perPage' | 'setMaxPagesVar' | 'urlParams' | 'countSeqRefVar'>) => types.Statement;
32
42
  /**
33
43
  * The whole `useEffect(...)` wrapper.
34
44
  *
@@ -1 +1 @@
1
- {"version":3,"file":"count-effect.d.ts","sourceRoot":"","sources":["../../src/count-effect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAErC;;;;;;;;;;;;GAYG;AAEH,UAAU,kBAAkB;IAC1B,qDAAqD;IACrD,QAAQ,EAAE,MAAM,CAAA;IAChB,mEAAmE;IACnE,OAAO,EAAE,MAAM,CAAA;IACf,6CAA6C;IAC7C,cAAc,EAAE,MAAM,CAAA;IACtB;;;;OAIG;IACH,SAAS,EAAE,KAAK,CAAC,cAAc,EAAE,CAAA;IACjC,iDAAiD;IACjD,IAAI,EAAE,KAAK,CAAC,UAAU,EAAE,CAAA;CACzB;AA6BD,eAAO,MAAM,wBAAwB,YAC1B,KAAK,kBAAkB,EAAE,UAAU,GAAG,SAAS,GAAG,gBAAgB,GAAG,WAAW,CAAC,KACzF,MAAM,SA0DN,CAAA;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,YAAa,kBAAkB,KAAG,MAAM,mBAMvE,CAAA"}
1
+ {"version":3,"file":"count-effect.d.ts","sourceRoot":"","sources":["../../src/count-effect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAErC;;;;;;;;;;;;GAYG;AAEH,UAAU,kBAAkB;IAC1B,qDAAqD;IACrD,QAAQ,EAAE,MAAM,CAAA;IAChB,mEAAmE;IACnE,OAAO,EAAE,MAAM,CAAA;IACf,6CAA6C;IAC7C,cAAc,EAAE,MAAM,CAAA;IACtB;;;;OAIG;IACH,SAAS,EAAE,KAAK,CAAC,cAAc,EAAE,CAAA;IACjC,iDAAiD;IACjD,IAAI,EAAE,KAAK,CAAC,UAAU,EAAE,CAAA;IACxB;;;;;;OAMG;IACH,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,wCAAwC;AACxC,eAAO,MAAM,wBAAwB,mBAAoB,MAAM,KAAG,MAAM,SAMpE,CAAA;AAqDJ,eAAO,MAAM,wBAAwB,YAC1B,KACP,kBAAkB,EAClB,UAAU,GAAG,SAAS,GAAG,gBAAgB,GAAG,WAAW,GAAG,gBAAgB,CAC3E,KACA,MAAM,SA2DN,CAAA;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,YAAa,kBAAkB,KAAG,MAAM,mBAYvE,CAAA"}
@@ -23,8 +23,19 @@ var __importStar = (this && this.__importStar) || function (mod) {
23
23
  return result;
24
24
  };
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.buildCountFetchEffect = exports.buildCountFetchStatement = void 0;
26
+ exports.buildCountFetchEffect = exports.buildCountFetchStatement = exports.buildCountSeqDeclaration = void 0;
27
27
  var types = __importStar(require("@babel/types"));
28
+ /** `const ds_N_countSeq = useRef(0)` */
29
+ var buildCountSeqDeclaration = function (countSeqRefVar) {
30
+ return types.variableDeclaration('const', [
31
+ types.variableDeclarator(types.identifier(countSeqRefVar), types.callExpression(types.identifier('useRef'), [types.numericLiteral(0)])),
32
+ ]);
33
+ };
34
+ exports.buildCountSeqDeclaration = buildCountSeqDeclaration;
35
+ var COUNT_SEQ_LOCAL = '__tqCountSeq';
36
+ var refCurrent = function (refVar) {
37
+ return types.memberExpression(types.identifier(refVar), types.identifier('current'));
38
+ };
28
39
  /**
29
40
  * `data.count === 0 ? 0 : Math.ceil(data.count / perPage)`
30
41
  *
@@ -37,6 +48,19 @@ var buildMaxPagesExpression = function (perPage) {
37
48
  types.binaryExpression('/', types.memberExpression(types.identifier('data'), types.identifier('count')), types.numericLiteral(perPage)),
38
49
  ]));
39
50
  };
51
+ /**
52
+ * `const __tqCountSeq = ++ds_N_countSeq.current` — the request's sequence
53
+ * number, taken before the fetch starts.
54
+ */
55
+ var buildCountSeqCapture = function (countSeqRefVar) {
56
+ return types.variableDeclaration('const', [
57
+ types.variableDeclarator(types.identifier(COUNT_SEQ_LOCAL), types.updateExpression('++', refCurrent(countSeqRefVar), true)),
58
+ ]);
59
+ };
60
+ /** `if (__tqCountSeq !== ds_N_countSeq.current) return` — a newer count is in flight. */
61
+ var buildStaleCountGuard = function (countSeqRefVar) {
62
+ return types.ifStatement(types.binaryExpression('!==', types.identifier(COUNT_SEQ_LOCAL), refCurrent(countSeqRefVar)), types.returnStatement());
63
+ };
40
64
  var buildCountFetchStatement = function (options) {
41
65
  return types.expressionStatement(types.callExpression(types.memberExpression(types.callExpression(types.memberExpression(types.callExpression(types.identifier('fetch'), [
42
66
  types.templateLiteral([
@@ -54,6 +78,7 @@ var buildCountFetchStatement = function (options) {
54
78
  types.arrowFunctionExpression([types.identifier('res')], types.callExpression(types.memberExpression(types.identifier('res'), types.identifier('json')), [])),
55
79
  ]), types.identifier('then')), [
56
80
  types.arrowFunctionExpression([types.identifier('data')], types.blockStatement([
81
+ buildStaleCountGuard(options.countSeqRefVar),
57
82
  types.ifStatement(types.logicalExpression('&&', types.identifier('data'), types.binaryExpression('in', types.stringLiteral('count'), types.identifier('data'))), types.blockStatement([
58
83
  types.expressionStatement(types.callExpression(types.identifier(options.setMaxPagesVar), [
59
84
  buildMaxPagesExpression(options.perPage),
@@ -73,7 +98,10 @@ exports.buildCountFetchStatement = buildCountFetchStatement;
73
98
  */
74
99
  var buildCountFetchEffect = function (options) {
75
100
  return types.expressionStatement(types.callExpression(types.identifier('useEffect'), [
76
- types.arrowFunctionExpression([], types.blockStatement([(0, exports.buildCountFetchStatement)(options)])),
101
+ types.arrowFunctionExpression([], types.blockStatement([
102
+ buildCountSeqCapture(options.countSeqRefVar),
103
+ (0, exports.buildCountFetchStatement)(options),
104
+ ])),
77
105
  types.arrayExpression(options.deps),
78
106
  ]));
79
107
  };
@@ -1 +1 @@
1
- {"version":3,"file":"count-effect.js","sourceRoot":"","sources":["../../src/count-effect.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"count-effect.js","sourceRoot":"","sources":["../../src/count-effect.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"airtable.d.ts","sourceRoot":"","sources":["../../../src/fetchers/airtable.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,sBAAsB,WACzB,OAAO,MAAM,EAAE,OAAO,CAAC,KAC9B;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAcpC,CAAA;AAQD,eAAO,MAAM,uBAAuB,WAC1B,OAAO,MAAM,EAAE,OAAO,CAAC,aACpB,MAAM,KAChB,MAqNF,CAAA"}
1
+ {"version":3,"file":"airtable.d.ts","sourceRoot":"","sources":["../../../src/fetchers/airtable.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,sBAAsB,WACzB,OAAO,MAAM,EAAE,OAAO,CAAC,KAC9B;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAcpC,CAAA;AAQD,eAAO,MAAM,uBAAuB,WAC1B,OAAO,MAAM,EAAE,OAAO,CAAC,aACpB,MAAM,KAChB,MAuNF,CAAA"}
@@ -19,7 +19,7 @@ var generateAirtableFetcher = function (config, tableName) {
19
19
  var airtableConfig = config;
20
20
  var baseId = airtableConfig.baseId;
21
21
  var personalAccessToken = airtableConfig.personalAccessToken;
22
- return "import fetch from 'node-fetch'\n\n".concat((0, utils_1.generateSafeJSONParseCode)(), "\n\n").concat((0, utils_1.generateFilterTreeHelpersCode)(), "\n\n").concat((0, utils_1.generateDateFormatterCode)(), "\n\n// Escape a string literal for Airtable formula grammar. Strings use\n// single quotes; embedded single-quote must be backslash-escaped.\n// Backslash itself also escapes.\nconst escapeAirtableString = (s) => {\n return \"'\" + String(s).replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\") + \"'\"\n}\n\n// Escape an Airtable field reference name for use inside curly braces.\n// Only close-brace and backslash are special inside the braces.\nconst escapeAirtableFieldRef = (name) => {\n return String(name).replace(/\\\\/g, '\\\\\\\\').replace(/}/g, '\\\\}')\n}\n\nconst buildAirtableSearchFormula = (rawQuery, rawQueryColumns) => {\n if (typeof rawQuery !== 'string' || rawQuery.trim() === '') return ''\n let cols = []\n if (rawQueryColumns) {\n const parsed = safeJSONParse(rawQueryColumns)\n cols = Array.isArray(parsed) ? parsed : (parsed ? [parsed] : [])\n }\n const validCols = cols.filter((c) => typeof c === 'string' && c.length > 0)\n if (validCols.length === 0) return ''\n const literal = escapeAirtableString(rawQuery)\n const parts = validCols.map(\n (col) => 'SEARCH(' + literal + ', {' + escapeAirtableFieldRef(col) + '})'\n )\n return parts.length === 1 ? parts[0] : 'OR(' + parts.join(',') + ')'\n}\n\nexport default async function handler(req, res) {\n try {\n const { query, queryColumns, view, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset: offsetParam } = req.query\n\n const queryParams = new URLSearchParams()\n const formulaParts = []\n\n if (view) {\n queryParams.append('view', view)\n }\n \n // Handle sorts - new array format\n if (sorts) {\n const parsedSorts = safeJSONParse(sorts)\n if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {\n parsedSorts.forEach((sort, index) => {\n if (!sort.field) return\n queryParams.append(`sort[${index}][field]`, sort.field)\n queryParams.append(`sort[${index}][direction]`, (sort.order || '').toLowerCase().startsWith('desc') ? 'desc' : 'asc')\n })\n }\n } else if (sortBy) {\n queryParams.append('sort[0][field]', sortBy)\n queryParams.append('sort[0][direction]', (sortOrder || '').toLowerCase().startsWith('desc') ? 'desc' : 'asc')\n }\n \n const perPageValue = limit || perPage || 100\n queryParams.append('pageSize', Math.min(parseInt(perPageValue), 100).toString())\n \n const formatAirtableValue = (value) => {\n if (typeof value === 'string') {\n return `'${value.replace(/'/g, \"\\\\'\")}'`\n } else if (typeof value === 'number') {\n return String(value)\n } else if (typeof value === 'boolean') {\n return value ? 'TRUE()' : 'FALSE()'\n }\n return `'${String(value)}'`\n }\n \n if (filters) {\n const filterTree = normalizeFilterTree(safeJSONParse(filters))\n \n if (filterTree) {\n const buildCondition = (condition) => {\n const field = condition.source\n const value = condition.destination\n const operand = condition.operand\n \n if (Array.isArray(value)) {\n if (value.length === 0) return null\n const arrayConditions = value.map((v) => `{${field}}=${formatAirtableValue(v)}`)\n return arrayConditions.length > 1\n ? `OR(${arrayConditions.join(',')})`\n : arrayConditions[0]\n }\n \n const operatorMap = {\n '=': '=',\n '!=': '!=',\n '>': '>',\n '<': '<',\n '>=': '>=',\n '<=': '<=',\n }\n const airtableOp = operatorMap[operand] || '='\n return `{${field}}${airtableOp}${formatAirtableValue(value)}`\n }\n \n // Airtable spells its logic as AND(a,b) / OR(a,b) rather than infix, so\n // the tree is walked here instead of with buildFilterTreeClause.\n const buildNode = (node) => {\n if (node.type === 'group') {\n const parts = node.children.map(buildNode).filter(Boolean)\n if (parts.length === 0) return null\n if (parts.length === 1) return parts[0]\n return node.operator === 'or'\n ? `OR(${parts.join(',')})`\n : `AND(${parts.join(',')})`\n }\n return buildCondition(node)\n }\n \n const filterFormula = buildNode(filterTree)\n if (filterFormula) {\n formulaParts.push(filterFormula)\n }\n }\n }\n\n // Apply the { query, queryColumns } search contract via the\n // Airtable SEARCH() function. Search-term string literal escaping\n // protects against single-quote injection; field-ref escaping\n // protects against close-brace or backslash breaking out of the\n // {field} reference.\n const searchFormula = buildAirtableSearchFormula(query, queryColumns)\n if (searchFormula) {\n formulaParts.push(searchFormula)\n }\n\n if (formulaParts.length === 1) {\n queryParams.append('filterByFormula', formulaParts[0])\n } else if (formulaParts.length > 1) {\n queryParams.append('filterByFormula', 'AND(' + formulaParts.join(',') + ')')\n }\n \n let url = `https://api.airtable.com/v0/").concat(baseId, "/${encodeURIComponent('").concat(tableName, "')}`\n if (queryParams.toString()) {\n url += `?${queryParams.toString()}`\n }\n \n const allRecords = []\n let airtableOffset\n const skipValue = offsetParam !== undefined ? parseInt(offsetParam) : (page ? (parseInt(page) - 1) * parseInt(perPageValue) : 0)\n const totalRecordsNeeded = skipValue + parseInt(perPageValue)\n \n do {\n const fetchUrl = airtableOffset ? `${url}&offset=${airtableOffset}` : url\n const response = await fetch(fetchUrl, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ").concat((0, utils_1.replaceSecretReference)(personalAccessToken, {
22
+ return "import fetch from 'node-fetch'\n\n".concat((0, utils_1.generateSafeJSONParseCode)(), "\n\n").concat((0, utils_1.generateFilterTreeHelpersCode)(), "\n\n").concat((0, utils_1.generateDateFormatterCode)(), "\n\n// Escape a string literal for Airtable formula grammar. Strings use\n// single quotes; embedded single-quote must be backslash-escaped.\n// Backslash itself also escapes.\nconst escapeAirtableString = (s) => {\n return \"'\" + String(s).replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\") + \"'\"\n}\n\n// Escape an Airtable field reference name for use inside curly braces.\n// Only close-brace and backslash are special inside the braces.\nconst escapeAirtableFieldRef = (name) => {\n return String(name).replace(/\\\\/g, '\\\\\\\\').replace(/}/g, '\\\\}')\n}\n\nconst buildAirtableSearchFormula = (rawQuery, rawQueryColumns) => {\n if (typeof rawQuery !== 'string' || rawQuery.trim() === '') return ''\n let cols = []\n if (rawQueryColumns) {\n const parsed = safeJSONParse(rawQueryColumns)\n cols = Array.isArray(parsed) ? parsed : (parsed ? [parsed] : [])\n }\n const validCols = cols.filter((c) => typeof c === 'string' && c.length > 0)\n if (validCols.length === 0) return ''\n const literal = escapeAirtableString(rawQuery)\n const parts = validCols.map(\n (col) => 'SEARCH(' + literal + ', {' + escapeAirtableFieldRef(col) + '})'\n )\n return parts.length === 1 ? parts[0] : 'OR(' + parts.join(',') + ')'\n}\n\nexport default async function handler(req, res) {\n try {\n const { query, queryColumns, view, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset: offsetParam } = req.query\n\n const queryParams = new URLSearchParams()\n const formulaParts = []\n\n if (view) {\n queryParams.append('view', view)\n }\n \n // Handle sorts - new array format\n if (sorts) {\n const parsedSorts = safeJSONParse(sorts)\n if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {\n parsedSorts.forEach((sort, index) => {\n if (!sort.field) return\n queryParams.append(`sort[${index}][field]`, sort.field)\n queryParams.append(`sort[${index}][direction]`, (sort.order || '').toLowerCase().startsWith('desc') ? 'desc' : 'asc')\n })\n }\n } else if (sortBy) {\n queryParams.append('sort[0][field]', sortBy)\n queryParams.append('sort[0][direction]', (sortOrder || '').toLowerCase().startsWith('desc') ? 'desc' : 'asc')\n }\n \n const perPageValue = limit || perPage || 100\n queryParams.append('pageSize', Math.min(parseInt(perPageValue), 100).toString())\n \n const formatAirtableValue = (value) => {\n if (typeof value === 'string') {\n return `'${value.replace(/'/g, \"\\\\'\")}'`\n } else if (typeof value === 'number') {\n return String(value)\n } else if (typeof value === 'boolean') {\n return value ? 'TRUE()' : 'FALSE()'\n }\n return `'${String(value)}'`\n }\n \n if (filters) {\n const filterTree = normalizeFilterTree(safeJSONParse(filters))\n \n if (filterTree) {\n const buildCondition = (condition) => {\n const field = condition.source\n const normalizedCondition = normalizeInOperand(condition.operand, condition.destination)\n if (normalizedCondition === null) return null\n const value = normalizedCondition.value\n const operand = normalizedCondition.operand\n \n if (Array.isArray(value)) {\n if (value.length === 0) return null\n const arrayConditions = value.map((v) => `{${field}}=${formatAirtableValue(v)}`)\n return arrayConditions.length > 1\n ? `OR(${arrayConditions.join(',')})`\n : arrayConditions[0]\n }\n \n const operatorMap = {\n '=': '=',\n '!=': '!=',\n '>': '>',\n '<': '<',\n '>=': '>=',\n '<=': '<=',\n }\n const airtableOp = operatorMap[operand] || '='\n return `{${field}}${airtableOp}${formatAirtableValue(value)}`\n }\n \n // Airtable spells its logic as AND(a,b) / OR(a,b) rather than infix, so\n // the tree is walked here instead of with buildFilterTreeClause.\n const buildNode = (node) => {\n if (node.type === 'group') {\n const parts = node.children.map(buildNode).filter(Boolean)\n if (parts.length === 0) return null\n if (parts.length === 1) return parts[0]\n return node.operator === 'or'\n ? `OR(${parts.join(',')})`\n : `AND(${parts.join(',')})`\n }\n return buildCondition(node)\n }\n \n const filterFormula = buildNode(filterTree)\n if (filterFormula) {\n formulaParts.push(filterFormula)\n }\n }\n }\n\n // Apply the { query, queryColumns } search contract via the\n // Airtable SEARCH() function. Search-term string literal escaping\n // protects against single-quote injection; field-ref escaping\n // protects against close-brace or backslash breaking out of the\n // {field} reference.\n const searchFormula = buildAirtableSearchFormula(query, queryColumns)\n if (searchFormula) {\n formulaParts.push(searchFormula)\n }\n\n if (formulaParts.length === 1) {\n queryParams.append('filterByFormula', formulaParts[0])\n } else if (formulaParts.length > 1) {\n queryParams.append('filterByFormula', 'AND(' + formulaParts.join(',') + ')')\n }\n \n let url = `https://api.airtable.com/v0/").concat(baseId, "/${encodeURIComponent('").concat(tableName, "')}`\n if (queryParams.toString()) {\n url += `?${queryParams.toString()}`\n }\n \n const allRecords = []\n let airtableOffset\n const skipValue = offsetParam !== undefined ? parseInt(offsetParam) : (page ? (parseInt(page) - 1) * parseInt(perPageValue) : 0)\n const totalRecordsNeeded = skipValue + parseInt(perPageValue)\n \n do {\n const fetchUrl = airtableOffset ? `${url}&offset=${airtableOffset}` : url\n const response = await fetch(fetchUrl, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ").concat((0, utils_1.replaceSecretReference)(personalAccessToken, {
23
23
  templateLiteral: true,
24
24
  }), "`,\n 'Content-Type': 'application/json'\n }\n })\n \n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}))\n return res.status(response.status).json({\n success: false,\n error: errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`,\n timestamp: Date.now()\n })\n }\n \n const data = await response.json()\n allRecords.push(...data.records)\n airtableOffset = data.offset\n \n if (allRecords.length >= totalRecordsNeeded || !airtableOffset) {\n break\n }\n } while (airtableOffset)\n \n const paginatedRecords = allRecords.slice(skipValue, skipValue + parseInt(perPageValue))\n \n const formattedRecords = paginatedRecords.map((record) => ({\n id: record.id,\n ...record.fields,\n createdTime: record.createdTime\n }))\n \n const safeData = JSON.parse(JSON.stringify(formattedRecords, dateReplacer))\n \n return res.status(200).json({\n success: true,\n data: safeData,\n timestamp: Date.now()\n })\n } catch (error) {\n console.error('Airtable fetch error:', error)\n return res.status(500).json({\n success: false,\n error: error.message || 'Failed to fetch data',\n timestamp: Date.now()\n })\n }\n}\n");
25
25
  };
@@ -1 +1 @@
1
- {"version":3,"file":"airtable.js","sourceRoot":"","sources":["../../../src/fetchers/airtable.ts"],"names":[],"mappings":";;;AAAA,kCAKiB;AAEV,IAAM,sBAAsB,GAAG,UACpC,MAA+B;IAE/B,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE;QACzC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,+BAA+B,EAAE,CAAA;KAClE;IAED,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;QACtF,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,8BAA8B,EAAE,CAAA;KACjE;IAED,IAAI,CAAC,MAAM,CAAC,mBAAmB,IAAI,OAAO,MAAM,CAAC,mBAAmB,KAAK,QAAQ,EAAE;QACjF,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,4CAA4C,EAAE,CAAA;KAC/E;IAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;AAC1B,CAAC,CAAA;AAhBY,QAAA,sBAAsB,0BAgBlC;AAQM,IAAM,uBAAuB,GAAG,UACrC,MAA+B,EAC/B,SAAiB;IAEjB,IAAM,cAAc,GAAG,MAAwB,CAAA;IAC/C,IAAM,MAAM,GAAG,cAAc,CAAC,MAAM,CAAA;IACpC,IAAM,mBAAmB,GAAG,cAAc,CAAC,mBAAmB,CAAA;IAE9D,OAAO,4CAEP,IAAA,iCAAyB,GAAE,iBAE3B,IAAA,qCAA6B,GAAE,iBAE/B,IAAA,iCAAyB,GAAE,irKAyIiB,MAAM,oCAA2B,SAAS,wkBAepD,IAAA,8BAAsB,EAAC,mBAAmB,EAAE;QACpE,eAAe,EAAE,IAAI;KACtB,CAAC,+1CA+CX,CAAA;AACD,CAAC,CAAA;AAxNY,QAAA,uBAAuB,2BAwNnC"}
1
+ {"version":3,"file":"airtable.js","sourceRoot":"","sources":["../../../src/fetchers/airtable.ts"],"names":[],"mappings":";;;AAAA,kCAKiB;AAEV,IAAM,sBAAsB,GAAG,UACpC,MAA+B;IAE/B,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE;QACzC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,+BAA+B,EAAE,CAAA;KAClE;IAED,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;QACtF,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,8BAA8B,EAAE,CAAA;KACjE;IAED,IAAI,CAAC,MAAM,CAAC,mBAAmB,IAAI,OAAO,MAAM,CAAC,mBAAmB,KAAK,QAAQ,EAAE;QACjF,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,4CAA4C,EAAE,CAAA;KAC/E;IAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;AAC1B,CAAC,CAAA;AAhBY,QAAA,sBAAsB,0BAgBlC;AAQM,IAAM,uBAAuB,GAAG,UACrC,MAA+B,EAC/B,SAAiB;IAEjB,IAAM,cAAc,GAAG,MAAwB,CAAA;IAC/C,IAAM,MAAM,GAAG,cAAc,CAAC,MAAM,CAAA;IACpC,IAAM,mBAAmB,GAAG,cAAc,CAAC,mBAAmB,CAAA;IAE9D,OAAO,4CAEP,IAAA,iCAAyB,GAAE,iBAE3B,IAAA,qCAA6B,GAAE,iBAE/B,IAAA,iCAAyB,GAAE,41KA2IiB,MAAM,oCAA2B,SAAS,wkBAepD,IAAA,8BAAsB,EAAC,mBAAmB,EAAE;QACpE,eAAe,EAAE,IAAI;KACtB,CAAC,+1CA+CX,CAAA;AACD,CAAC,CAAA;AA1NY,QAAA,uBAAuB,2BA0NnC"}