@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
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"config": {
|
|
3
|
+
"standard": { "route": "/products", "attribute": "slug" },
|
|
4
|
+
"pages": {
|
|
5
|
+
"personalise": { "route": "/personalise", "attribute": "slug", "name": "Personalise" },
|
|
6
|
+
"root-page": { "route": "/", "attribute": "slug", "name": "Root" },
|
|
7
|
+
"by-id": { "route": "/studio/items", "attribute": "id", "name": "By id" },
|
|
8
|
+
"by-sku": { "route": "/engrave", "attribute": "sku", "name": "By SKU" }
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"cases": [
|
|
12
|
+
{ "name": "no page", "raw": null, "values": { "slug": "mug", "id": "7" }, "key": "", "url": "/products/mug" },
|
|
13
|
+
{ "name": "blank page", "raw": "", "values": { "slug": "mug", "id": "7" }, "key": "", "url": "/products/mug" },
|
|
14
|
+
{ "name": "spaces around the key", "raw": " personalise ", "values": { "slug": "postcard", "id": "1" }, "key": "personalise", "url": "/personalise/postcard" },
|
|
15
|
+
{ "name": "custom page", "raw": "personalise", "values": { "slug": "postcard", "id": "1" }, "key": "personalise", "url": "/personalise/postcard" },
|
|
16
|
+
{ "name": "key no page carries", "raw": "deleted-page", "values": { "slug": "mug", "id": "7" }, "key": "", "url": "/products/mug" },
|
|
17
|
+
{ "name": "case matters", "raw": "Personalise", "values": { "slug": "mug", "id": "7" }, "key": "", "url": "/products/mug" },
|
|
18
|
+
{ "name": "page at the site root", "raw": "root-page", "values": { "slug": "tee", "id": "3" }, "key": "root-page", "url": "/tee" },
|
|
19
|
+
{ "name": "page keyed by id", "raw": "by-id", "values": { "slug": "tee", "id": "3" }, "key": "by-id", "url": "/studio/items/3" },
|
|
20
|
+
{ "name": "value is encoded", "raw": "personalise", "values": { "slug": "café crème", "id": "4" }, "key": "personalise", "url": "/personalise/caf%C3%A9%20cr%C3%A8me" },
|
|
21
|
+
{ "name": "no value, no address", "raw": "personalise", "values": { "slug": "", "id": "4" }, "key": "personalise", "url": "" },
|
|
22
|
+
{ "name": "a number is not a key", "raw": 5, "values": { "slug": "mug", "id": "7" }, "key": "", "url": "/products/mug" },
|
|
23
|
+
{ "name": "page keyed by another column", "raw": "by-sku", "values": { "slug": "ring", "id": "9", "sku": "RING-01" }, "key": "by-sku", "url": "/engrave/RING-01" }
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -130,7 +130,9 @@ describe('the generated Postgres route', () => {
|
|
|
130
130
|
const ordersRoute = generatePostgreSQLFetcher(PG_CONFIG, 'teleport_orders')
|
|
131
131
|
expect(ordersRoute).toContain('sortFieldSql(sort.field)')
|
|
132
132
|
expect(ordersRoute).not.toContain('if (field ===')
|
|
133
|
-
|
|
133
|
+
// The discount sub-select's expansion — not `jsonb_array_elements_text`,
|
|
134
|
+
// which the list-column filter (array-overlap-sql.ts) uses on every table.
|
|
135
|
+
expect(ordersRoute).not.toContain('jsonb_array_elements(')
|
|
134
136
|
})
|
|
135
137
|
|
|
136
138
|
it('still parses for a non-products table', () => {
|
|
@@ -263,7 +265,7 @@ describe('the fetcher a generated storefront actually gets', () => {
|
|
|
263
265
|
'teleport_orders'
|
|
264
266
|
)
|
|
265
267
|
expect(orders).not.toContain('if (field ===')
|
|
266
|
-
expect(orders).not.toContain('jsonb_array_elements')
|
|
268
|
+
expect(orders).not.toContain('jsonb_array_elements(')
|
|
267
269
|
expect(() => parse(orders, { sourceType: 'module', plugins: ['jsx'] })).not.toThrow()
|
|
268
270
|
})
|
|
269
271
|
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { generateSharedTransformationCode } from '../src/transformations/shared-utils'
|
|
2
|
+
import {
|
|
3
|
+
getTransformExpression,
|
|
4
|
+
getTransformWrapperCode,
|
|
5
|
+
RAW_ROWS_PARAM,
|
|
6
|
+
} from '../src/transformations'
|
|
7
|
+
|
|
8
|
+
// The generated admin's edit forms write every column they load back, so they
|
|
9
|
+
// ask a transformed table (products, blog posts, custom pages) for its rows AS
|
|
10
|
+
// STORED. Fed the product view model instead, a digital product's form showed
|
|
11
|
+
// it as not digital, ticked the gift-card switch of a product that is not one,
|
|
12
|
+
// lost its discounts, and every save wrote `discounts`, `category_ids`,
|
|
13
|
+
// `download_limit`, … back blank. These evaluate the EMITTED code.
|
|
14
|
+
|
|
15
|
+
type Runtime = {
|
|
16
|
+
rawRecords: (records: unknown, getClient: unknown) => Promise<Array<Record<string, unknown>>>
|
|
17
|
+
wantsRawRows: (query: unknown) => boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ASSET_ID = 'asset-7f3c'
|
|
21
|
+
const ASSET_URL = 'https://cdn.example.com/uploads/cover.png'
|
|
22
|
+
|
|
23
|
+
// `getAssetMap` reads the project's asset table through `getClient()`; the
|
|
24
|
+
// fake client answers that one query with a single uploaded asset.
|
|
25
|
+
const fakeGetClient = () => ({
|
|
26
|
+
connect: async () => undefined,
|
|
27
|
+
end: async () => undefined,
|
|
28
|
+
query: async () => ({ rows: [{ id: ASSET_ID, remote_src: ASSET_URL, src: ASSET_URL }] }),
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const load = (tableName: string): Runtime => {
|
|
32
|
+
const bundle = generateSharedTransformationCode() + '\n' + getTransformWrapperCode(tableName)
|
|
33
|
+
return new Function(
|
|
34
|
+
bundle + '\nreturn { rawRecords: rawRecords, wantsRawRows: __tqWantsRawRows };'
|
|
35
|
+
)() as Runtime
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const storedProduct = () => ({
|
|
39
|
+
id: 'p-1',
|
|
40
|
+
name: 'test',
|
|
41
|
+
status: null,
|
|
42
|
+
is_digital: true,
|
|
43
|
+
is_gift_card: false,
|
|
44
|
+
discounts: '[{"id":"d1","type":"fixed","value":10,"startsAt":null,"endsAt":null}]',
|
|
45
|
+
category_ids: '["cat_1"]',
|
|
46
|
+
download_limit: 3,
|
|
47
|
+
image_url: ASSET_ID,
|
|
48
|
+
gallery_images: `["https://img.example.com/a.png","${ASSET_ID}"]`,
|
|
49
|
+
metadata: '{"note":"asset-7f3c is only text here"}',
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('raw-rows mode of a transformed table', () => {
|
|
53
|
+
it('is chosen by the request parameter, as sent by a server call or over HTTP', () => {
|
|
54
|
+
const { wantsRawRows } = load('teleport_products')
|
|
55
|
+
expect(wantsRawRows({ [RAW_ROWS_PARAM]: true })).toBe(true)
|
|
56
|
+
expect(wantsRawRows({ [RAW_ROWS_PARAM]: 'true' })).toBe(true)
|
|
57
|
+
expect(wantsRawRows({ [RAW_ROWS_PARAM]: 'false' })).toBe(false)
|
|
58
|
+
expect(wantsRawRows({})).toBe(false)
|
|
59
|
+
expect(wantsRawRows(undefined)).toBe(false)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('returns every stored column untouched — booleans, NULLs and the columns the view model drops', async () => {
|
|
63
|
+
const [row] = await load('teleport_products').rawRecords([storedProduct()], fakeGetClient)
|
|
64
|
+
expect(row.is_digital).toBe(true)
|
|
65
|
+
expect(row.is_gift_card).toBe(false)
|
|
66
|
+
expect(row.status).toBeNull()
|
|
67
|
+
expect(row.discounts).toBe(storedProduct().discounts)
|
|
68
|
+
expect(row.category_ids).toBe('["cat_1"]')
|
|
69
|
+
expect(row.download_limit).toBe(3)
|
|
70
|
+
expect(Object.keys(row).sort()).toEqual(Object.keys(storedProduct()).sort())
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('turns an uploaded asset id into its URL, alone or inside a JSON list, and nothing else', async () => {
|
|
74
|
+
const [row] = await load('teleport_products').rawRecords([storedProduct()], fakeGetClient)
|
|
75
|
+
expect(row.image_url).toBe(ASSET_URL)
|
|
76
|
+
expect(JSON.parse(row.gallery_images as string)).toEqual([
|
|
77
|
+
'https://img.example.com/a.png',
|
|
78
|
+
ASSET_URL,
|
|
79
|
+
])
|
|
80
|
+
// An object column that merely mentions the id is not a reference.
|
|
81
|
+
expect(row.metadata).toBe(storedProduct().metadata)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('passes the rows through when the asset lookup fails', async () => {
|
|
85
|
+
const failingClient = () => ({
|
|
86
|
+
connect: async () => {
|
|
87
|
+
throw new Error('down')
|
|
88
|
+
},
|
|
89
|
+
end: async () => undefined,
|
|
90
|
+
query: async () => ({ rows: [] }),
|
|
91
|
+
})
|
|
92
|
+
const [row] = await load('teleport_products').rawRecords([storedProduct()], failingClient)
|
|
93
|
+
expect(row.image_url).toBe(ASSET_ID)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('is carried by every transformed table, and the handler picks it per request', async () => {
|
|
97
|
+
for (const table of ['teleport_products', 'teleport_blog_posts', 'teleport_pages']) {
|
|
98
|
+
expect(getTransformWrapperCode(table)).toContain('async function rawRecords(')
|
|
99
|
+
const expression = getTransformExpression(table)!
|
|
100
|
+
expect(expression).toContain('__tqWantsRawRows(req.query)')
|
|
101
|
+
expect(expression).toContain('rawRecords(safeData, getClient)')
|
|
102
|
+
expect(expression).toContain('transformRecords(safeData, getClient, req.query)')
|
|
103
|
+
}
|
|
104
|
+
expect(getTransformExpression('orders')).toBeNull()
|
|
105
|
+
const [page] = await load('teleport_pages').rawRecords(
|
|
106
|
+
[{ id: 'pg', blocks: '[]', no_index: 'true' }],
|
|
107
|
+
fakeGetClient
|
|
108
|
+
)
|
|
109
|
+
expect(page).toEqual({ id: 'pg', blocks: '[]', no_index: 'true' })
|
|
110
|
+
})
|
|
111
|
+
})
|
|
@@ -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,434 @@
|
|
|
1
|
+
import { DataCache } from '@teleporthq/teleport-shared'
|
|
2
|
+
import { UIDLDataSource } from '@teleporthq/teleport-types'
|
|
3
|
+
import { generateRESTAPIFetcher } from '../src/fetchers/rest-api'
|
|
4
|
+
import { generateDataSourceFetcherWithCore } from '../src/data-source-fetchers'
|
|
5
|
+
import { resolveRESTAPICacheTTLSeconds } from '../src/fetchers/utils/rest-api-response-cache'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The REST API data source's response cache, asserted on BEHAVIOUR: every test
|
|
9
|
+
* runs the generated handler against a counting `fetch`, the same way the
|
|
10
|
+
* published app runs it.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
type Handler = (req: Record<string, unknown>, res: Record<string, unknown>) => Promise<unknown>
|
|
14
|
+
|
|
15
|
+
interface Captured {
|
|
16
|
+
status: number
|
|
17
|
+
payload: Record<string, unknown>
|
|
18
|
+
headers: Record<string, string>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const URL = 'https://api.test/items'
|
|
22
|
+
const cachedConfig = (ttlSeconds: number, extra: Record<string, unknown> = {}) => ({
|
|
23
|
+
url: URL,
|
|
24
|
+
method: 'GET',
|
|
25
|
+
cache: { enabled: true, ttlSeconds },
|
|
26
|
+
...extra,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const upstreamResponse = (body: unknown, status = 200) => ({
|
|
30
|
+
ok: status >= 200 && status < 300,
|
|
31
|
+
status,
|
|
32
|
+
statusText: status === 200 ? 'OK' : 'Service Unavailable',
|
|
33
|
+
text: () => Promise.resolve(typeof body === 'string' ? body : JSON.stringify(body)),
|
|
34
|
+
json: () => Promise.resolve(body),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const loadHandler = (code: string, fetchImpl: jest.Mock): Handler => {
|
|
38
|
+
const withoutImports = code.replace(/^import\s+.*?$/gm, '')
|
|
39
|
+
const factory = new Function(
|
|
40
|
+
'fetch',
|
|
41
|
+
`${withoutImports.replace('export default async function handler', 'async function handler')}
|
|
42
|
+
return handler`
|
|
43
|
+
)
|
|
44
|
+
return factory(fetchImpl)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A generated `utils/data-sources` module, with its ES exports turned into a return. */
|
|
48
|
+
const loadDataSourceModule = (code: string, fetchImpl: jest.Mock) => {
|
|
49
|
+
const body = code.replace(/^import\s+.*?$/gm, '').replace(/^export\s+.*$/gm, '')
|
|
50
|
+
const factory = new Function('fetch', `${body}\nreturn { fetchData, fetchCount }`)
|
|
51
|
+
return factory(fetchImpl) as {
|
|
52
|
+
fetchData: (params?: Record<string, unknown>) => Promise<unknown>
|
|
53
|
+
fetchCount: (params?: Record<string, unknown>) => Promise<number>
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Calls the handler like a Next API route does. `withSetHeader: false` is the
|
|
59
|
+
* bare `{ status, json }` object fetchData, fetchCount and tqWithCache pass.
|
|
60
|
+
*/
|
|
61
|
+
const call = async (
|
|
62
|
+
handler: Handler,
|
|
63
|
+
req: Record<string, unknown> = {},
|
|
64
|
+
withSetHeader = true
|
|
65
|
+
): Promise<Captured> => {
|
|
66
|
+
const captured: Captured = { status: 200, payload: {}, headers: {} }
|
|
67
|
+
const res: Record<string, unknown> = {
|
|
68
|
+
status: (code: number) => {
|
|
69
|
+
captured.status = code
|
|
70
|
+
return res
|
|
71
|
+
},
|
|
72
|
+
json: (data: Record<string, unknown>) => {
|
|
73
|
+
captured.payload = data
|
|
74
|
+
return res
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
if (withSetHeader) {
|
|
78
|
+
res.setHeader = (name: string, value: string) => {
|
|
79
|
+
captured.headers[name] = value
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
await handler({ query: {}, method: 'GET', ...req }, res)
|
|
83
|
+
return captured
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const ROWS = [
|
|
87
|
+
{ id: 1, name: 'b' },
|
|
88
|
+
{ id: 2, name: 'c' },
|
|
89
|
+
{ id: 3, name: 'a' },
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
let now = 1_000_000
|
|
93
|
+
|
|
94
|
+
beforeEach(() => {
|
|
95
|
+
delete (globalThis as Record<string, unknown>).__tqRestApiCache
|
|
96
|
+
now = 1_000_000
|
|
97
|
+
jest.spyOn(Date, 'now').mockImplementation(() => now)
|
|
98
|
+
jest.spyOn(console, 'error').mockImplementation(() => undefined)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
afterEach(() => {
|
|
102
|
+
jest.restoreAllMocks()
|
|
103
|
+
delete (globalThis as Record<string, unknown>).__tqRestApiCache
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
describe('resolveRESTAPICacheTTLSeconds', () => {
|
|
107
|
+
it('is off unless the cache is explicitly enabled', () => {
|
|
108
|
+
expect(resolveRESTAPICacheTTLSeconds(undefined)).toBeUndefined()
|
|
109
|
+
expect(resolveRESTAPICacheTTLSeconds({ enabled: false, ttlSeconds: 60 })).toBeUndefined()
|
|
110
|
+
expect(resolveRESTAPICacheTTLSeconds({ ttlSeconds: 60 })).toBeUndefined()
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('uses the configured window in whole seconds', () => {
|
|
114
|
+
expect(resolveRESTAPICacheTTLSeconds({ enabled: true, ttlSeconds: 90 })).toBe(90)
|
|
115
|
+
expect(resolveRESTAPICacheTTLSeconds({ enabled: true, ttlSeconds: 90.9 })).toBe(90)
|
|
116
|
+
expect(resolveRESTAPICacheTTLSeconds({ enabled: true, ttlSeconds: 0.2 })).toBe(1)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('falls back to the shared default when the window is missing or unusable', () => {
|
|
120
|
+
const fallback = DataCache.DEFAULT_CACHE_TTL_SECONDS
|
|
121
|
+
expect(resolveRESTAPICacheTTLSeconds({ enabled: true })).toBe(fallback)
|
|
122
|
+
expect(resolveRESTAPICacheTTLSeconds({ enabled: true, ttlSeconds: 0 })).toBe(fallback)
|
|
123
|
+
expect(resolveRESTAPICacheTTLSeconds({ enabled: true, ttlSeconds: -5 })).toBe(fallback)
|
|
124
|
+
expect(resolveRESTAPICacheTTLSeconds({ enabled: true, ttlSeconds: NaN })).toBe(fallback)
|
|
125
|
+
expect(
|
|
126
|
+
resolveRESTAPICacheTTLSeconds({ enabled: true, ttlSeconds: '60' as unknown as number })
|
|
127
|
+
).toBe(fallback)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('never emits a window longer than a year', () => {
|
|
131
|
+
expect(resolveRESTAPICacheTTLSeconds({ enabled: true, ttlSeconds: 10 ** 12 })).toBe(31_536_000)
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
describe('generateRESTAPIFetcher — response cache', () => {
|
|
136
|
+
/** The regression pin: a data source that does not cache generates what it always did. */
|
|
137
|
+
it('emits byte-identical output when caching is absent or disabled', () => {
|
|
138
|
+
const base = { url: URL, method: 'GET' }
|
|
139
|
+
const uncached = generateRESTAPIFetcher(base)
|
|
140
|
+
|
|
141
|
+
expect(generateRESTAPIFetcher({ ...base, cache: { enabled: false, ttlSeconds: 60 } })).toBe(
|
|
142
|
+
uncached
|
|
143
|
+
)
|
|
144
|
+
expect(uncached).not.toContain('__tqRestApiCache')
|
|
145
|
+
expect(uncached).toContain('const response = await fetch(url, options)')
|
|
146
|
+
expect(uncached).toContain('let data = await response.json()')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('emits the configured window in milliseconds', () => {
|
|
150
|
+
const code = generateRESTAPIFetcher(cachedConfig(90))
|
|
151
|
+
|
|
152
|
+
expect(code).toContain('const REST_API_CACHE_TTL_MS = 90000')
|
|
153
|
+
expect(code).toContain('const response = await fetchRestApiWithCache(url, options)')
|
|
154
|
+
expect(code).toContain('let data = JSON.parse(response.bodyText)')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('calls the URL once and serves every later request in the window from memory', async () => {
|
|
158
|
+
const fetchImpl = jest.fn(() => Promise.resolve(upstreamResponse(ROWS)))
|
|
159
|
+
const handler = loadHandler(generateRESTAPIFetcher(cachedConfig(60)), fetchImpl)
|
|
160
|
+
|
|
161
|
+
const first = await call(handler)
|
|
162
|
+
now += 59_000
|
|
163
|
+
const second = await call(handler, { query: { page: '1', perPage: '2' } })
|
|
164
|
+
const third = await call(handler, { query: { query: 'a', queryColumns: '["name"]' } })
|
|
165
|
+
|
|
166
|
+
expect(fetchImpl).toHaveBeenCalledTimes(1)
|
|
167
|
+
expect(first.payload).toMatchObject({ success: true, data: ROWS })
|
|
168
|
+
expect(second.payload.data).toEqual(ROWS.slice(0, 2))
|
|
169
|
+
expect(third.payload.data).toEqual([{ id: 3, name: 'a' }])
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('calls the URL again once the window has passed', async () => {
|
|
173
|
+
const fetchImpl = jest
|
|
174
|
+
.fn()
|
|
175
|
+
.mockResolvedValueOnce(upstreamResponse(ROWS))
|
|
176
|
+
.mockResolvedValueOnce(upstreamResponse([{ id: 9, name: 'fresh' }]))
|
|
177
|
+
const handler = loadHandler(generateRESTAPIFetcher(cachedConfig(60)), fetchImpl)
|
|
178
|
+
|
|
179
|
+
await call(handler)
|
|
180
|
+
now += 60_000
|
|
181
|
+
const refreshed = await call(handler)
|
|
182
|
+
now += 1_000
|
|
183
|
+
const servedFromNewEntry = await call(handler)
|
|
184
|
+
|
|
185
|
+
expect(fetchImpl).toHaveBeenCalledTimes(2)
|
|
186
|
+
expect(refreshed.payload.data).toEqual([{ id: 9, name: 'fresh' }])
|
|
187
|
+
expect(servedFromNewEntry.payload.data).toEqual([{ id: 9, name: 'fresh' }])
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
/** A clock stepped back (NTP) must not make an entry look fresh for longer than its window. */
|
|
191
|
+
it('calls the URL again when the clock has moved backwards', async () => {
|
|
192
|
+
const fetchImpl = jest.fn(() => Promise.resolve(upstreamResponse(ROWS)))
|
|
193
|
+
const handler = loadHandler(generateRESTAPIFetcher(cachedConfig(60)), fetchImpl)
|
|
194
|
+
|
|
195
|
+
await call(handler)
|
|
196
|
+
now -= 10_000
|
|
197
|
+
await call(handler)
|
|
198
|
+
|
|
199
|
+
expect(fetchImpl).toHaveBeenCalledTimes(2)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('shares one upstream call between concurrent requests', async () => {
|
|
203
|
+
let resolveUpstream: (value: unknown) => void = () => undefined
|
|
204
|
+
const fetchImpl = jest.fn(
|
|
205
|
+
() =>
|
|
206
|
+
new Promise((resolve) => {
|
|
207
|
+
resolveUpstream = resolve
|
|
208
|
+
})
|
|
209
|
+
)
|
|
210
|
+
const handler = loadHandler(generateRESTAPIFetcher(cachedConfig(60)), fetchImpl)
|
|
211
|
+
|
|
212
|
+
const pending = [call(handler), call(handler, { query: { page: '2', perPage: '1' } })]
|
|
213
|
+
resolveUpstream(upstreamResponse(ROWS))
|
|
214
|
+
const [all, pageTwo] = await Promise.all(pending)
|
|
215
|
+
|
|
216
|
+
expect(fetchImpl).toHaveBeenCalledTimes(1)
|
|
217
|
+
expect(all.payload.data).toEqual(ROWS)
|
|
218
|
+
expect(pageTwo.payload.data).toEqual([ROWS[1]])
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('stops waiting on an upstream call that has been pending for too long', async () => {
|
|
222
|
+
const fetchImpl = jest
|
|
223
|
+
.fn()
|
|
224
|
+
.mockImplementationOnce(() => new Promise(() => undefined))
|
|
225
|
+
.mockResolvedValueOnce(upstreamResponse(ROWS))
|
|
226
|
+
const handler = loadHandler(generateRESTAPIFetcher(cachedConfig(60)), fetchImpl)
|
|
227
|
+
|
|
228
|
+
// Never settles: stands in for a request stuck on a dead socket.
|
|
229
|
+
void call(handler)
|
|
230
|
+
now += 10_000
|
|
231
|
+
const next = await call(handler)
|
|
232
|
+
|
|
233
|
+
expect(fetchImpl).toHaveBeenCalledTimes(2)
|
|
234
|
+
expect(next.payload.data).toEqual(ROWS)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('answers an upstream error as before and never caches it', async () => {
|
|
238
|
+
const fetchImpl = jest
|
|
239
|
+
.fn()
|
|
240
|
+
.mockResolvedValueOnce(upstreamResponse({ message: 'down' }, 503))
|
|
241
|
+
.mockResolvedValueOnce(upstreamResponse(ROWS))
|
|
242
|
+
const handler = loadHandler(generateRESTAPIFetcher(cachedConfig(60)), fetchImpl)
|
|
243
|
+
|
|
244
|
+
const failed = await call(handler)
|
|
245
|
+
const recovered = await call(handler)
|
|
246
|
+
|
|
247
|
+
expect(failed.status).toBe(503)
|
|
248
|
+
expect(failed.payload).toMatchObject({
|
|
249
|
+
success: false,
|
|
250
|
+
error: 'HTTP 503: Service Unavailable',
|
|
251
|
+
})
|
|
252
|
+
expect(failed.headers).toEqual({})
|
|
253
|
+
expect(recovered.payload.data).toEqual(ROWS)
|
|
254
|
+
expect(fetchImpl).toHaveBeenCalledTimes(2)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('never caches a network failure or a body that is not JSON', async () => {
|
|
258
|
+
const fetchImpl = jest
|
|
259
|
+
.fn()
|
|
260
|
+
.mockRejectedValueOnce(new Error('socket hang up'))
|
|
261
|
+
.mockResolvedValueOnce(upstreamResponse('<html>maintenance</html>'))
|
|
262
|
+
.mockResolvedValueOnce(upstreamResponse(ROWS))
|
|
263
|
+
const handler = loadHandler(generateRESTAPIFetcher(cachedConfig(60)), fetchImpl)
|
|
264
|
+
|
|
265
|
+
const networkFailure = await call(handler)
|
|
266
|
+
const notJson = await call(handler)
|
|
267
|
+
const recovered = await call(handler)
|
|
268
|
+
|
|
269
|
+
expect(networkFailure.status).toBe(500)
|
|
270
|
+
expect(networkFailure.payload).toMatchObject({ success: false, error: 'socket hang up' })
|
|
271
|
+
expect(notJson.status).toBe(500)
|
|
272
|
+
expect(notJson.payload.success).toBe(false)
|
|
273
|
+
expect(recovered.payload.data).toEqual(ROWS)
|
|
274
|
+
expect(fetchImpl).toHaveBeenCalledTimes(3)
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* The handler sorts in place. Were the cached payload handed out by
|
|
279
|
+
* reference, one visitor's sort would reorder every later visitor's rows.
|
|
280
|
+
*/
|
|
281
|
+
it('gives every request its own copy of the cached payload', async () => {
|
|
282
|
+
const fetchImpl = jest.fn(() => Promise.resolve(upstreamResponse(ROWS)))
|
|
283
|
+
const handler = loadHandler(generateRESTAPIFetcher(cachedConfig(60)), fetchImpl)
|
|
284
|
+
|
|
285
|
+
const sorted = await call(handler, { query: { sortBy: 'name', sortOrder: 'asc' } })
|
|
286
|
+
const unsorted = await call(handler)
|
|
287
|
+
|
|
288
|
+
expect((sorted.payload.data as typeof ROWS).map((row) => row.name)).toEqual(['a', 'b', 'c'])
|
|
289
|
+
expect(unsorted.payload.data).toEqual(ROWS)
|
|
290
|
+
expect(fetchImpl).toHaveBeenCalledTimes(1)
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it('keys entries by the request body, so different bodies never share a response', async () => {
|
|
294
|
+
const fetchImpl = jest.fn((_url: string, options: { body?: string }) =>
|
|
295
|
+
Promise.resolve(upstreamResponse([{ echo: options.body }]))
|
|
296
|
+
)
|
|
297
|
+
const handler = loadHandler(
|
|
298
|
+
generateRESTAPIFetcher(cachedConfig(60, { method: 'POST', bodyType: 'json' })),
|
|
299
|
+
fetchImpl
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
const first = await call(handler, { method: 'POST', body: { q: 'first' } })
|
|
303
|
+
const second = await call(handler, { method: 'POST', body: { q: 'second' } })
|
|
304
|
+
const firstAgain = await call(handler, { method: 'POST', body: { q: 'first' } })
|
|
305
|
+
|
|
306
|
+
expect(first.payload.data).toEqual([{ echo: '{"q":"first"}' }])
|
|
307
|
+
expect(second.payload.data).toEqual([{ echo: '{"q":"second"}' }])
|
|
308
|
+
expect(firstAgain.payload.data).toEqual([{ echo: '{"q":"first"}' }])
|
|
309
|
+
expect(fetchImpl).toHaveBeenCalledTimes(2)
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('shares the store between every REST API data source module in the process', async () => {
|
|
313
|
+
const fetchImpl = jest.fn(() => Promise.resolve(upstreamResponse(ROWS)))
|
|
314
|
+
const productsRoute = loadHandler(generateRESTAPIFetcher(cachedConfig(60)), fetchImpl)
|
|
315
|
+
const globalStateRoute = loadHandler(generateRESTAPIFetcher(cachedConfig(60)), fetchImpl)
|
|
316
|
+
|
|
317
|
+
await call(productsRoute)
|
|
318
|
+
const fromOtherModule = await call(globalStateRoute)
|
|
319
|
+
|
|
320
|
+
expect(fetchImpl).toHaveBeenCalledTimes(1)
|
|
321
|
+
expect(fromOtherModule.payload.data).toEqual(ROWS)
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
it("judges a shared entry by the reader's own window", async () => {
|
|
325
|
+
const fetchImpl = jest.fn(() => Promise.resolve(upstreamResponse(ROWS)))
|
|
326
|
+
const shortWindow = loadHandler(generateRESTAPIFetcher(cachedConfig(60)), fetchImpl)
|
|
327
|
+
const longWindow = loadHandler(generateRESTAPIFetcher(cachedConfig(3_600)), fetchImpl)
|
|
328
|
+
|
|
329
|
+
await call(longWindow)
|
|
330
|
+
now += 120_000
|
|
331
|
+
await call(longWindow)
|
|
332
|
+
expect(fetchImpl).toHaveBeenCalledTimes(1)
|
|
333
|
+
|
|
334
|
+
await call(shortWindow)
|
|
335
|
+
expect(fetchImpl).toHaveBeenCalledTimes(2)
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
it('drops the least recently used entry once the store is full', async () => {
|
|
339
|
+
const fetchImpl = jest.fn(() => Promise.resolve(upstreamResponse(ROWS)))
|
|
340
|
+
const handler = loadHandler(
|
|
341
|
+
generateRESTAPIFetcher(cachedConfig(3_600, { method: 'POST', bodyType: 'json' })),
|
|
342
|
+
fetchImpl
|
|
343
|
+
)
|
|
344
|
+
const request = (index: number) => call(handler, { method: 'POST', body: { index } })
|
|
345
|
+
|
|
346
|
+
await request(0)
|
|
347
|
+
await request(1)
|
|
348
|
+
// A hit makes entry 0 recent again, which leaves entry 1 the oldest.
|
|
349
|
+
await request(0)
|
|
350
|
+
for (let index = 2; index <= 100; index++) {
|
|
351
|
+
await request(index)
|
|
352
|
+
}
|
|
353
|
+
expect(fetchImpl).toHaveBeenCalledTimes(101)
|
|
354
|
+
|
|
355
|
+
await request(0)
|
|
356
|
+
expect(fetchImpl).toHaveBeenCalledTimes(101)
|
|
357
|
+
await request(1)
|
|
358
|
+
expect(fetchImpl).toHaveBeenCalledTimes(102)
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
describe('Cache-Control', () => {
|
|
362
|
+
it('lets the CDN keep a GET response for what is left of the window', async () => {
|
|
363
|
+
const fetchImpl = jest.fn(() => Promise.resolve(upstreamResponse(ROWS)))
|
|
364
|
+
const handler = loadHandler(generateRESTAPIFetcher(cachedConfig(60)), fetchImpl)
|
|
365
|
+
|
|
366
|
+
const fresh = await call(handler)
|
|
367
|
+
now += 45_500
|
|
368
|
+
const aged = await call(handler)
|
|
369
|
+
|
|
370
|
+
expect(fresh.headers['Cache-Control']).toBe('public, max-age=0, s-maxage=60')
|
|
371
|
+
expect(aged.headers['Cache-Control']).toBe('public, max-age=0, s-maxage=14')
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
it('sends nothing for a request that is not a GET', async () => {
|
|
375
|
+
const fetchImpl = jest.fn(() => Promise.resolve(upstreamResponse(ROWS)))
|
|
376
|
+
const handler = loadHandler(
|
|
377
|
+
generateRESTAPIFetcher(cachedConfig(60, { method: 'POST' })),
|
|
378
|
+
fetchImpl
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
const response = await call(handler, { method: 'POST', body: { q: 1 } })
|
|
382
|
+
|
|
383
|
+
expect(response.payload.success).toBe(true)
|
|
384
|
+
expect(response.headers).toEqual({})
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
it('tolerates the bare response object server-side callers pass', async () => {
|
|
388
|
+
const fetchImpl = jest.fn(() => Promise.resolve(upstreamResponse(ROWS)))
|
|
389
|
+
const handler = loadHandler(generateRESTAPIFetcher(cachedConfig(60)), fetchImpl)
|
|
390
|
+
|
|
391
|
+
const response = await call(handler, {}, false)
|
|
392
|
+
|
|
393
|
+
expect(response.payload).toMatchObject({ success: true, data: ROWS })
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
it('is never sent by a data source that does not cache', async () => {
|
|
397
|
+
const fetchImpl = jest.fn(() => Promise.resolve(upstreamResponse(ROWS)))
|
|
398
|
+
const handler = loadHandler(generateRESTAPIFetcher({ url: URL, method: 'GET' }), fetchImpl)
|
|
399
|
+
|
|
400
|
+
const response = await call(handler)
|
|
401
|
+
|
|
402
|
+
expect(response.payload.data).toEqual(ROWS)
|
|
403
|
+
expect(response.headers).toEqual({})
|
|
404
|
+
})
|
|
405
|
+
})
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
describe('generateDataSourceFetcherWithCore — cached REST API source', () => {
|
|
409
|
+
const dataSource: UIDLDataSource = {
|
|
410
|
+
id: 'ds_rest_0001',
|
|
411
|
+
name: 'Catalog',
|
|
412
|
+
type: 'rest-api',
|
|
413
|
+
config: cachedConfig(60),
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* A paginated page asks for its rows (getStaticProps -> fetchData) and its
|
|
418
|
+
* total (fetchCount) — two handler runs that must cost one upstream call.
|
|
419
|
+
*/
|
|
420
|
+
it('serves fetchData and fetchCount from one upstream call', async () => {
|
|
421
|
+
const fetchImpl = jest.fn(() => Promise.resolve(upstreamResponse(ROWS)))
|
|
422
|
+
const code = generateDataSourceFetcherWithCore(dataSource, 'data', false, {})
|
|
423
|
+
const dataSourceModule = loadDataSourceModule(code, fetchImpl)
|
|
424
|
+
|
|
425
|
+
const [rows, count] = await Promise.all([
|
|
426
|
+
dataSourceModule.fetchData({ limit: '2' }),
|
|
427
|
+
dataSourceModule.fetchCount({}),
|
|
428
|
+
])
|
|
429
|
+
|
|
430
|
+
expect(rows).toEqual(ROWS.slice(0, 2))
|
|
431
|
+
expect(count).toBe(3)
|
|
432
|
+
expect(fetchImpl).toHaveBeenCalledTimes(1)
|
|
433
|
+
})
|
|
434
|
+
})
|