@teleporthq/teleport-plugin-next-data-source 0.43.42 → 0.43.46

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 (87) hide show
  1. package/__tests__/ecommerce-product-variant-swatch.test.ts +93 -0
  2. package/__tests__/fetchers.test.ts +11 -0
  3. package/dist/cjs/array-mapper-registry.d.ts +0 -1
  4. package/dist/cjs/array-mapper-registry.d.ts.map +1 -1
  5. package/dist/cjs/array-mapper-registry.js +0 -3
  6. package/dist/cjs/array-mapper-registry.js.map +1 -1
  7. package/dist/cjs/data-source-fetchers.d.ts +3 -3
  8. package/dist/cjs/data-source-fetchers.d.ts.map +1 -1
  9. package/dist/cjs/data-source-fetchers.js +4 -4
  10. package/dist/cjs/data-source-fetchers.js.map +1 -1
  11. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  12. package/dist/cjs/fetchers/postgresql.js +1 -1
  13. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  14. package/dist/cjs/fetchers/teleport.d.ts +2 -1
  15. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  16. package/dist/cjs/fetchers/teleport.js +3 -3
  17. package/dist/cjs/fetchers/teleport.js.map +1 -1
  18. package/dist/cjs/filter-utils.d.ts.map +1 -1
  19. package/dist/cjs/filter-utils.js +12 -3
  20. package/dist/cjs/filter-utils.js.map +1 -1
  21. package/dist/cjs/index.d.ts.map +1 -1
  22. package/dist/cjs/index.js +8 -7
  23. package/dist/cjs/index.js.map +1 -1
  24. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  25. package/dist/cjs/pagination-plugin.js +12 -19
  26. package/dist/cjs/pagination-plugin.js.map +1 -1
  27. package/dist/cjs/transformations/ecommerce-product.d.ts +2 -1
  28. package/dist/cjs/transformations/ecommerce-product.d.ts.map +1 -1
  29. package/dist/cjs/transformations/ecommerce-product.js +36 -2
  30. package/dist/cjs/transformations/ecommerce-product.js.map +1 -1
  31. package/dist/cjs/transformations/index.d.ts +2 -1
  32. package/dist/cjs/transformations/index.d.ts.map +1 -1
  33. package/dist/cjs/transformations/index.js +9 -3
  34. package/dist/cjs/transformations/index.js.map +1 -1
  35. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  36. package/dist/cjs/utils.d.ts +3 -3
  37. package/dist/cjs/utils.d.ts.map +1 -1
  38. package/dist/cjs/utils.js +4 -4
  39. package/dist/cjs/utils.js.map +1 -1
  40. package/dist/esm/array-mapper-registry.d.ts +0 -1
  41. package/dist/esm/array-mapper-registry.d.ts.map +1 -1
  42. package/dist/esm/array-mapper-registry.js +0 -3
  43. package/dist/esm/array-mapper-registry.js.map +1 -1
  44. package/dist/esm/data-source-fetchers.d.ts +3 -3
  45. package/dist/esm/data-source-fetchers.d.ts.map +1 -1
  46. package/dist/esm/data-source-fetchers.js +4 -4
  47. package/dist/esm/data-source-fetchers.js.map +1 -1
  48. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  49. package/dist/esm/fetchers/postgresql.js +1 -1
  50. package/dist/esm/fetchers/postgresql.js.map +1 -1
  51. package/dist/esm/fetchers/teleport.d.ts +2 -1
  52. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  53. package/dist/esm/fetchers/teleport.js +3 -3
  54. package/dist/esm/fetchers/teleport.js.map +1 -1
  55. package/dist/esm/filter-utils.d.ts.map +1 -1
  56. package/dist/esm/filter-utils.js +12 -3
  57. package/dist/esm/filter-utils.js.map +1 -1
  58. package/dist/esm/index.d.ts.map +1 -1
  59. package/dist/esm/index.js +8 -7
  60. package/dist/esm/index.js.map +1 -1
  61. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  62. package/dist/esm/pagination-plugin.js +12 -19
  63. package/dist/esm/pagination-plugin.js.map +1 -1
  64. package/dist/esm/transformations/ecommerce-product.d.ts +2 -1
  65. package/dist/esm/transformations/ecommerce-product.d.ts.map +1 -1
  66. package/dist/esm/transformations/ecommerce-product.js +36 -2
  67. package/dist/esm/transformations/ecommerce-product.js.map +1 -1
  68. package/dist/esm/transformations/index.d.ts +2 -1
  69. package/dist/esm/transformations/index.d.ts.map +1 -1
  70. package/dist/esm/transformations/index.js +9 -3
  71. package/dist/esm/transformations/index.js.map +1 -1
  72. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  73. package/dist/esm/utils.d.ts +3 -3
  74. package/dist/esm/utils.d.ts.map +1 -1
  75. package/dist/esm/utils.js +4 -4
  76. package/dist/esm/utils.js.map +1 -1
  77. package/package.json +5 -5
  78. package/src/array-mapper-registry.ts +0 -5
  79. package/src/data-source-fetchers.ts +10 -5
  80. package/src/fetchers/postgresql.ts +17 -0
  81. package/src/fetchers/teleport.ts +38 -3
  82. package/src/filter-utils.ts +32 -5
  83. package/src/index.ts +9 -4
  84. package/src/pagination-plugin.ts +20 -42
  85. package/src/transformations/ecommerce-product.ts +430 -1
  86. package/src/transformations/index.ts +28 -4
  87. package/src/utils.ts +12 -4
