@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,188 @@
|
|
|
1
|
+
import { DataCache } from '@teleporthq/teleport-shared'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The REST API data source's response cache: `config.cache` as the editor
|
|
5
|
+
* stores it on the data source.
|
|
6
|
+
*/
|
|
7
|
+
export interface RESTAPICacheConfig {
|
|
8
|
+
enabled?: boolean
|
|
9
|
+
ttlSeconds?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** A year: past that, the window only ever ends with the next deployment. */
|
|
13
|
+
const MAX_TTL_SECONDS = 31_536_000
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Bounds for the one store every REST API data source in a server process
|
|
17
|
+
* shares. Entries are raw JSON text, so the character budget is close to the
|
|
18
|
+
* memory the cache holds.
|
|
19
|
+
*/
|
|
20
|
+
const MAX_ENTRIES = 100
|
|
21
|
+
const MAX_CHARS = 33_554_432
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* How long a request may wait on an upstream call another request started.
|
|
25
|
+
* An older call is treated as stuck and a new one is made, so one hung socket
|
|
26
|
+
* cannot hold every later visitor.
|
|
27
|
+
*/
|
|
28
|
+
const IN_FLIGHT_JOIN_MS = 10_000
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The window, in whole seconds, a REST API data source caches its upstream
|
|
32
|
+
* response for — or `undefined` when caching is off, which keeps the emitted
|
|
33
|
+
* fetcher exactly what it was before this option existed.
|
|
34
|
+
*/
|
|
35
|
+
export const resolveRESTAPICacheTTLSeconds = (
|
|
36
|
+
cache: RESTAPICacheConfig | undefined
|
|
37
|
+
): number | undefined => {
|
|
38
|
+
if (!cache || typeof cache !== 'object' || cache.enabled !== true) {
|
|
39
|
+
return undefined
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { ttlSeconds } = cache
|
|
43
|
+
const seconds =
|
|
44
|
+
typeof ttlSeconds === 'number' && Number.isFinite(ttlSeconds) && ttlSeconds > 0
|
|
45
|
+
? Math.max(1, Math.floor(ttlSeconds))
|
|
46
|
+
: DataCache.DEFAULT_CACHE_TTL_SECONDS
|
|
47
|
+
|
|
48
|
+
return Math.min(seconds, MAX_TTL_SECONDS)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Runtime helpers of a cached REST API fetcher.
|
|
53
|
+
*
|
|
54
|
+
* The cache sits under the handler, around the upstream `fetch` only: every
|
|
55
|
+
* caller — the API route, `getStaticProps` through `fetchData`, the count
|
|
56
|
+
* route, the global-state route — asks the same URL, and search, filters, sort
|
|
57
|
+
* and paging are applied to the cached payload afterwards. One upstream call
|
|
58
|
+
* per window therefore serves every page, filter and visitor.
|
|
59
|
+
*
|
|
60
|
+
* - The store lives on `globalThis`, so every data-source module in the
|
|
61
|
+
* process (each API route is its own bundle) shares it. On a serverless host
|
|
62
|
+
* that is one store per warm instance; the `s-maxage` header below is what
|
|
63
|
+
* carries the window to the CDN in front of all of them.
|
|
64
|
+
* - Entries are keyed by the whole upstream request (method, URL, headers,
|
|
65
|
+
* body) and hold the response TEXT: every reader parses its own copy, so a
|
|
66
|
+
* handler that sorts in place can never reorder another request's rows.
|
|
67
|
+
* - Freshness is judged by the READER's window, so two data sources calling
|
|
68
|
+
* the same URL with different windows each get what they asked for.
|
|
69
|
+
* - Only a 2xx JSON response is kept. Errors, non-JSON bodies and network
|
|
70
|
+
* failures reach the caller as before and are retried by the next request.
|
|
71
|
+
*
|
|
72
|
+
* Emitted inside a template literal: no backticks or template placeholders in
|
|
73
|
+
* here, not even in comments.
|
|
74
|
+
*/
|
|
75
|
+
export const generateRESTAPIResponseCacheCode = (ttlSeconds: number): string => {
|
|
76
|
+
return `const REST_API_CACHE_TTL_MS = ${ttlSeconds * 1000}
|
|
77
|
+
const REST_API_CACHE_MAX_ENTRIES = ${MAX_ENTRIES}
|
|
78
|
+
const REST_API_CACHE_MAX_CHARS = ${MAX_CHARS}
|
|
79
|
+
const REST_API_IN_FLIGHT_JOIN_MS = ${IN_FLIGHT_JOIN_MS}
|
|
80
|
+
|
|
81
|
+
// One store per server process, shared by every REST API data source module.
|
|
82
|
+
if (!globalThis.__tqRestApiCache) {
|
|
83
|
+
globalThis.__tqRestApiCache = { entries: new Map(), inFlight: new Map(), chars: 0 }
|
|
84
|
+
}
|
|
85
|
+
const restApiCache = globalThis.__tqRestApiCache
|
|
86
|
+
|
|
87
|
+
function restApiCacheKey(url, options) {
|
|
88
|
+
return JSON.stringify([options.method, url, options.headers, options.body === undefined ? null : options.body])
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function forgetRestApiResponse(key) {
|
|
92
|
+
const entry = restApiCache.entries.get(key)
|
|
93
|
+
if (entry) {
|
|
94
|
+
restApiCache.entries.delete(key)
|
|
95
|
+
restApiCache.chars -= entry.bodyText.length
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function rememberRestApiResponse(key, entry) {
|
|
100
|
+
forgetRestApiResponse(key)
|
|
101
|
+
if (entry.bodyText.length > REST_API_CACHE_MAX_CHARS) {
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
restApiCache.entries.set(key, entry)
|
|
105
|
+
restApiCache.chars += entry.bodyText.length
|
|
106
|
+
// A Map iterates in insertion order and every hit re-inserts its entry, so
|
|
107
|
+
// the first key is always the least recently used one.
|
|
108
|
+
while (
|
|
109
|
+
restApiCache.entries.size > REST_API_CACHE_MAX_ENTRIES ||
|
|
110
|
+
restApiCache.chars > REST_API_CACHE_MAX_CHARS
|
|
111
|
+
) {
|
|
112
|
+
forgetRestApiResponse(restApiCache.entries.keys().next().value)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function requestRestApi(url, options) {
|
|
117
|
+
const response = await fetch(url, options)
|
|
118
|
+
if (!response.ok) {
|
|
119
|
+
return { ok: false, status: response.status, statusText: response.statusText }
|
|
120
|
+
}
|
|
121
|
+
const bodyText = await response.text()
|
|
122
|
+
// Parsed once here so a body that is not JSON fails this request, as
|
|
123
|
+
// response.json() would, instead of being cached and failing every request
|
|
124
|
+
// until the window ends.
|
|
125
|
+
JSON.parse(bodyText)
|
|
126
|
+
return {
|
|
127
|
+
ok: true,
|
|
128
|
+
status: response.status,
|
|
129
|
+
statusText: response.statusText,
|
|
130
|
+
bodyText,
|
|
131
|
+
fetchedAt: Date.now(),
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function fetchRestApiWithCache(url, options) {
|
|
136
|
+
const key = restApiCacheKey(url, options)
|
|
137
|
+
const now = Date.now()
|
|
138
|
+
|
|
139
|
+
const cached = restApiCache.entries.get(key)
|
|
140
|
+
if (cached && now >= cached.fetchedAt && now - cached.fetchedAt < REST_API_CACHE_TTL_MS) {
|
|
141
|
+
restApiCache.entries.delete(key)
|
|
142
|
+
restApiCache.entries.set(key, cached)
|
|
143
|
+
return Promise.resolve(cached)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Concurrent misses share one upstream call instead of each making their own.
|
|
147
|
+
const pending = restApiCache.inFlight.get(key)
|
|
148
|
+
if (pending && now - pending.startedAt < REST_API_IN_FLIGHT_JOIN_MS) {
|
|
149
|
+
return pending.promise
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const flight = { startedAt: now, promise: null }
|
|
153
|
+
const release = () => {
|
|
154
|
+
if (restApiCache.inFlight.get(key) === flight) {
|
|
155
|
+
restApiCache.inFlight.delete(key)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
flight.promise = requestRestApi(url, options).then(
|
|
159
|
+
(result) => {
|
|
160
|
+
release()
|
|
161
|
+
if (result.ok) {
|
|
162
|
+
rememberRestApiResponse(key, result)
|
|
163
|
+
}
|
|
164
|
+
return result
|
|
165
|
+
},
|
|
166
|
+
(error) => {
|
|
167
|
+
release()
|
|
168
|
+
throw error
|
|
169
|
+
}
|
|
170
|
+
)
|
|
171
|
+
restApiCache.inFlight.set(key, flight)
|
|
172
|
+
return flight.promise
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Lets the CDN answer this exact URL until the upstream response it was built
|
|
176
|
+
// from expires, so neither layer serves it longer than the window. Only a real
|
|
177
|
+
// GET response gets the header: fetchData, fetchCount and tqWithCache call the
|
|
178
|
+
// handler with a bare { status, json } object.
|
|
179
|
+
function applyRestApiCacheHeaders(req, res, fetchedAt) {
|
|
180
|
+
if (!req || req.method !== 'GET' || !res || typeof res.setHeader !== 'function') {
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
const remainingSeconds = Math.floor((fetchedAt + REST_API_CACHE_TTL_MS - Date.now()) / 1000)
|
|
184
|
+
if (remainingSeconds > 0) {
|
|
185
|
+
res.setHeader('Cache-Control', 'public, max-age=0, s-maxage=' + remainingSeconds)
|
|
186
|
+
}
|
|
187
|
+
}`
|
|
188
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { SessionCookieResolver, TableAccess } from '@teleporthq/teleport-shared'
|
|
2
|
+
import { generateBrowserRowAccessCall, generateBrowserRowPolicyCode } from './browser-row-policy'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The money-table guard for a generated per-table read route
|
|
6
|
+
* (`/api/teleport-<table>-<source>` and its `-count` twin).
|
|
7
|
+
*
|
|
8
|
+
* Every table a page binds gets such a route, and the route serves the whole
|
|
9
|
+
* table — plus any single SELECT through `?rawQuery=` — to whoever asks. For a
|
|
10
|
+
* catalogue that is the product; for gift cards and voucher codes it is theft.
|
|
11
|
+
* The guard refuses a protected table (see `TableAccess.PROTECTED_TABLES`)
|
|
12
|
+
* unless the caller is a server segment presenting the app secret or a
|
|
13
|
+
* signed-in member of a trusted role — the generated admin, whose list pages
|
|
14
|
+
* are the one browser reader these tables have. It is followed by the
|
|
15
|
+
* browser row policy (see `browser-row-policy.ts`): unpublished rows and
|
|
16
|
+
* private columns of the public tables, and the admin-only tables.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Declares the session resolver and the `__ta*` guard once per module. */
|
|
20
|
+
export const generateTableAccessPreamble = (trustedReaderRoles: ReadonlyArray<string>): string => {
|
|
21
|
+
return `${SessionCookieResolver.generateCommonJsSessionTokenResolverCode()}
|
|
22
|
+
${TableAccess.generateTableAccessHelperCode({ trustedReaderRoles })}
|
|
23
|
+
${generateBrowserRowPolicyCode()}`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The early return at the top of a handler, before the client connects:
|
|
28
|
+
* `tableName` is the module's table, `rawQueryExpr` the request's raw statement
|
|
29
|
+
* (or `null` where the handler accepts none).
|
|
30
|
+
*/
|
|
31
|
+
export const generateReadGuardCall = (tableName: string, rawQueryExpr: string | null): string => {
|
|
32
|
+
return ` const __access = await __taGuardRead(req, ${JSON.stringify(tableName)}, ${
|
|
33
|
+
rawQueryExpr || 'null'
|
|
34
|
+
})
|
|
35
|
+
if (__access) {
|
|
36
|
+
return res.status(__access.status).json({ success: false, error: __access.message, timestamp: Date.now() })
|
|
37
|
+
}
|
|
38
|
+
${generateBrowserRowAccessCall(tableName, rawQueryExpr)}`
|
|
39
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
FileType,
|
|
5
5
|
ChunkType,
|
|
6
6
|
UIDLComponentSEO,
|
|
7
|
+
UIDLDependency,
|
|
7
8
|
UIDLDynamicReference,
|
|
8
9
|
} from '@teleporthq/teleport-types'
|
|
9
10
|
import { UIDLUtils, StringUtils } from '@teleporthq/teleport-shared'
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
import { createNextArrayMapperPaginationPlugin } from './pagination-plugin'
|
|
17
18
|
import { buildProductTransformOptions } from './transformations'
|
|
18
19
|
import { DATA_SOURCE_ISR_REVALIDATE_SECONDS } from './isr'
|
|
20
|
+
import { ensureRouterDeclaration } from './request-locale'
|
|
19
21
|
import * as types from '@babel/types'
|
|
20
22
|
|
|
21
23
|
interface SearchConfig {
|
|
@@ -55,6 +57,41 @@ interface WrapContext {
|
|
|
55
57
|
// from user-defined or standard renderPropIdentifier values
|
|
56
58
|
const WRAPPED_DS_EXPR_PREFIX = '__dsExpr_'
|
|
57
59
|
|
|
60
|
+
/**
|
|
61
|
+
* The statements of the component function the `jsx-component` chunk declares
|
|
62
|
+
* (`const Page = (props) => { … }`), or `null` when the chunk holds another
|
|
63
|
+
* shape — a test may hand the plugin a bare element.
|
|
64
|
+
*/
|
|
65
|
+
const findComponentBody = (componentChunk: { content: unknown }): types.Statement[] | null => {
|
|
66
|
+
const content = componentChunk.content as types.Node | undefined
|
|
67
|
+
if (content?.type !== 'VariableDeclaration') {
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
const init = content.declarations[0]?.init
|
|
71
|
+
if (
|
|
72
|
+
(init?.type !== 'ArrowFunctionExpression' && init?.type !== 'FunctionExpression') ||
|
|
73
|
+
init.body.type !== 'BlockStatement'
|
|
74
|
+
) {
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
return init.body.body
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A client-side fetch in a localized project reads `router.locale` (see
|
|
82
|
+
* `extractDataSourceIntoNextAPIFolder`), so the router has to be declared in
|
|
83
|
+
* the component that fetches.
|
|
84
|
+
*/
|
|
85
|
+
const ensureRouterForClientFetches = (
|
|
86
|
+
componentChunk: { content: unknown },
|
|
87
|
+
dependencies: Record<string, UIDLDependency>
|
|
88
|
+
): void => {
|
|
89
|
+
const body = findComponentBody(componentChunk)
|
|
90
|
+
if (body) {
|
|
91
|
+
ensureRouterDeclaration(body, dependencies)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
58
95
|
function containsDataSourceDataReference(astNode: any): boolean {
|
|
59
96
|
if (!astNode || typeof astNode !== 'object') {
|
|
60
97
|
return false
|
|
@@ -490,6 +527,11 @@ export const createNextPagesDataSourcePlugin: ComponentPluginFactory<{}> = () =>
|
|
|
490
527
|
|
|
491
528
|
let getStaticPropsChunk = chunks.find((chunk) => chunk.name === 'getStaticProps')
|
|
492
529
|
|
|
530
|
+
const transformOptions = buildProductTransformOptions(options)
|
|
531
|
+
// Whether a provider on this page fetches client-side — in a localized
|
|
532
|
+
// project that fetch reads `router.locale`.
|
|
533
|
+
let wiredClientFetch = false
|
|
534
|
+
|
|
493
535
|
// Track the first data source info for wrapping dataSourceData expressions
|
|
494
536
|
let firstDataSourceInfo: DataSourceInfo | null = null
|
|
495
537
|
// Track fetcher import name for wrapped providers
|
|
@@ -609,7 +651,7 @@ export const createNextPagesDataSourcePlugin: ComponentPluginFactory<{}> = () =>
|
|
|
609
651
|
dependencies,
|
|
610
652
|
dynamicRouteAttr,
|
|
611
653
|
uidl.outputOptions?.folderPath,
|
|
612
|
-
|
|
654
|
+
transformOptions
|
|
613
655
|
)
|
|
614
656
|
|
|
615
657
|
if (result.success && result.chunk) {
|
|
@@ -641,17 +683,23 @@ export const createNextPagesDataSourcePlugin: ComponentPluginFactory<{}> = () =>
|
|
|
641
683
|
}
|
|
642
684
|
}
|
|
643
685
|
}
|
|
644
|
-
} else
|
|
686
|
+
} else if (
|
|
645
687
|
extractDataSourceIntoNextAPIFolder(
|
|
646
688
|
dataSourceNode,
|
|
647
689
|
dataSources,
|
|
648
690
|
componentChunk,
|
|
649
691
|
options.extractedResources,
|
|
650
|
-
|
|
692
|
+
transformOptions
|
|
651
693
|
)
|
|
694
|
+
) {
|
|
695
|
+
wiredClientFetch = true
|
|
652
696
|
}
|
|
653
697
|
})
|
|
654
698
|
|
|
699
|
+
if (wiredClientFetch && transformOptions.localization) {
|
|
700
|
+
ensureRouterForClientFetches(componentChunk, dependencies)
|
|
701
|
+
}
|
|
702
|
+
|
|
655
703
|
// After processing all data source nodes, wrap any element props containing dataSourceData
|
|
656
704
|
// expressions in a DataProvider
|
|
657
705
|
if (firstDataSourceInfo && componentChunk.content) {
|
|
@@ -1207,6 +1255,11 @@ export const createNextComponentDataSourcePlugin: ComponentPluginFactory<{}> = (
|
|
|
1207
1255
|
return structure
|
|
1208
1256
|
}
|
|
1209
1257
|
|
|
1258
|
+
const transformOptions = buildProductTransformOptions(options)
|
|
1259
|
+
// Whether a provider in this component fetches client-side — in a
|
|
1260
|
+
// localized project that fetch reads `router.locale`.
|
|
1261
|
+
let wiredClientFetch = false
|
|
1262
|
+
|
|
1210
1263
|
// Track the first data source info for wrapping dataSourceData expressions
|
|
1211
1264
|
let firstDataSourceInfo: DataSourceInfo | null = null
|
|
1212
1265
|
|
|
@@ -1264,15 +1317,23 @@ export const createNextComponentDataSourcePlugin: ComponentPluginFactory<{}> = (
|
|
|
1264
1317
|
}
|
|
1265
1318
|
}
|
|
1266
1319
|
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1320
|
+
if (
|
|
1321
|
+
extractDataSourceIntoNextAPIFolder(
|
|
1322
|
+
dataSourceNode,
|
|
1323
|
+
dataSources,
|
|
1324
|
+
componentChunk,
|
|
1325
|
+
options.extractedResources,
|
|
1326
|
+
transformOptions
|
|
1327
|
+
)
|
|
1328
|
+
) {
|
|
1329
|
+
wiredClientFetch = true
|
|
1330
|
+
}
|
|
1274
1331
|
})
|
|
1275
1332
|
|
|
1333
|
+
if (wiredClientFetch && transformOptions.localization) {
|
|
1334
|
+
ensureRouterForClientFetches(componentChunk, dependencies)
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1276
1337
|
// If no data source nodes were found but there are dataSources available,
|
|
1277
1338
|
// use the first available dataSource to create firstDataSourceInfo
|
|
1278
1339
|
// This handles the case where a component has element props with dataSourceData expressions
|
|
@@ -1355,7 +1416,7 @@ export const createNextComponentDataSourcePlugin: ComponentPluginFactory<{}> = (
|
|
|
1355
1416
|
dataSource,
|
|
1356
1417
|
firstDataSourceInfo.tableName,
|
|
1357
1418
|
false,
|
|
1358
|
-
|
|
1419
|
+
transformOptions
|
|
1359
1420
|
)
|
|
1360
1421
|
options.extractedResources[`utils/${fileName}`] = {
|
|
1361
1422
|
fileName,
|
|
@@ -1399,8 +1460,15 @@ export * from './utils'
|
|
|
1399
1460
|
export * from './array-mapper-pagination'
|
|
1400
1461
|
export * from './pagination-plugin'
|
|
1401
1462
|
export * from './count-fetchers'
|
|
1463
|
+
export * from './product-price-sort'
|
|
1464
|
+
export * from './product-filter-fields'
|
|
1402
1465
|
// The product transform's build-time options (category taxonomy + storefront
|
|
1403
1466
|
// tax rate) — every package that emits a data-source fetcher has to derive
|
|
1404
1467
|
// them the same way.
|
|
1405
1468
|
export { buildProductTransformOptions } from './transformations'
|
|
1406
1469
|
export type { EcommerceProductTransformOptions, EntityTransformOptions } from './transformations'
|
|
1470
|
+
// How a fetch says which language it is for, and when a project needs it to —
|
|
1471
|
+
// shared with every other package that emits a data-source fetch.
|
|
1472
|
+
export { isLocalizedProject, resolveContentLocalization } from './content-localization'
|
|
1473
|
+
export type { ContentLocalization } from './content-localization'
|
|
1474
|
+
export { REQUEST_LOCALE_PARAM, buildServerLocaleParam } from './request-locale'
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as types from '@babel/types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Where a paginated/searchable list's rows sit inside the payload its data
|
|
5
|
+
* source returns, read off the repeater's `source` expression.
|
|
6
|
+
*
|
|
7
|
+
* A REST API or JavaScript source may answer `{ templates: [...], total }`, and
|
|
8
|
+
* the editor binds the list to `All_Templates_data_data?.templates || []`. The
|
|
9
|
+
* generated fetcher only searches, filters, sorts and slices a payload that IS
|
|
10
|
+
* an array, so without this every row came back on every page (the canvas
|
|
11
|
+
* slices on the client and looked right) and the count measured the wrapper as
|
|
12
|
+
* 0 rows, disabling "Next".
|
|
13
|
+
*
|
|
14
|
+
* Returns undefined when the list is bound to the payload itself, or when the
|
|
15
|
+
* source is anything other than a plain property chain off the data source's
|
|
16
|
+
* render prop — an expression this cannot read is left exactly as before.
|
|
17
|
+
*/
|
|
18
|
+
const SEGMENT_RE =
|
|
19
|
+
/^(?:\?\.([A-Za-z_$][\w$]*)|\.([A-Za-z_$][\w$]*)|(?:\?\.)?\[\s*(?:"([^"]*)"|'([^']*)')\s*\])/
|
|
20
|
+
|
|
21
|
+
const PATH_AWARE_SOURCE_TYPES = new Set(['rest-api', 'javascript'])
|
|
22
|
+
|
|
23
|
+
export const extractItemsPath = (
|
|
24
|
+
source: unknown,
|
|
25
|
+
dataSourceIdentifier: string,
|
|
26
|
+
dataSourceType: string
|
|
27
|
+
): string[] | undefined => {
|
|
28
|
+
if (typeof source !== 'string' || !dataSourceIdentifier) {
|
|
29
|
+
return undefined
|
|
30
|
+
}
|
|
31
|
+
if (!PATH_AWARE_SOURCE_TYPES.has(dataSourceType)) {
|
|
32
|
+
return undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let rest = source
|
|
36
|
+
.trim()
|
|
37
|
+
.replace(/\s*\|\|\s*\[\s*\]$/, '')
|
|
38
|
+
.trim()
|
|
39
|
+
if (!rest.startsWith(dataSourceIdentifier)) {
|
|
40
|
+
return undefined
|
|
41
|
+
}
|
|
42
|
+
rest = rest.slice(dataSourceIdentifier.length)
|
|
43
|
+
|
|
44
|
+
const path: string[] = []
|
|
45
|
+
while (rest.length > 0) {
|
|
46
|
+
const match = rest.match(SEGMENT_RE)
|
|
47
|
+
if (!match) {
|
|
48
|
+
return undefined
|
|
49
|
+
}
|
|
50
|
+
path.push(match[1] ?? match[2] ?? match[3] ?? match[4])
|
|
51
|
+
rest = rest.slice(match[0].length)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return path.length > 0 ? path : undefined
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Push `<key>: '["templates"]'` onto a params object. Fetch calls send it as
|
|
59
|
+
* `itemsPath` (process the inner array, keep the wrapper the page reads back
|
|
60
|
+
* through); count calls send it as `collectionPath`, so the count handler
|
|
61
|
+
* measures the inner array instead of the wrapper.
|
|
62
|
+
*/
|
|
63
|
+
export const appendItemsPathParam = (
|
|
64
|
+
paramsProps: types.ObjectProperty[],
|
|
65
|
+
itemsPath: string[] | undefined,
|
|
66
|
+
key: 'itemsPath' | 'collectionPath'
|
|
67
|
+
): void => {
|
|
68
|
+
if (!itemsPath || itemsPath.length === 0) {
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
paramsProps.push(
|
|
72
|
+
types.objectProperty(types.identifier(key), types.stringLiteral(JSON.stringify(itemsPath)))
|
|
73
|
+
)
|
|
74
|
+
}
|
package/src/loading-state.ts
CHANGED
|
@@ -67,6 +67,19 @@ export interface LoadingStateVars {
|
|
|
67
67
|
* stale rows back for the rest of the second request.
|
|
68
68
|
*/
|
|
69
69
|
inFlightRefVar: string
|
|
70
|
+
/**
|
|
71
|
+
* Ref counting the requests STARTED, so a response can tell whether a newer
|
|
72
|
+
* request has overtaken it. `DataProvider` keeps whatever resolves last:
|
|
73
|
+
* two controls changed in quick succession (a "Clear all" emptying several
|
|
74
|
+
* filter states, one state write after another) start two requests, and
|
|
75
|
+
* when the older, slower one settles after the newer it painted the OLDER
|
|
76
|
+
* rows over the newer — a list showing two products for a URL that filters
|
|
77
|
+
* nothing. Each request notes its sequence number and, if overtaken, hands
|
|
78
|
+
* over the newest settled rows instead of its own.
|
|
79
|
+
*/
|
|
80
|
+
fetchSeqRefVar: string
|
|
81
|
+
/** Ref holding the rows of the newest request that settled — what an overtaken one resolves with. */
|
|
82
|
+
latestDataRefVar: string
|
|
70
83
|
}
|
|
71
84
|
|
|
72
85
|
export function getLoadingStateVars(index: number): LoadingStateVars {
|
|
@@ -74,14 +87,18 @@ export function getLoadingStateVars(index: number): LoadingStateVars {
|
|
|
74
87
|
isFetchingVar: `ds_${index}_isFetching`,
|
|
75
88
|
setIsFetchingVar: `setDs_${index}_isFetching`,
|
|
76
89
|
inFlightRefVar: `ds_${index}_fetchesInFlight`,
|
|
90
|
+
fetchSeqRefVar: `ds_${index}_fetchSeq`,
|
|
91
|
+
latestDataRefVar: `ds_${index}_latestData`,
|
|
77
92
|
}
|
|
78
93
|
}
|
|
79
94
|
|
|
80
95
|
/**
|
|
81
96
|
* `const ds_N_fetchesInFlight = useRef(0)`
|
|
97
|
+
* `const ds_N_fetchSeq = useRef(0)`
|
|
98
|
+
* `const ds_N_latestData = useRef(undefined)`
|
|
82
99
|
* `const [ds_N_isFetching, setDs_N_isFetching] = useState(false)`
|
|
83
100
|
*
|
|
84
|
-
*
|
|
101
|
+
* All are stable across renders (ref objects and a `useState` setter), which
|
|
85
102
|
* is what lets the wrapped `fetchData` keep its empty `useCallback` dependency
|
|
86
103
|
* array — a changing `fetchData` identity would retrigger the provider's fetch
|
|
87
104
|
* effect on every render.
|
|
@@ -94,6 +111,18 @@ export function buildLoadingStateDeclarations(vars: LoadingStateVars): types.Sta
|
|
|
94
111
|
types.callExpression(types.identifier('useRef'), [types.numericLiteral(0)])
|
|
95
112
|
),
|
|
96
113
|
]),
|
|
114
|
+
types.variableDeclaration('const', [
|
|
115
|
+
types.variableDeclarator(
|
|
116
|
+
types.identifier(vars.fetchSeqRefVar),
|
|
117
|
+
types.callExpression(types.identifier('useRef'), [types.numericLiteral(0)])
|
|
118
|
+
),
|
|
119
|
+
]),
|
|
120
|
+
types.variableDeclaration('const', [
|
|
121
|
+
types.variableDeclarator(
|
|
122
|
+
types.identifier(vars.latestDataRefVar),
|
|
123
|
+
types.callExpression(types.identifier('useRef'), [types.identifier('undefined')])
|
|
124
|
+
),
|
|
125
|
+
]),
|
|
97
126
|
types.variableDeclaration('const', [
|
|
98
127
|
types.variableDeclarator(
|
|
99
128
|
types.arrayPattern([
|
|
@@ -183,10 +212,54 @@ function injectTrackingIntoCachedBody(body: types.BlockStatement, vars: LoadingS
|
|
|
183
212
|
|
|
184
213
|
const tracked = buildTrackedFetchBody(networkChain.statement.argument as types.Expression, vars)
|
|
185
214
|
body.body.splice(networkChain.index, 1, ...tracked.body)
|
|
215
|
+
markCacheHitAsNewest(body, vars)
|
|
186
216
|
|
|
187
217
|
return true
|
|
188
218
|
}
|
|
189
219
|
|
|
220
|
+
/**
|
|
221
|
+
* A cache hit answers synchronously with the rows of THIS request, so it is
|
|
222
|
+
* the newest settled result too: it advances the sequence and records its rows,
|
|
223
|
+
* or an older network request still in flight would resolve after it and paint
|
|
224
|
+
* over it. Adds the two statements at the head of the hit branch — before its
|
|
225
|
+
* `return Promise.resolve(__tqHit)` — and nothing else, so the flag the branch
|
|
226
|
+
* deliberately never raises stays untouched.
|
|
227
|
+
*/
|
|
228
|
+
function markCacheHitAsNewest(body: types.BlockStatement, vars: LoadingStateVars): void {
|
|
229
|
+
const hit = body.body.find(
|
|
230
|
+
(statement): statement is types.IfStatement =>
|
|
231
|
+
statement.type === 'IfStatement' &&
|
|
232
|
+
statement.test.type === 'BinaryExpression' &&
|
|
233
|
+
statement.test.left.type === 'Identifier' &&
|
|
234
|
+
statement.test.left.name === CACHE_HIT_IDENTIFIER &&
|
|
235
|
+
statement.consequent.type === 'BlockStatement'
|
|
236
|
+
)
|
|
237
|
+
if (!hit || hit.consequent.type !== 'BlockStatement') {
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
hit.consequent.body.unshift(
|
|
241
|
+
types.expressionStatement(types.updateExpression('++', refCurrent(vars.fetchSeqRefVar), true)),
|
|
242
|
+
types.expressionStatement(
|
|
243
|
+
types.assignmentExpression(
|
|
244
|
+
'=',
|
|
245
|
+
refCurrent(vars.latestDataRefVar),
|
|
246
|
+
types.identifier(CACHE_HIT_IDENTIFIER)
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** The identifier the cache wiring binds a hit to — see `cache/ast.ts`. */
|
|
253
|
+
const CACHE_HIT_IDENTIFIER = '__tqHit'
|
|
254
|
+
/** The rows a tracked request resolved with, inside its own `then`. */
|
|
255
|
+
const TRACKED_ROWS_IDENTIFIER = '__tqRows'
|
|
256
|
+
/** This request's sequence number. */
|
|
257
|
+
const TRACKED_SEQ_IDENTIFIER = '__tqSeq'
|
|
258
|
+
|
|
259
|
+
function refCurrent(refVar: string): types.MemberExpression {
|
|
260
|
+
return types.memberExpression(types.identifier(refVar), types.identifier('current'))
|
|
261
|
+
}
|
|
262
|
+
|
|
190
263
|
// tslint:disable-next-line:no-any
|
|
191
264
|
function containsIdentifier(node: any, name: string): boolean {
|
|
192
265
|
if (!node || typeof node !== 'object') {
|
|
@@ -257,9 +330,16 @@ function findMemoizedFetchDataArrow(
|
|
|
257
330
|
* Turns `(params) => fetch(...).then(...)` into
|
|
258
331
|
*
|
|
259
332
|
* (params) => {
|
|
333
|
+
* const __tqSeq = ++ds_N_fetchSeq.current
|
|
260
334
|
* ds_N_fetchesInFlight.current += 1
|
|
261
335
|
* setDs_N_isFetching(true)
|
|
262
|
-
* return fetch(...).then(...).
|
|
336
|
+
* return fetch(...).then(...).then((__tqRows) => {
|
|
337
|
+
* if (__tqSeq !== ds_N_fetchSeq.current) {
|
|
338
|
+
* return ds_N_latestData.current !== undefined ? ds_N_latestData.current : __tqRows
|
|
339
|
+
* }
|
|
340
|
+
* ds_N_latestData.current = __tqRows
|
|
341
|
+
* return __tqRows
|
|
342
|
+
* }).finally(() => {
|
|
263
343
|
* ds_N_fetchesInFlight.current -= 1
|
|
264
344
|
* if (ds_N_fetchesInFlight.current <= 0) {
|
|
265
345
|
* ds_N_fetchesInFlight.current = 0
|
|
@@ -268,6 +348,12 @@ function findMemoizedFetchDataArrow(
|
|
|
268
348
|
* })
|
|
269
349
|
* }
|
|
270
350
|
*
|
|
351
|
+
* The `then` is the stale guard (see `fetchSeqRefVar`): a request overtaken by
|
|
352
|
+
* a newer one resolves with the newest settled rows rather than its own, so
|
|
353
|
+
* the provider — which keeps whatever resolves LAST — never paints older rows
|
|
354
|
+
* over newer ones. An overtaken request whose successor has not settled yet
|
|
355
|
+
* resolves with its own rows; the successor then replaces them.
|
|
356
|
+
*
|
|
271
357
|
* `finally` (rather than a `then` pair) keeps the flag honest when the request
|
|
272
358
|
* rejects: the provider switches to its error status and the loading state must
|
|
273
359
|
* not stay on screen. The counter is clamped at 0 so a late settle from a
|
|
@@ -317,7 +403,52 @@ function buildTrackedFetchBody(
|
|
|
317
403
|
])
|
|
318
404
|
)
|
|
319
405
|
|
|
406
|
+
const latestData = refCurrent(vars.latestDataRefVar)
|
|
407
|
+
const staleGuard = types.arrowFunctionExpression(
|
|
408
|
+
[types.identifier(TRACKED_ROWS_IDENTIFIER)],
|
|
409
|
+
types.blockStatement([
|
|
410
|
+
types.ifStatement(
|
|
411
|
+
types.binaryExpression(
|
|
412
|
+
'!==',
|
|
413
|
+
types.identifier(TRACKED_SEQ_IDENTIFIER),
|
|
414
|
+
refCurrent(vars.fetchSeqRefVar)
|
|
415
|
+
),
|
|
416
|
+
types.blockStatement([
|
|
417
|
+
types.returnStatement(
|
|
418
|
+
types.conditionalExpression(
|
|
419
|
+
types.binaryExpression(
|
|
420
|
+
'!==',
|
|
421
|
+
types.cloneNode(latestData, true),
|
|
422
|
+
types.identifier('undefined')
|
|
423
|
+
),
|
|
424
|
+
types.cloneNode(latestData, true),
|
|
425
|
+
types.identifier(TRACKED_ROWS_IDENTIFIER)
|
|
426
|
+
)
|
|
427
|
+
),
|
|
428
|
+
])
|
|
429
|
+
),
|
|
430
|
+
types.expressionStatement(
|
|
431
|
+
types.assignmentExpression(
|
|
432
|
+
'=',
|
|
433
|
+
types.cloneNode(latestData, true),
|
|
434
|
+
types.identifier(TRACKED_ROWS_IDENTIFIER)
|
|
435
|
+
)
|
|
436
|
+
),
|
|
437
|
+
types.returnStatement(types.identifier(TRACKED_ROWS_IDENTIFIER)),
|
|
438
|
+
])
|
|
439
|
+
)
|
|
440
|
+
const guardedFetch = types.callExpression(
|
|
441
|
+
types.memberExpression(fetchExpression, types.identifier('then')),
|
|
442
|
+
[staleGuard]
|
|
443
|
+
)
|
|
444
|
+
|
|
320
445
|
return types.blockStatement([
|
|
446
|
+
types.variableDeclaration('const', [
|
|
447
|
+
types.variableDeclarator(
|
|
448
|
+
types.identifier(TRACKED_SEQ_IDENTIFIER),
|
|
449
|
+
types.updateExpression('++', refCurrent(vars.fetchSeqRefVar), true)
|
|
450
|
+
),
|
|
451
|
+
]),
|
|
321
452
|
types.expressionStatement(
|
|
322
453
|
types.assignmentExpression(
|
|
323
454
|
'+=',
|
|
@@ -329,7 +460,7 @@ function buildTrackedFetchBody(
|
|
|
329
460
|
types.callExpression(types.identifier(vars.setIsFetchingVar), [types.booleanLiteral(true)])
|
|
330
461
|
),
|
|
331
462
|
types.returnStatement(
|
|
332
|
-
types.callExpression(types.memberExpression(
|
|
463
|
+
types.callExpression(types.memberExpression(guardedFetch, types.identifier('finally')), [
|
|
333
464
|
settleHandler,
|
|
334
465
|
])
|
|
335
466
|
),
|