@teleporthq/teleport-plugin-next-data-source 0.43.66 → 0.43.68
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.
- package/__tests__/array-mapper-cache-wiring.test.ts +11 -0
- package/__tests__/array-overlap-sql.test.ts +53 -0
- package/__tests__/blog-post-page-enrichment.test.ts +214 -0
- package/__tests__/browser-row-policy.test.ts +503 -0
- package/__tests__/data-source-cache-wrapping.test.ts +18 -2
- package/__tests__/data-source-request-locale.test.ts +313 -0
- package/__tests__/ecommerce-product-options.test.ts +206 -0
- package/__tests__/ecommerce-product-pages.test.ts +82 -0
- package/__tests__/ecommerce-product-pre-options.golden.json +505 -0
- package/__tests__/ecommerce-product-storefront-contract.test.ts +293 -0
- package/__tests__/ecommerce-product-subscriptions.test.ts +121 -0
- package/__tests__/fetchers.test.ts +2 -2
- package/__tests__/gated-data-provider.test.ts +88 -0
- package/__tests__/items-path.test.ts +150 -0
- package/__tests__/localized-columns.test.ts +217 -0
- package/__tests__/numbered-pagination.test.ts +15 -0
- package/__tests__/product-filter-fields.ts +230 -0
- package/__tests__/product-filter-sql.fixture.json +7 -0
- package/__tests__/product-options-storefront.fixture.json +2510 -0
- package/__tests__/product-page-url-fixtures.json +25 -0
- package/__tests__/product-price-sort.ts +4 -2
- package/__tests__/raw-rows-mode.test.ts +111 -0
- package/__tests__/refetch-loading-state.test.ts +23 -0
- package/__tests__/rest-api-response-cache.test.ts +434 -0
- package/__tests__/search-clear-button.test.ts +122 -0
- package/__tests__/table-read-route-protected-tables.test.ts +493 -0
- package/dist/cjs/array-overlap-sql.d.ts +21 -0
- package/dist/cjs/array-overlap-sql.d.ts.map +1 -0
- package/dist/cjs/array-overlap-sql.js +25 -0
- package/dist/cjs/array-overlap-sql.js.map +1 -0
- package/dist/cjs/content-localization.d.ts +37 -0
- package/dist/cjs/content-localization.d.ts.map +1 -0
- package/dist/cjs/content-localization.js +29 -0
- package/dist/cjs/content-localization.js.map +1 -0
- package/dist/cjs/count-effect.d.ts +11 -1
- package/dist/cjs/count-effect.d.ts.map +1 -1
- package/dist/cjs/count-effect.js +30 -2
- package/dist/cjs/count-effect.js.map +1 -1
- package/dist/cjs/data-source-fetchers.d.ts.map +1 -1
- package/dist/cjs/data-source-fetchers.js +10 -4
- package/dist/cjs/data-source-fetchers.js.map +1 -1
- package/dist/cjs/fetchers/airtable.d.ts.map +1 -1
- package/dist/cjs/fetchers/airtable.js +1 -1
- package/dist/cjs/fetchers/airtable.js.map +1 -1
- package/dist/cjs/fetchers/clickhouse.d.ts.map +1 -1
- package/dist/cjs/fetchers/clickhouse.js +1 -1
- package/dist/cjs/fetchers/clickhouse.js.map +1 -1
- package/dist/cjs/fetchers/firestore.d.ts.map +1 -1
- package/dist/cjs/fetchers/firestore.js +1 -1
- package/dist/cjs/fetchers/firestore.js.map +1 -1
- package/dist/cjs/fetchers/javascript.d.ts.map +1 -1
- package/dist/cjs/fetchers/javascript.js +1 -1
- package/dist/cjs/fetchers/javascript.js.map +1 -1
- package/dist/cjs/fetchers/mariadb.d.ts.map +1 -1
- package/dist/cjs/fetchers/mariadb.js +1 -1
- package/dist/cjs/fetchers/mariadb.js.map +1 -1
- package/dist/cjs/fetchers/mongodb.d.ts.map +1 -1
- package/dist/cjs/fetchers/mongodb.js +1 -1
- package/dist/cjs/fetchers/mongodb.js.map +1 -1
- package/dist/cjs/fetchers/mysql.d.ts.map +1 -1
- package/dist/cjs/fetchers/mysql.js +1 -1
- package/dist/cjs/fetchers/mysql.js.map +1 -1
- package/dist/cjs/fetchers/postgresql.d.ts +1 -1
- package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
- package/dist/cjs/fetchers/postgresql.js +9 -4
- package/dist/cjs/fetchers/postgresql.js.map +1 -1
- package/dist/cjs/fetchers/raw-query.d.ts +8 -1
- package/dist/cjs/fetchers/raw-query.d.ts.map +1 -1
- package/dist/cjs/fetchers/raw-query.js +42 -4
- package/dist/cjs/fetchers/raw-query.js.map +1 -1
- package/dist/cjs/fetchers/redshift.d.ts.map +1 -1
- package/dist/cjs/fetchers/redshift.js +1 -1
- package/dist/cjs/fetchers/redshift.js.map +1 -1
- package/dist/cjs/fetchers/rest-api.d.ts.map +1 -1
- package/dist/cjs/fetchers/rest-api.js +7 -2
- package/dist/cjs/fetchers/rest-api.js.map +1 -1
- package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
- package/dist/cjs/fetchers/teleport.js +16 -6
- package/dist/cjs/fetchers/teleport.js.map +1 -1
- package/dist/cjs/fetchers/turso.d.ts.map +1 -1
- package/dist/cjs/fetchers/turso.js +1 -1
- package/dist/cjs/fetchers/turso.js.map +1 -1
- package/dist/cjs/fetchers/utils/browser-row-policy.d.ts +43 -0
- package/dist/cjs/fetchers/utils/browser-row-policy.d.ts.map +1 -0
- package/dist/cjs/fetchers/utils/browser-row-policy.js +66 -0
- package/dist/cjs/fetchers/utils/browser-row-policy.js.map +1 -0
- package/dist/cjs/fetchers/utils/request-sql-guards.d.ts +8 -0
- package/dist/cjs/fetchers/utils/request-sql-guards.d.ts.map +1 -0
- package/dist/cjs/fetchers/utils/request-sql-guards.js +12 -0
- package/dist/cjs/fetchers/utils/request-sql-guards.js.map +1 -0
- package/dist/cjs/fetchers/utils/rest-api-response-cache.d.ts +40 -0
- package/dist/cjs/fetchers/utils/rest-api-response-cache.d.ts.map +1 -0
- package/dist/cjs/fetchers/utils/rest-api-response-cache.js +64 -0
- package/dist/cjs/fetchers/utils/rest-api-response-cache.js.map +1 -0
- package/dist/cjs/fetchers/utils/table-access-guard.d.ts +23 -0
- package/dist/cjs/fetchers/utils/table-access-guard.d.ts.map +1 -0
- package/dist/cjs/fetchers/utils/table-access-guard.js +34 -0
- package/dist/cjs/fetchers/utils/table-access-guard.js.map +1 -0
- package/dist/cjs/index.d.ts +5 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +60 -8
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/items-path.d.ts +10 -0
- package/dist/cjs/items-path.d.ts.map +1 -0
- package/dist/cjs/items-path.js +86 -0
- package/dist/cjs/items-path.js.map +1 -0
- package/dist/cjs/loading-state.d.ts +16 -1
- package/dist/cjs/loading-state.d.ts.map +1 -1
- package/dist/cjs/loading-state.js +69 -3
- package/dist/cjs/loading-state.js.map +1 -1
- package/dist/cjs/localized-columns.d.ts +56 -0
- package/dist/cjs/localized-columns.d.ts.map +1 -0
- package/dist/cjs/localized-columns.js +137 -0
- package/dist/cjs/localized-columns.js.map +1 -0
- package/dist/cjs/pagination-plugin.d.ts.map +1 -1
- package/dist/cjs/pagination-plugin.js +140 -50
- package/dist/cjs/pagination-plugin.js.map +1 -1
- package/dist/cjs/product-filter-fields.d.ts +87 -0
- package/dist/cjs/product-filter-fields.d.ts.map +1 -0
- package/dist/cjs/product-filter-fields.js +161 -0
- package/dist/cjs/product-filter-fields.js.map +1 -0
- package/dist/cjs/request-locale.d.ts +49 -0
- package/dist/cjs/request-locale.d.ts.map +1 -0
- package/dist/cjs/request-locale.js +130 -0
- package/dist/cjs/request-locale.js.map +1 -0
- package/dist/cjs/search-clear.d.ts +31 -0
- package/dist/cjs/search-clear.d.ts.map +1 -0
- package/dist/cjs/search-clear.js +103 -0
- package/dist/cjs/search-clear.js.map +1 -0
- package/dist/cjs/transformations/blog-adjacent-posts.d.ts +16 -0
- package/dist/cjs/transformations/blog-adjacent-posts.d.ts.map +1 -0
- package/dist/cjs/transformations/blog-adjacent-posts.js +25 -0
- package/dist/cjs/transformations/blog-adjacent-posts.js.map +1 -0
- package/dist/cjs/transformations/blog-comments.d.ts +30 -0
- package/dist/cjs/transformations/blog-comments.d.ts.map +1 -0
- package/dist/cjs/transformations/blog-comments.js +34 -0
- package/dist/cjs/transformations/blog-comments.js.map +1 -0
- package/dist/cjs/transformations/blog-post.d.ts +2 -0
- package/dist/cjs/transformations/blog-post.d.ts.map +1 -1
- package/dist/cjs/transformations/blog-post.js +4 -1
- package/dist/cjs/transformations/blog-post.js.map +1 -1
- package/dist/cjs/transformations/ecommerce-product.d.ts +9 -1
- package/dist/cjs/transformations/ecommerce-product.d.ts.map +1 -1
- package/dist/cjs/transformations/ecommerce-product.js +28 -1
- package/dist/cjs/transformations/ecommerce-product.js.map +1 -1
- package/dist/cjs/transformations/heading-anchors.d.ts +25 -0
- package/dist/cjs/transformations/heading-anchors.d.ts.map +1 -0
- package/dist/cjs/transformations/heading-anchors.js +29 -0
- package/dist/cjs/transformations/heading-anchors.js.map +1 -0
- package/dist/cjs/transformations/index.d.ts +61 -4
- package/dist/cjs/transformations/index.d.ts.map +1 -1
- package/dist/cjs/transformations/index.js +69 -19
- package/dist/cjs/transformations/index.js.map +1 -1
- package/dist/cjs/transformations/shared-utils.d.ts.map +1 -1
- package/dist/cjs/transformations/shared-utils.js +1 -1
- package/dist/cjs/transformations/shared-utils.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/utils.d.ts +11 -1
- package/dist/cjs/utils.d.ts.map +1 -1
- package/dist/cjs/utils.js +60 -16
- package/dist/cjs/utils.js.map +1 -1
- package/dist/esm/array-overlap-sql.d.ts +21 -0
- package/dist/esm/array-overlap-sql.d.ts.map +1 -0
- package/dist/esm/array-overlap-sql.js +21 -0
- package/dist/esm/array-overlap-sql.js.map +1 -0
- package/dist/esm/content-localization.d.ts +37 -0
- package/dist/esm/content-localization.d.ts.map +1 -0
- package/dist/esm/content-localization.js +24 -0
- package/dist/esm/content-localization.js.map +1 -0
- package/dist/esm/count-effect.d.ts +11 -1
- package/dist/esm/count-effect.d.ts.map +1 -1
- package/dist/esm/count-effect.js +28 -1
- package/dist/esm/count-effect.js.map +1 -1
- package/dist/esm/data-source-fetchers.d.ts.map +1 -1
- package/dist/esm/data-source-fetchers.js +11 -5
- package/dist/esm/data-source-fetchers.js.map +1 -1
- package/dist/esm/fetchers/airtable.d.ts.map +1 -1
- package/dist/esm/fetchers/airtable.js +1 -1
- package/dist/esm/fetchers/airtable.js.map +1 -1
- package/dist/esm/fetchers/clickhouse.d.ts.map +1 -1
- package/dist/esm/fetchers/clickhouse.js +1 -1
- package/dist/esm/fetchers/clickhouse.js.map +1 -1
- package/dist/esm/fetchers/firestore.d.ts.map +1 -1
- package/dist/esm/fetchers/firestore.js +1 -1
- package/dist/esm/fetchers/firestore.js.map +1 -1
- package/dist/esm/fetchers/javascript.d.ts.map +1 -1
- package/dist/esm/fetchers/javascript.js +1 -1
- package/dist/esm/fetchers/javascript.js.map +1 -1
- package/dist/esm/fetchers/mariadb.d.ts.map +1 -1
- package/dist/esm/fetchers/mariadb.js +1 -1
- package/dist/esm/fetchers/mariadb.js.map +1 -1
- package/dist/esm/fetchers/mongodb.d.ts.map +1 -1
- package/dist/esm/fetchers/mongodb.js +1 -1
- package/dist/esm/fetchers/mongodb.js.map +1 -1
- package/dist/esm/fetchers/mysql.d.ts.map +1 -1
- package/dist/esm/fetchers/mysql.js +1 -1
- package/dist/esm/fetchers/mysql.js.map +1 -1
- package/dist/esm/fetchers/postgresql.d.ts +1 -1
- package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
- package/dist/esm/fetchers/postgresql.js +9 -4
- package/dist/esm/fetchers/postgresql.js.map +1 -1
- package/dist/esm/fetchers/raw-query.d.ts +8 -1
- package/dist/esm/fetchers/raw-query.d.ts.map +1 -1
- package/dist/esm/fetchers/raw-query.js +42 -4
- package/dist/esm/fetchers/raw-query.js.map +1 -1
- package/dist/esm/fetchers/redshift.d.ts.map +1 -1
- package/dist/esm/fetchers/redshift.js +1 -1
- package/dist/esm/fetchers/redshift.js.map +1 -1
- package/dist/esm/fetchers/rest-api.d.ts.map +1 -1
- package/dist/esm/fetchers/rest-api.js +7 -2
- package/dist/esm/fetchers/rest-api.js.map +1 -1
- package/dist/esm/fetchers/teleport.d.ts.map +1 -1
- package/dist/esm/fetchers/teleport.js +16 -6
- package/dist/esm/fetchers/teleport.js.map +1 -1
- package/dist/esm/fetchers/turso.d.ts.map +1 -1
- package/dist/esm/fetchers/turso.js +1 -1
- package/dist/esm/fetchers/turso.js.map +1 -1
- package/dist/esm/fetchers/utils/browser-row-policy.d.ts +43 -0
- package/dist/esm/fetchers/utils/browser-row-policy.d.ts.map +1 -0
- package/dist/esm/fetchers/utils/browser-row-policy.js +60 -0
- package/dist/esm/fetchers/utils/browser-row-policy.js.map +1 -0
- package/dist/esm/fetchers/utils/request-sql-guards.d.ts +8 -0
- package/dist/esm/fetchers/utils/request-sql-guards.d.ts.map +1 -0
- package/dist/esm/fetchers/utils/request-sql-guards.js +8 -0
- package/dist/esm/fetchers/utils/request-sql-guards.js.map +1 -0
- package/dist/esm/fetchers/utils/rest-api-response-cache.d.ts +40 -0
- package/dist/esm/fetchers/utils/rest-api-response-cache.d.ts.map +1 -0
- package/dist/esm/fetchers/utils/rest-api-response-cache.js +59 -0
- package/dist/esm/fetchers/utils/rest-api-response-cache.js.map +1 -0
- package/dist/esm/fetchers/utils/table-access-guard.d.ts +23 -0
- package/dist/esm/fetchers/utils/table-access-guard.d.ts.map +1 -0
- package/dist/esm/fetchers/utils/table-access-guard.js +29 -0
- package/dist/esm/fetchers/utils/table-access-guard.js.map +1 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +55 -7
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/items-path.d.ts +10 -0
- package/dist/esm/items-path.d.ts.map +1 -0
- package/dist/esm/items-path.js +58 -0
- package/dist/esm/items-path.js.map +1 -0
- package/dist/esm/loading-state.d.ts +16 -1
- package/dist/esm/loading-state.d.ts.map +1 -1
- package/dist/esm/loading-state.js +69 -3
- package/dist/esm/loading-state.js.map +1 -1
- package/dist/esm/localized-columns.d.ts +56 -0
- package/dist/esm/localized-columns.d.ts.map +1 -0
- package/dist/esm/localized-columns.js +132 -0
- package/dist/esm/localized-columns.js.map +1 -0
- package/dist/esm/pagination-plugin.d.ts.map +1 -1
- package/dist/esm/pagination-plugin.js +141 -51
- package/dist/esm/pagination-plugin.js.map +1 -1
- package/dist/esm/product-filter-fields.d.ts +87 -0
- package/dist/esm/product-filter-fields.d.ts.map +1 -0
- package/dist/esm/product-filter-fields.js +154 -0
- package/dist/esm/product-filter-fields.js.map +1 -0
- package/dist/esm/request-locale.d.ts +49 -0
- package/dist/esm/request-locale.d.ts.map +1 -0
- package/dist/esm/request-locale.js +98 -0
- package/dist/esm/request-locale.js.map +1 -0
- package/dist/esm/search-clear.d.ts +31 -0
- package/dist/esm/search-clear.d.ts.map +1 -0
- package/dist/esm/search-clear.js +74 -0
- package/dist/esm/search-clear.js.map +1 -0
- package/dist/esm/transformations/blog-adjacent-posts.d.ts +16 -0
- package/dist/esm/transformations/blog-adjacent-posts.d.ts.map +1 -0
- package/dist/esm/transformations/blog-adjacent-posts.js +21 -0
- package/dist/esm/transformations/blog-adjacent-posts.js.map +1 -0
- package/dist/esm/transformations/blog-comments.d.ts +30 -0
- package/dist/esm/transformations/blog-comments.d.ts.map +1 -0
- package/dist/esm/transformations/blog-comments.js +30 -0
- package/dist/esm/transformations/blog-comments.js.map +1 -0
- package/dist/esm/transformations/blog-post.d.ts +2 -0
- package/dist/esm/transformations/blog-post.d.ts.map +1 -1
- package/dist/esm/transformations/blog-post.js +4 -1
- package/dist/esm/transformations/blog-post.js.map +1 -1
- package/dist/esm/transformations/ecommerce-product.d.ts +9 -1
- package/dist/esm/transformations/ecommerce-product.d.ts.map +1 -1
- package/dist/esm/transformations/ecommerce-product.js +29 -2
- package/dist/esm/transformations/ecommerce-product.js.map +1 -1
- package/dist/esm/transformations/heading-anchors.d.ts +25 -0
- package/dist/esm/transformations/heading-anchors.d.ts.map +1 -0
- package/dist/esm/transformations/heading-anchors.js +25 -0
- package/dist/esm/transformations/heading-anchors.js.map +1 -0
- package/dist/esm/transformations/index.d.ts +61 -4
- package/dist/esm/transformations/index.d.ts.map +1 -1
- package/dist/esm/transformations/index.js +69 -19
- package/dist/esm/transformations/index.js.map +1 -1
- package/dist/esm/transformations/shared-utils.d.ts.map +1 -1
- package/dist/esm/transformations/shared-utils.js +1 -1
- package/dist/esm/transformations/shared-utils.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/utils.d.ts +11 -1
- package/dist/esm/utils.d.ts.map +1 -1
- package/dist/esm/utils.js +60 -16
- package/dist/esm/utils.js.map +1 -1
- package/package.json +5 -5
- package/src/array-overlap-sql.ts +41 -0
- package/src/content-localization.ts +53 -0
- package/src/count-effect.ts +53 -2
- package/src/data-source-fetchers.ts +41 -5
- package/src/fetchers/airtable.ts +4 -2
- package/src/fetchers/clickhouse.ts +4 -2
- package/src/fetchers/firestore.ts +4 -2
- package/src/fetchers/javascript.ts +9 -1
- package/src/fetchers/mariadb.ts +4 -2
- package/src/fetchers/mongodb.ts +4 -2
- package/src/fetchers/mysql.ts +4 -2
- package/src/fetchers/postgresql.ts +53 -12
- package/src/fetchers/raw-query.ts +60 -5
- package/src/fetchers/redshift.ts +4 -2
- package/src/fetchers/rest-api.ts +27 -5
- package/src/fetchers/teleport.ts +85 -22
- package/src/fetchers/turso.ts +4 -2
- package/src/fetchers/utils/browser-row-policy.ts +212 -0
- package/src/fetchers/utils/request-sql-guards.ts +25 -0
- package/src/fetchers/utils/rest-api-response-cache.ts +188 -0
- package/src/fetchers/utils/table-access-guard.ts +39 -0
- package/src/index.ts +79 -11
- package/src/items-path.ts +74 -0
- package/src/loading-state.ts +134 -3
- package/src/localized-columns.ts +146 -0
- package/src/pagination-plugin.ts +206 -60
- package/src/product-filter-fields.ts +173 -0
- package/src/request-locale.ts +113 -0
- package/src/search-clear.ts +102 -0
- package/src/transformations/blog-adjacent-posts.ts +74 -0
- package/src/transformations/blog-comments.ts +132 -0
- package/src/transformations/blog-post.ts +38 -5
- package/src/transformations/ecommerce-product.ts +568 -5
- package/src/transformations/heading-anchors.ts +85 -0
- package/src/transformations/index.ts +191 -23
- package/src/transformations/shared-utils.ts +6 -0
- package/src/utils.ts +109 -19
|
@@ -145,3 +145,296 @@ describe('ecommerce product transform — storefront picker contract', () => {
|
|
|
145
145
|
expect(Array.isArray(productWithVariants().visibleProperties)).toBe(true)
|
|
146
146
|
})
|
|
147
147
|
})
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Every field the generated product page reads to say what buying a product
|
|
151
|
+
* MEANS — the "Purchase details" block and the price-row pills. Strings where
|
|
152
|
+
* a rendering condition gates on them; the block is hidden by absence, so a
|
|
153
|
+
* plain one-time product must still carry every key.
|
|
154
|
+
*/
|
|
155
|
+
const PURCHASE_DETAILS_CONTRACT = [
|
|
156
|
+
'billingPeriod',
|
|
157
|
+
'billingSummary',
|
|
158
|
+
'trialDays',
|
|
159
|
+
'recurringIntervalCount',
|
|
160
|
+
'isDigital',
|
|
161
|
+
'isGiftCard',
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
const subscriptionPlan = (over: Record<string, unknown>) =>
|
|
165
|
+
buildEcommerceProduct(
|
|
166
|
+
{
|
|
167
|
+
id: 'p3',
|
|
168
|
+
name: 'Plan',
|
|
169
|
+
slug: 'plan',
|
|
170
|
+
price: '12',
|
|
171
|
+
currency: 'USD',
|
|
172
|
+
payment_type: 'recurring',
|
|
173
|
+
recurring_interval: 'month',
|
|
174
|
+
...over,
|
|
175
|
+
},
|
|
176
|
+
{ variantsByProductId: {} }
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
describe('ecommerce product transform — storefront purchase-details contract', () => {
|
|
180
|
+
it.each(PURCHASE_DETAILS_CONTRACT)('always emits %s, for a product WITH variants', (field) => {
|
|
181
|
+
expect(productWithVariants()).toHaveProperty(field)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it.each(PURCHASE_DETAILS_CONTRACT)('always emits %s, for a product WITHOUT variants', (field) => {
|
|
185
|
+
expect(productWithoutVariants()).toHaveProperty(field)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('says nothing about billing, a download or a gift card on a plain one-time product', () => {
|
|
189
|
+
expect(productWithoutVariants()).toMatchObject({
|
|
190
|
+
billingSummary: '',
|
|
191
|
+
trialDays: 0,
|
|
192
|
+
isDigital: 'false',
|
|
193
|
+
isGiftCard: 'false',
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('prints the billing sentence of a subscription, with its free trial when it has one', () => {
|
|
198
|
+
expect(subscriptionPlan({ recurring_interval_count: '3', trial_days: '14' })).toMatchObject({
|
|
199
|
+
billingPeriod: '3 months',
|
|
200
|
+
billingSummary: 'Billed every 3 months · 14-day free trial',
|
|
201
|
+
trialDays: 14,
|
|
202
|
+
recurringIntervalCount: 3,
|
|
203
|
+
})
|
|
204
|
+
// NULL / zero columns read as "every month, no trial" — and the count the
|
|
205
|
+
// schedule is built from is never below 1 on a subscription.
|
|
206
|
+
for (const over of [
|
|
207
|
+
{ recurring_interval_count: null, trial_days: null },
|
|
208
|
+
{ recurring_interval_count: 0, trial_days: 0 },
|
|
209
|
+
{ recurring_interval_count: 'x', trial_days: -3 },
|
|
210
|
+
]) {
|
|
211
|
+
expect(subscriptionPlan(over)).toMatchObject({
|
|
212
|
+
billingPeriod: 'month',
|
|
213
|
+
billingSummary: 'Billed every month',
|
|
214
|
+
trialDays: 0,
|
|
215
|
+
recurringIntervalCount: 1,
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('gates the gift-card pill with the STRING "true"/"false", however the column is spelled', () => {
|
|
221
|
+
for (const raw of [true, 't', 'true', 1, '1']) {
|
|
222
|
+
expect(
|
|
223
|
+
buildEcommerceProduct({ id: 'g', name: 'G', price: '5', is_gift_card: raw }).isGiftCard
|
|
224
|
+
).toBe('true')
|
|
225
|
+
}
|
|
226
|
+
for (const raw of [false, 'f', 'false', 0, '0', null, undefined, '']) {
|
|
227
|
+
expect(
|
|
228
|
+
buildEcommerceProduct({ id: 'g', name: 'G', price: '5', is_gift_card: raw }).isGiftCard
|
|
229
|
+
).toBe('false')
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Every field the generated product page and cards read to draw product
|
|
236
|
+
* options: the configurator, the configured price, the card's quick-add gate
|
|
237
|
+
* and the buy button's attributes. A listing and a product without options
|
|
238
|
+
* carry the EMPTY values — a binding to an absent field renders undefined, and
|
|
239
|
+
* the configurator's gates would read that as a configured product.
|
|
240
|
+
*/
|
|
241
|
+
const OPTIONS_CONTRACT = [
|
|
242
|
+
'hasProductOptions',
|
|
243
|
+
'optionsRequireInput',
|
|
244
|
+
'optionsQuickAdd',
|
|
245
|
+
'optionGroups',
|
|
246
|
+
'optionGroupsJson',
|
|
247
|
+
'optionsPricingJson',
|
|
248
|
+
'optionsDefaultDisplayPrice',
|
|
249
|
+
'optionsDefaultOriginalDisplayPrice',
|
|
250
|
+
'optionsDefaultSummary',
|
|
251
|
+
'optionsFreshKey',
|
|
252
|
+
'optionsValidKey',
|
|
253
|
+
'optionsOriginalKey',
|
|
254
|
+
'optionsSummaryKey',
|
|
255
|
+
'optionsFilesKey',
|
|
256
|
+
]
|
|
257
|
+
|
|
258
|
+
const OPTION_GROUP_CONTRACT = [
|
|
259
|
+
'id',
|
|
260
|
+
'key',
|
|
261
|
+
'label',
|
|
262
|
+
'helpText',
|
|
263
|
+
'placeholder',
|
|
264
|
+
'unit',
|
|
265
|
+
'required',
|
|
266
|
+
'isChoice',
|
|
267
|
+
'isMulti',
|
|
268
|
+
'isText',
|
|
269
|
+
'isLongText',
|
|
270
|
+
'isNumber',
|
|
271
|
+
'isToggle',
|
|
272
|
+
'isUpload',
|
|
273
|
+
'isButtons',
|
|
274
|
+
'isSwatches',
|
|
275
|
+
'isCards',
|
|
276
|
+
'isSelectable',
|
|
277
|
+
'isInput',
|
|
278
|
+
'initiallyVisible',
|
|
279
|
+
'cellRole',
|
|
280
|
+
'rowRole',
|
|
281
|
+
'rowRequired',
|
|
282
|
+
'idleTabIndex',
|
|
283
|
+
'inputType',
|
|
284
|
+
'inputMode',
|
|
285
|
+
'maxLengthText',
|
|
286
|
+
'minText',
|
|
287
|
+
'maxText',
|
|
288
|
+
'stepText',
|
|
289
|
+
'defaultValueText',
|
|
290
|
+
'rangeHint',
|
|
291
|
+
'acceptKind',
|
|
292
|
+
'acceptMime',
|
|
293
|
+
'priceVariants',
|
|
294
|
+
'headerPriceVariants',
|
|
295
|
+
'hiddenKey',
|
|
296
|
+
'shownKey',
|
|
297
|
+
'missingKey',
|
|
298
|
+
'invalidKey',
|
|
299
|
+
'rejectedKey',
|
|
300
|
+
'failedKey',
|
|
301
|
+
'uploadedKey',
|
|
302
|
+
'uploadingKey',
|
|
303
|
+
'freshKey',
|
|
304
|
+
'values',
|
|
305
|
+
]
|
|
306
|
+
|
|
307
|
+
const OPTION_VALUE_CONTRACT = [
|
|
308
|
+
'id',
|
|
309
|
+
'key',
|
|
310
|
+
'label',
|
|
311
|
+
'description',
|
|
312
|
+
'color',
|
|
313
|
+
'imageUrl',
|
|
314
|
+
'isDefault',
|
|
315
|
+
'showSwatch',
|
|
316
|
+
'showImage',
|
|
317
|
+
'selectedKey',
|
|
318
|
+
'freshKey',
|
|
319
|
+
'priceVariants',
|
|
320
|
+
]
|
|
321
|
+
|
|
322
|
+
const OPTION_GROUPS = [
|
|
323
|
+
{
|
|
324
|
+
key: 'size',
|
|
325
|
+
label: 'Size',
|
|
326
|
+
type: 'choice',
|
|
327
|
+
required: true,
|
|
328
|
+
values: [
|
|
329
|
+
{ key: 'a5', label: 'A5', price: { mode: 'none' }, default: true },
|
|
330
|
+
{ key: 'a3', label: 'A3', price: { mode: 'fixed', amount: 9 } },
|
|
331
|
+
],
|
|
332
|
+
},
|
|
333
|
+
{ key: 'gift-wrap', label: 'Gift wrap', type: 'toggle', price: { mode: 'fixed', amount: 2 } },
|
|
334
|
+
{ key: 'engraving', label: 'Engraving', type: 'text', maxLength: 20 },
|
|
335
|
+
]
|
|
336
|
+
|
|
337
|
+
const configurableProduct = (includeOptionDetail: boolean) =>
|
|
338
|
+
buildEcommerceProduct(
|
|
339
|
+
{
|
|
340
|
+
id: 'p4',
|
|
341
|
+
name: 'Lighter',
|
|
342
|
+
slug: 'lighter',
|
|
343
|
+
price: '30',
|
|
344
|
+
currency: 'USD',
|
|
345
|
+
option_groups: JSON.stringify(OPTION_GROUPS),
|
|
346
|
+
},
|
|
347
|
+
{ variantsByProductId: {}, includeOptionDetail }
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
describe('ecommerce product transform — storefront product-options contract', () => {
|
|
351
|
+
it.each(OPTIONS_CONTRACT)('always emits %s, for a product WITH variants', (field) => {
|
|
352
|
+
expect(productWithVariants()).toHaveProperty(field)
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it.each(OPTIONS_CONTRACT)('always emits %s, for a product WITHOUT variants', (field) => {
|
|
356
|
+
expect(productWithoutVariants()).toHaveProperty(field)
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
it.each(OPTIONS_CONTRACT)('always emits %s, for a configurable product in a listing', (field) => {
|
|
360
|
+
expect(configurableProduct(false)).toHaveProperty(field)
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
it('gates with the STRING flags, never booleans', () => {
|
|
364
|
+
expect(productWithoutVariants()).toMatchObject({
|
|
365
|
+
hasProductOptions: 'false',
|
|
366
|
+
optionsRequireInput: 'false',
|
|
367
|
+
optionsQuickAdd: 'true',
|
|
368
|
+
})
|
|
369
|
+
expect(configurableProduct(false)).toMatchObject({
|
|
370
|
+
hasProductOptions: 'true',
|
|
371
|
+
optionsRequireInput: 'false',
|
|
372
|
+
optionsQuickAdd: 'true',
|
|
373
|
+
})
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it('enriches every group and value with the fields the configurator binds', () => {
|
|
377
|
+
const groups = configurableProduct(true).optionGroups as Array<Record<string, unknown>>
|
|
378
|
+
expect(groups).toHaveLength(3)
|
|
379
|
+
for (const group of groups) {
|
|
380
|
+
for (const field of OPTION_GROUP_CONTRACT) {
|
|
381
|
+
expect(group).toHaveProperty(field)
|
|
382
|
+
}
|
|
383
|
+
for (const value of group.values as Array<Record<string, unknown>>) {
|
|
384
|
+
for (const field of OPTION_VALUE_CONTRACT) {
|
|
385
|
+
expect(value).toHaveProperty(field)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
it('keys every repeater row by the product, so typed text never follows the shopper', () => {
|
|
392
|
+
const [size, wrap] = configurableProduct(true).optionGroups as Array<{
|
|
393
|
+
id: string
|
|
394
|
+
values: Array<{ id: string }>
|
|
395
|
+
}>
|
|
396
|
+
expect(size.id).toBe('p4|size')
|
|
397
|
+
expect(size.values.map((value) => value.id)).toEqual(['p4|size|a5', 'p4|size|a3'])
|
|
398
|
+
expect(wrap.values.map((value) => value.id)).toEqual(['p4|gift-wrap|on'])
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
it('ships the JSON companions the configurator workflows parse', () => {
|
|
402
|
+
const product = configurableProduct(true)
|
|
403
|
+
expect(JSON.parse(product.optionGroupsJson as string)).toHaveLength(3)
|
|
404
|
+
expect(JSON.parse(product.optionsPricingJson as string)).toMatchObject({
|
|
405
|
+
productId: 'p4',
|
|
406
|
+
netBase: 30,
|
|
407
|
+
pricesDisabled: false,
|
|
408
|
+
})
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
// ARIA allows aria-required on a radiogroup, not on a group: a required
|
|
412
|
+
// pick-several row carrying it fails axe and Lighthouse (aria-allowed-attr).
|
|
413
|
+
it('lets only a pick-one row say it is required, with null (never undefined) elsewhere', () => {
|
|
414
|
+
const extras = {
|
|
415
|
+
key: 'extras',
|
|
416
|
+
label: 'Extras',
|
|
417
|
+
type: 'multi-choice',
|
|
418
|
+
required: true,
|
|
419
|
+
values: [{ key: 'ribbon', label: 'Ribbon', price: { mode: 'none' } }],
|
|
420
|
+
}
|
|
421
|
+
const product = buildEcommerceProduct(
|
|
422
|
+
{
|
|
423
|
+
id: 'p5',
|
|
424
|
+
name: 'Hamper',
|
|
425
|
+
slug: 'hamper',
|
|
426
|
+
price: '40',
|
|
427
|
+
currency: 'USD',
|
|
428
|
+
option_groups: JSON.stringify([...OPTION_GROUPS, extras]),
|
|
429
|
+
},
|
|
430
|
+
{ variantsByProductId: {}, includeOptionDetail: true }
|
|
431
|
+
)
|
|
432
|
+
const groups = product.optionGroups as Array<Record<string, unknown>>
|
|
433
|
+
expect(groups.map((group) => [group.key, group.rowRole, group.rowRequired])).toEqual([
|
|
434
|
+
['size', 'radiogroup', 'true'],
|
|
435
|
+
['gift-wrap', 'group', null],
|
|
436
|
+
['engraving', 'group', null],
|
|
437
|
+
['extras', 'group', null],
|
|
438
|
+
])
|
|
439
|
+
})
|
|
440
|
+
})
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/* tslint:disable:function-constructor */
|
|
2
|
+
import { generateEcommerceProductTransformationCode } from '../src/transformations/ecommerce-product'
|
|
3
|
+
import { generateSharedTransformationCode } from '../src/transformations/shared-utils'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What a product card says about a subscription and a download: the period a
|
|
7
|
+
* recurring price is followed by (`billingPeriod`, "/ month") and the
|
|
8
|
+
* `isDigital` gate of the "Instant download" pill. Both are STRINGS the
|
|
9
|
+
* builders bind and gate on, and both exist in two copies — this generated
|
|
10
|
+
* transform and the editor's canvas transform.
|
|
11
|
+
*
|
|
12
|
+
* ⚠️ `PERIOD_FIXTURES` IS SHARED. It is byte-identical to the table in
|
|
13
|
+
* teleport-gui's `features/e-commerce/utils/__tests__/recurring-summary-parity.spec.ts`,
|
|
14
|
+
* which runs it against `formatBillingPeriod`. Two specs, one table.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const buildProduct = (): ((record: unknown, options?: unknown) => Record<string, unknown>) => {
|
|
18
|
+
const code =
|
|
19
|
+
generateSharedTransformationCode() + '\n' + generateEcommerceProductTransformationCode({})
|
|
20
|
+
return new Function(code + '\nreturn buildEcommerceProduct;')() as (
|
|
21
|
+
record: unknown,
|
|
22
|
+
options?: unknown
|
|
23
|
+
) => Record<string, unknown>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** SHARED PARITY TABLE — keep byte-identical with the GUI spec. */
|
|
27
|
+
const PERIOD_FIXTURES: Array<{ interval: unknown; count: unknown; period: string }> = [
|
|
28
|
+
{ interval: 'month', count: 1, period: 'month' },
|
|
29
|
+
{ interval: 'month', count: 3, period: '3 months' },
|
|
30
|
+
{ interval: 'year', count: 1, period: 'year' },
|
|
31
|
+
{ interval: 'week', count: 2, period: '2 weeks' },
|
|
32
|
+
{ interval: ' DAY ', count: '7', period: '7 days' },
|
|
33
|
+
{ interval: 'fortnight', count: 0, period: 'month' },
|
|
34
|
+
{ interval: null, count: null, period: 'month' },
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
const BASE = { id: 'p1', name: 'P', slug: 'p', price: '10.00', currency: 'USD' }
|
|
38
|
+
|
|
39
|
+
describe('ecommerce product transform — subscriptions and digital delivery', () => {
|
|
40
|
+
const build = buildProduct()
|
|
41
|
+
|
|
42
|
+
it.each(PERIOD_FIXTURES)(
|
|
43
|
+
'follows a recurring price billed every $interval × $count with "$period"',
|
|
44
|
+
({ interval, count, period }) => {
|
|
45
|
+
const product = build({
|
|
46
|
+
...BASE,
|
|
47
|
+
payment_type: 'recurring',
|
|
48
|
+
recurring_interval: interval,
|
|
49
|
+
recurring_interval_count: count,
|
|
50
|
+
})
|
|
51
|
+
expect(product.billingPeriod).toBe(period)
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
it('follows a one-time price with nothing, whatever the interval columns hold', () => {
|
|
56
|
+
expect(build({ ...BASE, recurring_interval: 'month' }).billingPeriod).toBe('')
|
|
57
|
+
expect(
|
|
58
|
+
build({ ...BASE, payment_type: 'one_time', recurring_interval: 'year' }).billingPeriod
|
|
59
|
+
).toBe('')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('reads the digital flag as every backend spells it, and NULL as physical', () => {
|
|
63
|
+
for (const raw of [true, 't', 'true', 1, '1', 'YES']) {
|
|
64
|
+
expect(build({ ...BASE, is_digital: raw }).isDigital).toBe('true')
|
|
65
|
+
}
|
|
66
|
+
for (const raw of [false, 'f', 'false', 0, '0', null, undefined, '']) {
|
|
67
|
+
expect(build({ ...BASE, is_digital: raw }).isDigital).toBe('false')
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* SHARED PARITY TABLE — keep byte-identical with the GUI's
|
|
74
|
+
* `packages/renderer/src/utils/__tests__/ecommerce-products-purchase-details.test.ts`,
|
|
75
|
+
* which runs it against the canvas transform.
|
|
76
|
+
*/
|
|
77
|
+
const BILLING_SUMMARY_FIXTURES: Array<{
|
|
78
|
+
interval: unknown
|
|
79
|
+
count: unknown
|
|
80
|
+
trialDays: unknown
|
|
81
|
+
summary: string
|
|
82
|
+
}> = [
|
|
83
|
+
{ interval: 'month', count: 1, trialDays: null, summary: 'Billed every month' },
|
|
84
|
+
{
|
|
85
|
+
interval: 'month',
|
|
86
|
+
count: 3,
|
|
87
|
+
trialDays: 14,
|
|
88
|
+
summary: 'Billed every 3 months · 14-day free trial',
|
|
89
|
+
},
|
|
90
|
+
{ interval: 'year', count: 1, trialDays: '30', summary: 'Billed every year · 30-day free trial' },
|
|
91
|
+
{
|
|
92
|
+
interval: 'week',
|
|
93
|
+
count: '2',
|
|
94
|
+
trialDays: 1,
|
|
95
|
+
summary: 'Billed every 2 weeks · 1-day free trial',
|
|
96
|
+
},
|
|
97
|
+
{ interval: ' DAY ', count: '7', trialDays: 0, summary: 'Billed every 7 days' },
|
|
98
|
+
{ interval: null, count: null, trialDays: '', summary: 'Billed every month' },
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
describe('ecommerce product transform — the billing summary of a subscription', () => {
|
|
102
|
+
const build = buildProduct()
|
|
103
|
+
|
|
104
|
+
it.each(BILLING_SUMMARY_FIXTURES)(
|
|
105
|
+
'describes $interval × $count with a $trialDays-day trial as "$summary"',
|
|
106
|
+
({ interval, count, trialDays, summary }) => {
|
|
107
|
+
const product = build({
|
|
108
|
+
...BASE,
|
|
109
|
+
payment_type: 'recurring',
|
|
110
|
+
recurring_interval: interval,
|
|
111
|
+
recurring_interval_count: count,
|
|
112
|
+
trial_days: trialDays,
|
|
113
|
+
})
|
|
114
|
+
expect(product.billingSummary).toBe(summary)
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
it('describes a one-time product with nothing, whatever its trial column holds', () => {
|
|
119
|
+
expect(build({ ...BASE, trial_days: 14 }).billingSummary).toBe('')
|
|
120
|
+
})
|
|
121
|
+
})
|
|
@@ -355,9 +355,9 @@ describe('generateDataSourceFetcher', () => {
|
|
|
355
355
|
const code = generateDataSourceFetcher(dataSource, 'teleport_products')
|
|
356
356
|
|
|
357
357
|
// The multi-select Category Filter writes ?categoryFilter=a,b,c; the scalar
|
|
358
|
-
// array_overlap branch splits it so
|
|
358
|
+
// array_overlap branch splits it so the overlap matches the union.
|
|
359
359
|
expect(code).toContain('array_overlap')
|
|
360
|
-
expect(code).toContain('
|
|
360
|
+
expect(code).toContain('function arrayOverlapSql(columns, param)')
|
|
361
361
|
expect(code).toContain("String(value).split(',')")
|
|
362
362
|
})
|
|
363
363
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import * as types from '@babel/types'
|
|
2
|
+
import generator from '@babel/generator'
|
|
3
|
+
import { parse } from '@babel/parser'
|
|
4
|
+
import {
|
|
5
|
+
ChunkType,
|
|
6
|
+
FileType,
|
|
7
|
+
ComponentStructure,
|
|
8
|
+
ChunkDefinition,
|
|
9
|
+
} from '@teleporthq/teleport-types'
|
|
10
|
+
import { createNextArrayMapperPaginationPlugin } from '../src/pagination-plugin'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A paginated list under a tab panel — `{activeTabIndex === 0 && (…)}` — is a
|
|
14
|
+
* DataProvider on the RIGHT of a logical expression. The plugin's JSX walkers
|
|
15
|
+
* visited children, bodies, arguments and ternary branches, never `left` /
|
|
16
|
+
* `right`, so the provider was never wired: no page params, no fetcher, and a
|
|
17
|
+
* shop whose grid sat under its first tab rendered nothing.
|
|
18
|
+
*/
|
|
19
|
+
const makeComponentChunk = (): ChunkDefinition => {
|
|
20
|
+
const program = parse(
|
|
21
|
+
`const TestComponent = (props) => {
|
|
22
|
+
return (
|
|
23
|
+
<div>
|
|
24
|
+
{activeTabIndex === 0 && (
|
|
25
|
+
<div className="panel">
|
|
26
|
+
<DataProvider name={'items'} renderSuccess={(items) => (
|
|
27
|
+
<Repeater items={items || []} renderItem={(product) => <div>{product.name}</div>} />
|
|
28
|
+
)} />
|
|
29
|
+
</div>
|
|
30
|
+
)}
|
|
31
|
+
</div>
|
|
32
|
+
)
|
|
33
|
+
}`,
|
|
34
|
+
{ sourceType: 'module', plugins: ['jsx'] }
|
|
35
|
+
)
|
|
36
|
+
return {
|
|
37
|
+
name: 'jsx-component',
|
|
38
|
+
type: ChunkType.AST,
|
|
39
|
+
fileType: FileType.JS,
|
|
40
|
+
linkAfter: [],
|
|
41
|
+
content: program.program.body[0] as types.VariableDeclaration,
|
|
42
|
+
meta: {},
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// tslint:disable-next-line:no-any
|
|
47
|
+
const makeUidlNode = (): any => ({
|
|
48
|
+
type: 'data-source-list',
|
|
49
|
+
content: {
|
|
50
|
+
renderPropIdentifier: 'items',
|
|
51
|
+
resourceDefinition: {
|
|
52
|
+
dataSourceId: 'ds1',
|
|
53
|
+
tableName: 'products',
|
|
54
|
+
dataSourceType: 'postgresql',
|
|
55
|
+
},
|
|
56
|
+
resource: { params: { queryColumns: { content: ['name'] } } },
|
|
57
|
+
nodes: {
|
|
58
|
+
success: {
|
|
59
|
+
type: 'cms-list-repeater',
|
|
60
|
+
content: {
|
|
61
|
+
renderPropIdentifier: 'product',
|
|
62
|
+
paginated: true,
|
|
63
|
+
perPage: 20,
|
|
64
|
+
searchEnabled: false,
|
|
65
|
+
nodes: { list: { type: 'element', content: { elementType: 'div' } } },
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe('pagination plugin — a provider gated by a logical expression', () => {
|
|
73
|
+
it('wires the page params and the fetcher onto a DataProvider under `cond && (…)`', async () => {
|
|
74
|
+
const chunk = makeComponentChunk()
|
|
75
|
+
const structure: ComponentStructure = {
|
|
76
|
+
uidl: { name: 'TestComponent', node: makeUidlNode() },
|
|
77
|
+
chunks: [chunk],
|
|
78
|
+
dependencies: {},
|
|
79
|
+
options: { dataSources: {}, extractedResources: {} },
|
|
80
|
+
} as never
|
|
81
|
+
await createNextArrayMapperPaginationPlugin()(structure)
|
|
82
|
+
const code = generator(chunk.content as types.Node).code.replace(/\s+/g, ' ')
|
|
83
|
+
|
|
84
|
+
expect(code).toContain('page: ds_0_page')
|
|
85
|
+
expect(code).toContain('perPage: 20')
|
|
86
|
+
expect(code).toContain('fetchData={')
|
|
87
|
+
})
|
|
88
|
+
})
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import * as types from '@babel/types'
|
|
2
|
+
import generator from '@babel/generator'
|
|
3
|
+
import { ComponentStructure } from '@teleporthq/teleport-types'
|
|
4
|
+
import { createNextArrayMapperPaginationPlugin } from '../src/pagination-plugin'
|
|
5
|
+
import { extractItemsPath } from '../src/items-path'
|
|
6
|
+
import { generateRESTAPIFetcher } from '../src/fetchers/rest-api'
|
|
7
|
+
import { generateJavaScriptFetcher } from '../src/fetchers/javascript'
|
|
8
|
+
import { makeUidlNode, makeWidgetComponentChunk } from './_helpers/pagination-widget-fixture'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A paginated list bound to an array INSIDE a REST payload
|
|
12
|
+
* (`{ templates: [...], total }` -> `data?.templates || []`) rendered every row
|
|
13
|
+
* on every page: the fetcher only paged a payload that was itself an array, and
|
|
14
|
+
* the count measured the wrapper as 0 rows.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const runHandler = async (
|
|
18
|
+
code: string,
|
|
19
|
+
query: Record<string, unknown>,
|
|
20
|
+
fetchImpl?: () => Promise<unknown>
|
|
21
|
+
) => {
|
|
22
|
+
const withoutImports = code.replace(/^import\s+.*?$/gm, '')
|
|
23
|
+
const factory = new Function(
|
|
24
|
+
'fetch',
|
|
25
|
+
`${withoutImports.replace('export default async function handler', 'async function handler')}
|
|
26
|
+
return handler`
|
|
27
|
+
)
|
|
28
|
+
const handler = factory(fetchImpl)
|
|
29
|
+
|
|
30
|
+
let payload: Record<string, unknown> = {}
|
|
31
|
+
const res = {
|
|
32
|
+
status: () => res,
|
|
33
|
+
json: (data: Record<string, unknown>) => {
|
|
34
|
+
payload = data
|
|
35
|
+
return res
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
await handler({ query, body: undefined }, res)
|
|
40
|
+
return payload
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const rows = Array.from({ length: 25 }, (_, i) => ({ id: i + 1, name: `row ${i + 1}` }))
|
|
44
|
+
const respondWith = (body: unknown) => () =>
|
|
45
|
+
Promise.resolve({ ok: true, status: 200, statusText: 'OK', json: () => Promise.resolve(body) })
|
|
46
|
+
|
|
47
|
+
describe('extractItemsPath', () => {
|
|
48
|
+
it('reads the property chain off the data source render prop', () => {
|
|
49
|
+
expect(extractItemsPath('ds?.templates || []', 'ds', 'rest-api')).toEqual(['templates'])
|
|
50
|
+
expect(extractItemsPath('ds?.data?.items', 'ds', 'rest-api')).toEqual(['data', 'items'])
|
|
51
|
+
expect(extractItemsPath('ds?.["my list"] || []', 'ds', 'javascript')).toEqual(['my list'])
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('is absent when the list is bound to the payload itself', () => {
|
|
55
|
+
expect(extractItemsPath('ds || []', 'ds', 'rest-api')).toBeUndefined()
|
|
56
|
+
expect(extractItemsPath('ds', 'ds', 'rest-api')).toBeUndefined()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('leaves anything it cannot read, and non-wrapping source types, alone', () => {
|
|
60
|
+
expect(extractItemsPath('ds?.items.filter(Boolean)', 'ds', 'rest-api')).toBeUndefined()
|
|
61
|
+
expect(extractItemsPath('other?.items', 'ds', 'rest-api')).toBeUndefined()
|
|
62
|
+
expect(extractItemsPath('ds?.items', 'ds', 'postgresql')).toBeUndefined()
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
interface FetcherSetup {
|
|
67
|
+
code: string
|
|
68
|
+
fetchImpl?: () => Promise<unknown>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe.each<[string, FetcherSetup]>([
|
|
72
|
+
[
|
|
73
|
+
'REST API',
|
|
74
|
+
{
|
|
75
|
+
code: generateRESTAPIFetcher({ url: 'https://api.test/items', method: 'GET' }),
|
|
76
|
+
fetchImpl: respondWith({ templates: rows, total: 25 }),
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
[
|
|
80
|
+
'JavaScript',
|
|
81
|
+
{ code: generateJavaScriptFetcher({ code: JSON.stringify({ templates: rows, total: 25 }) }) },
|
|
82
|
+
],
|
|
83
|
+
])('%s fetcher — itemsPath', (_label, setup) => {
|
|
84
|
+
it('pages the inner array and keeps the wrapper', async () => {
|
|
85
|
+
const result = await runHandler(
|
|
86
|
+
setup.code,
|
|
87
|
+
{
|
|
88
|
+
page: '2',
|
|
89
|
+
perPage: '10',
|
|
90
|
+
itemsPath: JSON.stringify(['templates']),
|
|
91
|
+
},
|
|
92
|
+
setup.fetchImpl
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
const data = result.data as { templates: Array<{ id: number }>; total: number }
|
|
96
|
+
expect(data.total).toBe(25)
|
|
97
|
+
expect(data.templates.map((r) => r.id)).toEqual([11, 12, 13, 14, 15, 16, 17, 18, 19, 20])
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('searches the inner array', async () => {
|
|
101
|
+
const result = await runHandler(
|
|
102
|
+
setup.code,
|
|
103
|
+
{
|
|
104
|
+
query: 'row 2',
|
|
105
|
+
queryColumns: JSON.stringify(['name']),
|
|
106
|
+
itemsPath: JSON.stringify(['templates']),
|
|
107
|
+
},
|
|
108
|
+
setup.fetchImpl
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
const data = result.data as { templates: Array<{ id: number }> }
|
|
112
|
+
expect(data.templates.map((r) => r.id)).toEqual([2, 20, 21, 22, 23, 24, 25])
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('returns the payload untouched without the param', async () => {
|
|
116
|
+
const result = await runHandler(setup.code, { page: '1', perPage: '10' }, setup.fetchImpl)
|
|
117
|
+
expect((result.data as { templates: unknown[] }).templates).toHaveLength(25)
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
describe('pagination plugin — list bound to a wrapped REST payload', () => {
|
|
122
|
+
const runPlugin = async (source: string): Promise<string> => {
|
|
123
|
+
const node = makeUidlNode({})
|
|
124
|
+
node.content.resourceDefinition.dataSourceType = 'rest-api'
|
|
125
|
+
node.content.nodes.success.content.source = source
|
|
126
|
+
const chunk = makeWidgetComponentChunk({ controls: ['previous', 'next'] })
|
|
127
|
+
const structure: ComponentStructure = {
|
|
128
|
+
uidl: { name: 'TestComponent', node },
|
|
129
|
+
chunks: [chunk],
|
|
130
|
+
dependencies: {},
|
|
131
|
+
options: { dataSources: {}, extractedResources: {} },
|
|
132
|
+
} as never
|
|
133
|
+
await createNextArrayMapperPaginationPlugin()(structure)
|
|
134
|
+
return generator(chunk.content as types.Node).code.replace(/\s+/g, ' ')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
it('sends itemsPath with the page fetch and collectionPath with the count', async () => {
|
|
138
|
+
const flat = await runPlugin('items?.templates || []')
|
|
139
|
+
|
|
140
|
+
expect(flat).toContain('itemsPath: "[\\"templates\\"]"')
|
|
141
|
+
expect(flat).toContain('collectionPath: "[\\"templates\\"]"')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('sends neither when the list is the payload', async () => {
|
|
145
|
+
const flat = await runPlugin('items || []')
|
|
146
|
+
|
|
147
|
+
expect(flat).not.toContain('itemsPath')
|
|
148
|
+
expect(flat).not.toContain('collectionPath')
|
|
149
|
+
})
|
|
150
|
+
})
|