@@ -1,4 +1,4 @@
1
- import { DataSourceType, UIDLDataSource } from '@teleporthq/teleport-types'
1
+ import { DataSourceType, UIDLDataSource, UIDLEcommerceCategory } from '@teleporthq/teleport-types'
2
2
  import {
3
3
  generatePostgreSQLFetcher,
4
4
  generateMySQLFetcher,
@@ -78,7 +78,11 @@ export const getDataSourceDependencies = (
78
78
  return dependencyMap[type]
79
79
  }
80
80
 
81
- export function generateDataSourceFetcher(dataSource: UIDLDataSource, tableName: string): string {
81
+ export function generateDataSourceFetcher(
82
+ dataSource: UIDLDataSource,
83
+ tableName: string,
84
+ categories?: UIDLEcommerceCategory[]
85
+ ): string {
82
86
  if (!dataSource || typeof dataSource !== 'object') {
83
87
  throw new Error('Invalid data source: data source must be a valid object')
84
88
  }
@@ -233,7 +237,7 @@ export function generateDataSourceFetcher(dataSource: UIDLDataSource, tableName:
233
237
  if (!validation.isValid) {
234
238
  throw new Error(`Teleport config validation failed: ${validation.error}`)
235
239
  }
236
- return generateTeleportFetcher(config, tableName)
240
+ return generateTeleportFetcher(config, tableName, categories)
237
241
  }
238
242
 
239
243
  default:
@@ -253,9 +257,10 @@ export function generateDataSourceFetcher(dataSource: UIDLDataSource, tableName:
253
257
  export function generateDataSourceFetcherWithCore(
254
258
  dataSource: UIDLDataSource,
255
259
  tableName: string,
256
- isApiRoute: boolean = false
260
+ isApiRoute: boolean = false,
261
+ categories?: UIDLEcommerceCategory[]
257
262
  ): string {
258
- const apiHandler = generateDataSourceFetcher(dataSource, tableName)
263
+ const apiHandler = generateDataSourceFetcher(dataSource, tableName, categories)
259
264
 
260
265
  // Extract the handler function body from the API route
261
266
  // The API route is structured as: export default async function handler(req, res) { ... }
@@ -75,6 +75,12 @@ const processFilters = (filters, conditions, queryParams, paramIndex) => {
75
75
 
76
76
  if (Array.isArray(value)) {
77
77
  if (value.length === 0) return
78
+ if (operand === 'array_overlap') {
79
+ conditions.push(\`jsonb_exists_any(NULLIF(\${field}, '')::jsonb, $\${paramIndex}::text[])\`)
80
+ queryParams.push(value.map((entry) => String(entry)))
81
+ paramIndex++
82
+ return
83
+ }
78
84
  const placeholders = value.map(() => \`$\${paramIndex++}\`)
79
85
  queryParams.push(...value)
80
86
  if (operand === '!=') {
@@ -83,6 +89,17 @@ const processFilters = (filters, conditions, queryParams, paramIndex) => {
83
89
  conditions.push(\`\${field} IN (\${placeholders.join(', ')})\`)
84
90
  }
85
91
  } else {
92
+ if (operand === 'array_overlap') {
93
+ if (value === '' || value === null || value === undefined) return
94
+ // A single comma-joined string (the multi-select Category Filter's
95
+ // ?categoryFilter=a,b,c) expands to multiple ids; one id stays one.
96
+ const overlapValues = String(value).split(',').map((entry) => entry.trim()).filter(Boolean)
97
+ if (overlapValues.length === 0) return
98
+ conditions.push(\`jsonb_exists_any(NULLIF(\${field}, '')::jsonb, $\${paramIndex}::text[])\`)
99
+ queryParams.push(overlapValues)
100
+ paramIndex++
101
+ return
102
+ }
86
103
  if (value === null) {
87
104
  if (operand === '=') {
88
105
  conditions.push(\`\${field} IS NULL\`)
@@ -1,3 +1,4 @@
1
+ import type { UIDLEcommerceCategory } from '@teleporthq/teleport-types'
1
2
  import {
2
3
  replaceSecretReference,
3
4
  generateDateFormatterCode,
@@ -50,7 +51,8 @@ export const validateTeleportConfig = (
50
51
 
51
52
  export const generateTeleportFetcher = (
52
53
  config: Record<string, unknown>,
53
- tableName: string
54
+ tableName: string,
55
+ categories?: UIDLEcommerceCategory[]
54
56
  ): string => {
55
57
  const dbConfig = config as TeleportDBConfig
56
58
  const schema = dbConfig.options?.schema
@@ -140,7 +142,7 @@ const getClient = () => {
140
142
  ${generateSafeJSONParseCode()}
141
143
 
142
144
  ${generateSearchEscapeHelpersCode()}
143
- ${getTransformationCode(tableName)}
145
+ ${getTransformationCode(tableName, categories)}
144
146
  ${getTransformWrapperCode(tableName)}
145
147
  const processFilters = (filters, conditions, queryParams, paramIndex) => {
146
148
  if (!filters) return paramIndex
@@ -157,6 +159,15 @@ const processFilters = (filters, conditions, queryParams, paramIndex) => {
157
159
 
158
160
  if (Array.isArray(value)) {
159
161
  if (value.length === 0) return
162
+ if (operand === 'array_overlap') {
163
+ // Row column is a JSON array (e.g. category_ids). Match when it
164
+ // shares any element with the destination set. jsonb ?| (function
165
+ // form) avoids the '?' placeholder-token ambiguity some drivers hit.
166
+ conditions.push(\`jsonb_exists_any(NULLIF(\${field}, '')::jsonb, $\${paramIndex}::text[])\`)
167
+ queryParams.push(value.map((entry) => String(entry)))
168
+ paramIndex++
169
+ return
170
+ }
160
171
  const placeholders = value.map(() => \`$\${paramIndex++}\`)
161
172
  queryParams.push(...value)
162
173
  if (operand === '!=') {
@@ -165,6 +176,17 @@ const processFilters = (filters, conditions, queryParams, paramIndex) => {
165
176
  conditions.push(\`\${field} IN (\${placeholders.join(', ')})\`)
166
177
  }
167
178
  } else {
179
+ if (operand === 'array_overlap') {
180
+ if (value === '' || value === null || value === undefined) return
181
+ // A single comma-joined string (the multi-select Category Filter's
182
+ // ?categoryFilter=a,b,c) expands to multiple ids; one id stays one.
183
+ const overlapValues = String(value).split(',').map((entry) => entry.trim()).filter(Boolean)
184
+ if (overlapValues.length === 0) return
185
+ conditions.push(\`jsonb_exists_any(NULLIF(\${field}, '')::jsonb, $\${paramIndex}::text[])\`)
186
+ queryParams.push(overlapValues)
187
+ paramIndex++
188
+ return
189
+ }
168
190
  if (value === null) {
169
191
  if (operand === '=') {
170
192
  conditions.push(\`\${field} IS NULL\`)
@@ -364,9 +386,22 @@ export default async function handler(req, res) {
364
386
  : ''
365
387
  }
366
388
 
389
+ // Stamp each row with a globally-correct 1-based \`__rowNumber\` (the page
390
+ // offset is applied server-side above), so list index columns keep counting
391
+ // across pages instead of restarting at 1. Renderer-internal field — it is
392
+ // filtered from ordinary field bindings and only read by the index column.
393
+ const __finalData = ${getTransformExpression(tableName) ? 'transformedData' : 'safeData'}
394
+ const __rowNumberStart = (typeof offsetValue === 'number' && !isNaN(offsetValue)) ? offsetValue : 0
395
+ const __numberedData = Array.isArray(__finalData)
396
+ ? __finalData.map((__row, __i) =>
397
+ __row && typeof __row === 'object' && !Array.isArray(__row)
398
+ ? Object.assign({}, __row, { __rowNumber: __rowNumberStart + __i + 1 })
399
+ : __row)
400
+ : __finalData
401
+
367
402
  return res.status(200).json({
368
403
  success: true,
369
- data: ${getTransformExpression(tableName) ? 'transformedData' : 'safeData'},
404
+ data: __numberedData,
370
405
  timestamp: Date.now()
371
406
  })
372
407
  } catch (error) {
@@ -12,27 +12,54 @@ const buildFilterEntry = (
12
12
  types.objectProperty(types.identifier('operand'), types.stringLiteral(operand)),
13
13
  ])
14
14
 
15
- // Predicate `(__f) => __f.destination !== '' && __f.destination !== null && __f.destination !== undefined`
15
+ // Predicate:
16
+ // (__f) => __f.destination !== '' && __f.destination !== null &&
17
+ // __f.destination !== undefined &&
18
+ // !(Array.isArray(__f.destination) && __f.destination.filter(Boolean).length === 0)
16
19
  // Used at runtime to drop filter conditions whose destination resolves to an
17
20
  // empty value. Keeps the page generic: state-bound filters (e.g.
18
21
  // `selectedCategory === ''` after picking "All Categories") and url-bound
19
22
  // filters (e.g. no `?categoryFilter=` in URL → `router?.query?.categoryFilter`
20
23
  // is `undefined`) both fall away cleanly instead of being sent to the API as
21
24
  // `destination: ''`, which the SQL backend interprets as a literal empty
22
- // string and returns zero rows.
25
+ // string and returns zero rows. The array clause covers array-overlap filters
26
+ // (e.g. category `CATEGORY_DESCENDANTS[selectedCategory] || []`) whose "All"
27
+ // reset resolves to `[]` — an empty overlap set must mean "no filter", not
28
+ // "match nothing".
23
29
  const buildNonEmptyDestinationPredicate = (): types.ArrowFunctionExpression => {
24
30
  const f = types.identifier('__f')
25
31
  const dest = types.memberExpression(f, types.identifier('destination'))
32
+ const isEmptyArray = types.logicalExpression(
33
+ '&&',
34
+ types.callExpression(
35
+ types.memberExpression(types.identifier('Array'), types.identifier('isArray')),
36
+ [dest]
37
+ ),
38
+ types.binaryExpression(
39
+ '===',
40
+ types.memberExpression(
41
+ types.callExpression(types.memberExpression(dest, types.identifier('filter')), [
42
+ types.identifier('Boolean'),
43
+ ]),
44
+ types.identifier('length')
45
+ ),
46
+ types.numericLiteral(0)
47
+ )
48
+ )
26
49
  return types.arrowFunctionExpression(
27
50
  [f],
28
51
  types.logicalExpression(
29
52
  '&&',
30
53
  types.logicalExpression(
31
54
  '&&',
32
- types.binaryExpression('!==', dest, types.stringLiteral('')),
33
- types.binaryExpression('!==', dest, types.nullLiteral())
55
+ types.logicalExpression(
56
+ '&&',
57
+ types.binaryExpression('!==', dest, types.stringLiteral('')),
58
+ types.binaryExpression('!==', dest, types.nullLiteral())
59
+ ),
60
+ types.binaryExpression('!==', dest, types.identifier('undefined'))
34
61
  ),
35
- types.binaryExpression('!==', dest, types.identifier('undefined'))
62
+ types.unaryExpression('!', isEmptyArray)
36
63
  )
37
64
  )
38
65
  }
package/src/index.ts CHANGED
@@ -606,7 +606,8 @@ export const createNextPagesDataSourcePlugin: ComponentPluginFactory<{}> = () =>
606
606
  options.extractedResources,
607
607
  dependencies,
608
608
  dynamicRouteAttr,
609
- uidl.outputOptions?.folderPath
609
+ uidl.outputOptions?.folderPath,
610
+ options.ecommerceSettings?.categories
610
611
  )
611
612
 
612
613
  if (result.success && result.chunk) {
@@ -643,7 +644,8 @@ export const createNextPagesDataSourcePlugin: ComponentPluginFactory<{}> = () =>
643
644
  dataSourceNode,
644
645
  dataSources,
645
646
  componentChunk,
646
- options.extractedResources
647
+ options.extractedResources,
648
+ options.ecommerceSettings?.categories
647
649
  )
648
650
  }
649
651
  })
@@ -1253,7 +1255,8 @@ export const createNextComponentDataSourcePlugin: ComponentPluginFactory<{}> = (
1253
1255
  dataSourceNode,
1254
1256
  dataSources,
1255
1257
  componentChunk,
1256
- options.extractedResources
1258
+ options.extractedResources,
1259
+ options.ecommerceSettings?.categories
1257
1260
  )
1258
1261
  })
1259
1262
 
@@ -1337,7 +1340,9 @@ export const createNextComponentDataSourcePlugin: ComponentPluginFactory<{}> = (
1337
1340
  try {
1338
1341
  const fetcherCode = generateDataSourceFetcherWithCore(
1339
1342
  dataSource,
1340
- firstDataSourceInfo.tableName
1343
+ firstDataSourceInfo.tableName,
1344
+ false,
1345
+ options.ecommerceSettings?.categories
1341
1346
  )
1342
1347
  options.extractedResources[`utils/${fileName}`] = {
1343
1348
  fileName,
@@ -3,6 +3,7 @@ import {
3
3
  ComponentPluginFactory,
4
4
  ChunkType,
5
5
  FileType,
6
+ UIDLEcommerceCategory,
6
7
  } from '@teleporthq/teleport-types'
7
8
  import * as types from '@babel/types'
8
9
  import { parseExpression } from '@babel/parser'
@@ -642,7 +643,6 @@ function getStateVarsForUsage(usage: DataSourceUsage): {
642
643
  combinedStateVar: string
643
644
  setCombinedStateVar: string
644
645
  skipDebounceRefVar: string
645
- skipCountFetchRefVar: string
646
646
  propsPrefix: string
647
647
  } {
648
648
  const idx = usage.index
@@ -659,7 +659,6 @@ function getStateVarsForUsage(usage: DataSourceUsage): {
659
659
  combinedStateVar: `ds_${idx}_state`,
660
660
  setCombinedStateVar: `setDs_${idx}_state`,
661
661
  skipDebounceRefVar: `ds_${idx}_skipDebounce`,
662
- skipCountFetchRefVar: `ds_${idx}_skipCountFetch`,
663
662
  propsPrefix: `${usage.dataSourceIdentifier}_ds_${idx}`,
664
663
  }
665
664
  }
@@ -843,19 +842,6 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
843
842
  ])
844
843
  )
845
844
 
846
- // Only add skipCountFetchRef for pages (where we have server-side count)
847
- // For components, we need to fetch count on mount
848
- if (isPage) {
849
- stateDeclarations.push(
850
- types.variableDeclaration('const', [
851
- types.variableDeclarator(
852
- types.identifier(vars.skipCountFetchRefVar),
853
- types.callExpression(types.identifier('useRef'), [types.booleanLiteral(true)])
854
- ),
855
- ])
856
- )
857
- }
858
-
859
845
  // maxPages state
860
846
  const maxPagesInit = isPage
861
847
  ? types.logicalExpression(
@@ -1049,30 +1035,14 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
1049
1035
  // Build the count fetch effect body
1050
1036
  const countFetchEffectBody: types.Statement[] = []
1051
1037
 
1052
- // Only add skip-on-mount check for pages (where we have server-side count)
1053
- if (isPage) {
1054
- countFetchEffectBody.push(
1055
- types.ifStatement(
1056
- types.memberExpression(
1057
- types.identifier(vars.skipCountFetchRefVar),
1058
- types.identifier('current')
1059
- ),
1060
- types.blockStatement([
1061
- types.expressionStatement(
1062
- types.assignmentExpression(
1063
- '=',
1064
- types.memberExpression(
1065
- types.identifier(vars.skipCountFetchRefVar),
1066
- types.identifier('current')
1067
- ),
1068
- types.booleanLiteral(false)
1069
- )
1070
- ),
1071
- types.returnStatement(),
1072
- ])
1073
- )
1074
- )
1075
- }
1038
+ // Refresh the count on mount (no skip-on-mount guard). Pages seed maxPages
1039
+ // from the build-time `getStaticProps` count, but that snapshot goes stale:
1040
+ // rows added AFTER the build (e.g. the normalized product_variants table,
1041
+ // which is seeded/grows past one page during store generation) are not
1042
+ // reflected, so a frozen maxPages of 0/1 leaves "Next" permanently disabled
1043
+ // on data-heavy tables. Re-fetching the live count on mount corrects
1044
+ // maxPages so pagination tracks the real row count. The effect still runs
1045
+ // when the search query or a filter changes (see deps below).
1076
1046
 
1077
1047
  // Add the fetch call
1078
1048
  countFetchEffectBody.push(
@@ -1568,7 +1538,12 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
1568
1538
  }
1569
1539
 
1570
1540
  // Create API route for all categories (including 'plain' for components)
1571
- ensureAPIRouteExists(options.extractedResources, usage, options.dataSources)
1541
+ ensureAPIRouteExists(
1542
+ options.extractedResources,
1543
+ usage,
1544
+ options.dataSources,
1545
+ options.ecommerceSettings?.categories
1546
+ )
1572
1547
  })
1573
1548
 
1574
1549
  // STEP 3.5: Handle DataProviders WITHOUT repeaters (data-source-item type)
@@ -2700,7 +2675,8 @@ function wirePaginationButtons(
2700
2675
  function ensureAPIRouteExists(
2701
2676
  extractedResources: any,
2702
2677
  usage: DataSourceUsage,
2703
- dataSources: Record<string, any>
2678
+ dataSources: Record<string, any>,
2679
+ categories?: UIDLEcommerceCategory[]
2704
2680
  ): void {
2705
2681
  // Generate file name for the API route
2706
2682
  const fileName = generateSafeFileName(
@@ -2718,7 +2694,9 @@ function ensureAPIRouteExists(
2718
2694
  // Generate the utils file with fetchData, fetchCount, handler, and getCount
2719
2695
  const fetcherCode = generateDataSourceFetcherWithCore(
2720
2696
  dataSource,
2721
- usage.resourceDefinition.tableName || 'data'
2697
+ usage.resourceDefinition.tableName || 'data',
2698
+ false,
2699
+ categories
2722
2700
  )
2723
2701
 
2724
2702
  extractedResources[`utils/${fileName}`] = {