@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
package/src/fetchers/teleport.ts
CHANGED
|
@@ -16,6 +16,12 @@ import {
|
|
|
16
16
|
generateSortFieldSqlHelper,
|
|
17
17
|
generateSortTiebreakSql,
|
|
18
18
|
} from '../product-price-sort'
|
|
19
|
+
import { generateProductFilterClauseHelper } from '../product-filter-fields'
|
|
20
|
+
import { collectLocalizedColumns, generateLocalizedColumnsHelper } from '../localized-columns'
|
|
21
|
+
import { REQUEST_LOCALE_PARAM } from '../request-locale'
|
|
22
|
+
import { generateReadGuardCall, generateTableAccessPreamble } from './utils/table-access-guard'
|
|
23
|
+
import { generateRequestSqlGuardsCode } from './utils/request-sql-guards'
|
|
24
|
+
import { generateArrayOverlapSqlCode } from '../array-overlap-sql'
|
|
19
25
|
|
|
20
26
|
interface TeleportDBConfig {
|
|
21
27
|
host?: string
|
|
@@ -27,6 +33,8 @@ interface TeleportDBConfig {
|
|
|
27
33
|
ssl?: boolean | { ca?: string; cert?: string; key?: string; rejectUnauthorized?: boolean }
|
|
28
34
|
sslConfig?: { ca?: string; cert?: string; key?: string; rejectUnauthorized?: boolean }
|
|
29
35
|
options?: { schema?: string }
|
|
36
|
+
/** The provisioned schema, as the editor recorded it — the source of the per-language columns. */
|
|
37
|
+
selectedTables?: Record<string, { columns?: Array<{ name?: string }> }>
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
const DEFAULT_ENV_KEYS = {
|
|
@@ -62,6 +70,10 @@ export const generateTeleportFetcher = (
|
|
|
62
70
|
): string => {
|
|
63
71
|
const dbConfig = config as TeleportDBConfig
|
|
64
72
|
const schema = dbConfig.options?.schema
|
|
73
|
+
const columnNames = (dbConfig.selectedTables?.[tableName]?.columns || [])
|
|
74
|
+
.map((column) => column.name)
|
|
75
|
+
.filter((name): name is string => typeof name === 'string')
|
|
76
|
+
const localizedColumns = collectLocalizedColumns(columnNames, transformOptions.localization)
|
|
65
77
|
|
|
66
78
|
const hostRef = resolveEnvReference(dbConfig.host, DEFAULT_ENV_KEYS.host)
|
|
67
79
|
const portRef = resolveEnvReference(dbConfig.port, DEFAULT_ENV_KEYS.port)
|
|
@@ -95,6 +107,7 @@ export const generateTeleportFetcher = (
|
|
|
95
107
|
|
|
96
108
|
return `import { Client } from 'pg'
|
|
97
109
|
|
|
110
|
+
${generateTableAccessPreamble(transformOptions.trustedReaderRoles || [])}
|
|
98
111
|
function normalizePostgresConnectionString(connectionString) {
|
|
99
112
|
if (!connectionString || typeof connectionString !== 'string') return connectionString;
|
|
100
113
|
if (/^postgresql:\\/(?!\\/)/i.test(connectionString)) {
|
|
@@ -150,8 +163,10 @@ ${generateSafeJSONParseCode()}
|
|
|
150
163
|
${generateFilterTreeHelpersCode()}
|
|
151
164
|
|
|
152
165
|
${generateSearchEscapeHelpersCode()}
|
|
166
|
+
${generateArrayOverlapSqlCode()}
|
|
153
167
|
${getTransformationCode(tableName, transformOptions)}
|
|
154
|
-
${getTransformWrapperCode(tableName)}
|
|
168
|
+
${getTransformWrapperCode(tableName, transformOptions)}
|
|
169
|
+
${generateRequestSqlGuardsCode()}
|
|
155
170
|
const processFilters = (filters, conditions, queryParams, paramIndex) => {
|
|
156
171
|
if (!filters) return paramIndex
|
|
157
172
|
|
|
@@ -160,16 +175,34 @@ const processFilters = (filters, conditions, queryParams, paramIndex) => {
|
|
|
160
175
|
|
|
161
176
|
const buildCondition = (condition) => {
|
|
162
177
|
const field = condition.source
|
|
163
|
-
const
|
|
164
|
-
|
|
178
|
+
const normalizedCondition = normalizeInOperand(condition.operand, condition.destination)
|
|
179
|
+
if (normalizedCondition === null) return null
|
|
180
|
+
const value = normalizedCondition.value
|
|
181
|
+
const operand = normalizedCondition.operand
|
|
182
|
+
|
|
183
|
+
// The products table's virtual fields (effective price, rating bucket,
|
|
184
|
+
// on-sale flag, variant axes) are whole clauses, not columns - so they are
|
|
185
|
+
// answered before any branch below can interpolate the field name raw.
|
|
186
|
+
const productClause = productFilterClause(field, operand, value, (param) => {
|
|
187
|
+
queryParams.push(param)
|
|
188
|
+
return '$' + paramIndex++
|
|
189
|
+
})
|
|
190
|
+
if (productClause !== null) return productClause
|
|
191
|
+
|
|
192
|
+
// A translatable column is compared against the value in every language,
|
|
193
|
+
// so a localized slug resolves under any locale - see localized-columns.ts.
|
|
194
|
+
const localizedClause = localizedEqualityClause(field, operand, value, (param) => {
|
|
195
|
+
queryParams.push(param)
|
|
196
|
+
return '$' + paramIndex++
|
|
197
|
+
})
|
|
198
|
+
if (localizedClause !== null) return localizedClause
|
|
199
|
+
assertRequestColumn(field, 'filter field')
|
|
165
200
|
|
|
166
201
|
if (Array.isArray(value)) {
|
|
167
202
|
if (value.length === 0) return null
|
|
168
203
|
if (operand === 'array_overlap') {
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
// form) avoids the '?' placeholder-token ambiguity some drivers hit.
|
|
172
|
-
const clause = \`jsonb_exists_any(NULLIF(\${field}, '')::jsonb, $\${paramIndex}::text[])\`
|
|
204
|
+
// The row's list (see array-overlap-sql.ts) shares any value with the set.
|
|
205
|
+
const clause = arrayOverlapSql((localizedColumnList(field) || [field]), '$' + paramIndex)
|
|
173
206
|
queryParams.push(value.map((entry) => String(entry)))
|
|
174
207
|
paramIndex++
|
|
175
208
|
return clause
|
|
@@ -187,7 +220,7 @@ const processFilters = (filters, conditions, queryParams, paramIndex) => {
|
|
|
187
220
|
// ?categoryFilter=a,b,c) expands to multiple ids; one id stays one.
|
|
188
221
|
const overlapValues = String(value).split(',').map((entry) => entry.trim()).filter(Boolean)
|
|
189
222
|
if (overlapValues.length === 0) return null
|
|
190
|
-
const clause =
|
|
223
|
+
const clause = arrayOverlapSql((localizedColumnList(field) || [field]), '$' + paramIndex)
|
|
191
224
|
queryParams.push(overlapValues)
|
|
192
225
|
paramIndex++
|
|
193
226
|
return clause
|
|
@@ -219,6 +252,10 @@ ${generateSortFieldSqlHelper(tableName)}
|
|
|
219
252
|
|
|
220
253
|
${generateSortFallbackFieldHelper(tableName)}
|
|
221
254
|
|
|
255
|
+
${generateProductFilterClauseHelper(tableName)}
|
|
256
|
+
|
|
257
|
+
${generateLocalizedColumnsHelper(localizedColumns)}
|
|
258
|
+
|
|
222
259
|
// Matches DDL / dangerous statements the raw-query branch should refuse.
|
|
223
260
|
// Keep this list conservative — anything destructive or schema-changing is
|
|
224
261
|
// out of scope for a client-triggered fetch. SELECT and CTEs (WITH ...) are
|
|
@@ -263,6 +300,7 @@ function assertRawQuerySafe(rawQuery) {
|
|
|
263
300
|
}
|
|
264
301
|
|
|
265
302
|
export default async function handler(req, res) {
|
|
303
|
+
${generateReadGuardCall(tableName, 'req.query && req.query.rawQuery')}
|
|
266
304
|
const client = getClient()
|
|
267
305
|
|
|
268
306
|
try {
|
|
@@ -285,12 +323,18 @@ export default async function handler(req, res) {
|
|
|
285
323
|
const rawSafe = JSON.parse(JSON.stringify(rawPlain, dateReplacer))
|
|
286
324
|
return res.status(200).json({
|
|
287
325
|
success: true,
|
|
288
|
-
|
|
326
|
+
// Every row here ends up in a browser (a page's props or a client
|
|
327
|
+
// refetch), so no credential column survives, whatever joined it.
|
|
328
|
+
data: __taWithoutCredentials(rawSafe, null),
|
|
289
329
|
timestamp: Date.now()
|
|
290
330
|
})
|
|
291
331
|
}
|
|
292
332
|
|
|
293
333
|
const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset } = req.query
|
|
334
|
+
// A visitor never filters, sorts or searches by a column it is not served.
|
|
335
|
+
if (__rowPolicy) __brpAssertVisibleFields(__rowPolicy, req.query)
|
|
336
|
+
// The language the page is in - what a translatable sort orders by.
|
|
337
|
+
const requestLocale = typeof req.query.${REQUEST_LOCALE_PARAM} === 'string' ? req.query.${REQUEST_LOCALE_PARAM} : null
|
|
294
338
|
|
|
295
339
|
const conditions = []
|
|
296
340
|
const queryParams = []
|
|
@@ -323,6 +367,7 @@ export default async function handler(req, res) {
|
|
|
323
367
|
console.warn('Failed to fetch column names from information_schema:', schemaError.message)
|
|
324
368
|
}
|
|
325
369
|
}
|
|
370
|
+
if (__rowPolicy) columns = __brpVisibleColumns(__rowPolicy, columns)
|
|
326
371
|
|
|
327
372
|
if (columns.length > 0) {
|
|
328
373
|
const pattern = '%' + escapeLikePattern(query) + '%'
|
|
@@ -337,6 +382,7 @@ export default async function handler(req, res) {
|
|
|
337
382
|
}
|
|
338
383
|
|
|
339
384
|
paramIndex = processFilters(filters, conditions, queryParams, paramIndex)
|
|
385
|
+
if (__rowPolicy) conditions.push('(' + __rowPolicy.predicate + ')')
|
|
340
386
|
|
|
341
387
|
let sql = \`SELECT * FROM ${tableName}\`
|
|
342
388
|
|
|
@@ -345,15 +391,18 @@ export default async function handler(req, res) {
|
|
|
345
391
|
}
|
|
346
392
|
|
|
347
393
|
// Handle sorts - new array format. Two ORDER BY clauses are built: the one
|
|
348
|
-
// actually used
|
|
394
|
+
// actually used (a translatable column in the request's language, the
|
|
395
|
+
// products table's price rewrites), and a plain-column twin kept only as
|
|
396
|
+
// the fallback below.
|
|
349
397
|
let orderBySql = ''
|
|
350
398
|
let plainOrderBySql = ''
|
|
351
399
|
if (sorts) {
|
|
352
400
|
const parsedSorts = safeJSONParse(sorts)
|
|
353
401
|
if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
|
|
354
402
|
const valid = parsedSorts.filter((sort) => sort && sort.field)
|
|
403
|
+
valid.forEach((sort) => assertRequestColumn(sort.field, 'sort field'))
|
|
355
404
|
const orderOf = (sort) => (sort.order || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'
|
|
356
|
-
const orderClauses = valid.map((sort) => \`\${sortFieldSql(sort.field)} \${orderOf(sort)}\`)
|
|
405
|
+
const orderClauses = valid.map((sort) => \`\${localizedSortFieldSql(sort.field, requestLocale) || sortFieldSql(sort.field)} \${orderOf(sort)}\`)
|
|
357
406
|
const plainClauses = valid.map((sort) => \`\${sortFallbackField(sort.field)} \${orderOf(sort)}\`)
|
|
358
407
|
|
|
359
408
|
if (orderClauses.length > 0) {
|
|
@@ -369,16 +418,18 @@ export default async function handler(req, res) {
|
|
|
369
418
|
}
|
|
370
419
|
}
|
|
371
420
|
} else if (sortBy) {
|
|
372
|
-
|
|
373
|
-
|
|
421
|
+
assertRequestColumn(sortBy, 'sort field')
|
|
422
|
+
const sortByDirection = (sortOrder || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'
|
|
423
|
+
orderBySql = \` ORDER BY \${localizedSortFieldSql(sortBy, requestLocale) || sortBy} \${sortByDirection}\`
|
|
424
|
+
plainOrderBySql = \` ORDER BY \${sortBy} \${sortByDirection}\`
|
|
374
425
|
}
|
|
375
426
|
const usedDiscountAwareSort = orderBySql !== plainOrderBySql
|
|
376
427
|
|
|
377
|
-
const limitValue = limit || perPage
|
|
428
|
+
const limitValue = requestRowCount(limit || perPage)
|
|
378
429
|
const offsetValue = offset !== undefined ? parseInt(offset) : (page && perPage ? (parseInt(page) - 1) * parseInt(perPage) : undefined)
|
|
379
430
|
|
|
380
431
|
let sqlTail = ''
|
|
381
|
-
if (limitValue) {
|
|
432
|
+
if (limitValue !== undefined) {
|
|
382
433
|
sqlTail += \` LIMIT \${limitValue}\`
|
|
383
434
|
}
|
|
384
435
|
|
|
@@ -392,14 +443,16 @@ export default async function handler(req, res) {
|
|
|
392
443
|
// The discount-aware price ordering is an inline sub-select over a JSON
|
|
393
444
|
// column. It is written to be unraisable, but it runs inside ORDER BY for
|
|
394
445
|
// the whole table - so if a database ever rejects it, fall back to ordering
|
|
395
|
-
// by the stored list price rather than serving an empty products page.
|
|
446
|
+
// by the stored list price rather than serving an empty products page. The
|
|
447
|
+
// translatable-column ordering takes the same exit: a per-language column
|
|
448
|
+
// the schema had at generation time may have been dropped since.
|
|
396
449
|
let result
|
|
397
450
|
try {
|
|
398
451
|
result = await client.query(sql, queryParams)
|
|
399
452
|
} catch (sortError) {
|
|
400
453
|
if (!usedDiscountAwareSort) throw sortError
|
|
401
454
|
console.warn(
|
|
402
|
-
'
|
|
455
|
+
'Rewritten sort failed; falling back to the plain column:',
|
|
403
456
|
sortError && sortError.message
|
|
404
457
|
)
|
|
405
458
|
result = await client.query(baseSql + plainOrderBySql + sqlTail, queryParams)
|
|
@@ -430,14 +483,20 @@ export default async function handler(req, res) {
|
|
|
430
483
|
|
|
431
484
|
return res.status(200).json({
|
|
432
485
|
success: true,
|
|
433
|
-
data:
|
|
486
|
+
data: __taWithoutCredentials(
|
|
487
|
+
__rowPolicy ? __brpStripHidden(__rowPolicy, __numberedData) : __numberedData,
|
|
488
|
+
${JSON.stringify(tableName)}
|
|
489
|
+
),
|
|
434
490
|
timestamp: Date.now()
|
|
435
491
|
})
|
|
436
492
|
} catch (error) {
|
|
437
493
|
console.error('Teleport DB fetch error:', error)
|
|
438
|
-
|
|
494
|
+
// A database error names tables, columns and values: it stays in the server
|
|
495
|
+
// log. Only an error this route raised for the caller (it carries a status)
|
|
496
|
+
// is repeated to it.
|
|
497
|
+
return res.status(error.status || 500).json({
|
|
439
498
|
success: false,
|
|
440
|
-
error: error.message
|
|
499
|
+
error: error.status ? error.message : 'Failed to fetch data',
|
|
441
500
|
timestamp: Date.now()
|
|
442
501
|
})
|
|
443
502
|
} finally {
|
|
@@ -462,11 +521,13 @@ export const generateTeleportCountFetcher = (
|
|
|
462
521
|
|
|
463
522
|
return `
|
|
464
523
|
async function getCount(req, res) {
|
|
524
|
+
${generateReadGuardCall(tableName, null)}
|
|
465
525
|
const client = getClient()
|
|
466
526
|
|
|
467
527
|
try {
|
|
468
528
|
await client.connect()
|
|
469
529
|
const { query, queryColumns, filters } = req.query
|
|
530
|
+
if (__rowPolicy) __brpAssertVisibleFields(__rowPolicy, req.query)
|
|
470
531
|
const conditions = []
|
|
471
532
|
const queryParams = []
|
|
472
533
|
let paramIndex = 1
|
|
@@ -498,6 +559,7 @@ async function getCount(req, res) {
|
|
|
498
559
|
console.warn('Failed to fetch column names from information_schema:', schemaError.message)
|
|
499
560
|
}
|
|
500
561
|
}
|
|
562
|
+
if (__rowPolicy) columns = __brpVisibleColumns(__rowPolicy, columns)
|
|
501
563
|
|
|
502
564
|
if (columns.length > 0) {
|
|
503
565
|
const pattern = '%' + escapeLikePattern(query) + '%'
|
|
@@ -514,6 +576,7 @@ async function getCount(req, res) {
|
|
|
514
576
|
}
|
|
515
577
|
|
|
516
578
|
paramIndex = processFilters(filters, conditions, queryParams, paramIndex)
|
|
579
|
+
if (__rowPolicy) conditions.push('(' + __rowPolicy.predicate + ')')
|
|
517
580
|
|
|
518
581
|
let countSql = \`SELECT COUNT(*) FROM ${tableName}\`
|
|
519
582
|
if (conditions.length > 0) {
|
|
@@ -530,9 +593,9 @@ async function getCount(req, res) {
|
|
|
530
593
|
})
|
|
531
594
|
} catch (error) {
|
|
532
595
|
console.error('Error getting count:', error)
|
|
533
|
-
return res.status(500).json({
|
|
596
|
+
return res.status(error.status || 500).json({
|
|
534
597
|
success: false,
|
|
535
|
-
error: error.message
|
|
598
|
+
error: error.status ? error.message : 'Failed to get count',
|
|
536
599
|
timestamp: Date.now()
|
|
537
600
|
})
|
|
538
601
|
} finally {
|
package/src/fetchers/turso.ts
CHANGED
|
@@ -100,8 +100,10 @@ export default async function handler(req, res) {
|
|
|
100
100
|
if (filterTree) {
|
|
101
101
|
const buildCondition = (condition) => {
|
|
102
102
|
const field = sanitizeIdentifier(condition.source)
|
|
103
|
-
const
|
|
104
|
-
|
|
103
|
+
const normalizedCondition = normalizeInOperand(condition.operand, condition.destination)
|
|
104
|
+
if (normalizedCondition === null) return null
|
|
105
|
+
const value = normalizedCondition.value
|
|
106
|
+
const operand = normalizedCondition.operand
|
|
105
107
|
|
|
106
108
|
if (Array.isArray(value)) {
|
|
107
109
|
if (value.length === 0) return null
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a per-table read route (`/api/teleport-<table>-<source>` and its count
|
|
3
|
+
* twin) serves a BROWSER of the tables whose unpublished rows sit beside the
|
|
4
|
+
* public ones. (The tables it serves a browser not at all — the AI assistant's
|
|
5
|
+
* among them — are `TableAccess.PROTECTED_TABLES`, guarded by `__taGuardRead`.)
|
|
6
|
+
*
|
|
7
|
+
* The route answers whoever asks, with the whole table. For the blog that was
|
|
8
|
+
* every draft, scheduled and archived post — and each author's email address;
|
|
9
|
+
* for the catalogue, every draft and inactive product. The storefront's own
|
|
10
|
+
* bindings ask for the public rows, but the route is a URL anyone can call
|
|
11
|
+
* without them. So a caller that is not the store itself (its server code, or a
|
|
12
|
+
* signed-in administrator — the generated admin edits drafts through these
|
|
13
|
+
* routes) is served only the rows the storefront publishes, never a hidden
|
|
14
|
+
* column, and may neither filter, sort nor search by a hidden column: a filter
|
|
15
|
+
* is an oracle for a column it never returns.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export interface BrowserRowPolicy {
|
|
19
|
+
/** SQL every browser read of the table is narrowed with. */
|
|
20
|
+
predicate: string
|
|
21
|
+
/** Keys never served to a browser, wherever they sit in a row (raw and transformed names). */
|
|
22
|
+
hiddenColumns: ReadonlyArray<string>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const BROWSER_ROW_POLICIES: Readonly<Record<string, BrowserRowPolicy>> = {
|
|
26
|
+
// ONLY `published` is public — the blog's own rule (see the GUI's
|
|
27
|
+
// `blog-post-status.ts`): drafts, scheduled and archived posts are not.
|
|
28
|
+
teleport_blog_posts: {
|
|
29
|
+
predicate: "status = 'published'",
|
|
30
|
+
hiddenColumns: ['author_email', 'authorEmail'],
|
|
31
|
+
},
|
|
32
|
+
// The storefront sells `active` products; a draft or inactive one is the
|
|
33
|
+
// merchant's, spelled in any case the admin form stored it in.
|
|
34
|
+
teleport_products: {
|
|
35
|
+
predicate: "LOWER(TRIM(status)) = 'active'",
|
|
36
|
+
hiddenColumns: [],
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* ES5 source inlined after the table-access preamble (it uses `__ta*` and
|
|
42
|
+
* `__tqSessionToken`). Declares `__brpAccess(req, tableName, rawQuery)`, which
|
|
43
|
+
* resolves to `{ denied, policy }`: `denied` a `{ status, message }` refusal or
|
|
44
|
+
* null, `policy` the narrowing to apply to this read or null (the caller reads
|
|
45
|
+
* the table whole). Plus the helpers that apply a policy to a request and to
|
|
46
|
+
* its rows.
|
|
47
|
+
*/
|
|
48
|
+
export const generateBrowserRowPolicyCode =
|
|
49
|
+
(): string => `// GENERATED — see browser-row-policy.ts in teleport-plugin-next-data-source.
|
|
50
|
+
var __BRP_POLICIES = ${JSON.stringify(BROWSER_ROW_POLICIES)};
|
|
51
|
+
|
|
52
|
+
// Who is asking: the store's own server code and its administrators read every
|
|
53
|
+
// row; anyone else is a visitor.
|
|
54
|
+
async function __brpReader(req) {
|
|
55
|
+
if (__taIsInternalRequest(req)) return { trusted: true };
|
|
56
|
+
var token = null;
|
|
57
|
+
try {
|
|
58
|
+
token = await __tqSessionToken(req);
|
|
59
|
+
} catch (e) {
|
|
60
|
+
token = null;
|
|
61
|
+
}
|
|
62
|
+
var role = token ? __taRoleOf(token) : null;
|
|
63
|
+
return { trusted: !!role && __TA_TRUSTED_READER_ROLES.indexOf(role) !== -1 };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function __brpMentionsAny(rawQuery, tables) {
|
|
67
|
+
for (var i = 0; i < tables.length; i++) {
|
|
68
|
+
if (__taSqlMentionsTable(rawQuery, tables[i])) return true;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// A raw statement cannot be narrowed, so a visitor's statement that names a
|
|
74
|
+
// narrowed table is refused whole. The session is only decoded when the read
|
|
75
|
+
// touches one of these tables.
|
|
76
|
+
async function __brpAccess(req, tableName, rawQuery) {
|
|
77
|
+
var raw = typeof rawQuery === 'string' && rawQuery.length > 0 ? rawQuery : null;
|
|
78
|
+
var bare = __taNormalizeTable(tableName);
|
|
79
|
+
var rawNarrowed = !!raw && __brpMentionsAny(raw, Object.keys(__BRP_POLICIES));
|
|
80
|
+
var policy = raw ? null : (Object.prototype.hasOwnProperty.call(__BRP_POLICIES, bare) ? __BRP_POLICIES[bare] : null);
|
|
81
|
+
if (!rawNarrowed && !policy) return { denied: null, policy: null };
|
|
82
|
+
var reader = await __brpReader(req);
|
|
83
|
+
if (reader.trusted) return { denied: null, policy: null };
|
|
84
|
+
if (rawNarrowed) {
|
|
85
|
+
return { denied: { status: 403, message: 'Forbidden: raw queries over this table are only available to server-side calls' }, policy: null };
|
|
86
|
+
}
|
|
87
|
+
return { denied: null, policy: policy };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function __brpColumnName(field) {
|
|
91
|
+
var parts = String(field == null ? '' : field).replace(/"/g, '').trim().split('.');
|
|
92
|
+
return parts[parts.length - 1];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function __brpIsHidden(policy, field) {
|
|
96
|
+
var name = __brpColumnName(field).toLowerCase();
|
|
97
|
+
for (var i = 0; i < policy.hiddenColumns.length; i++) {
|
|
98
|
+
if (policy.hiddenColumns[i].toLowerCase() === name) return true;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Every field a request filters, sorts or searches by, however deep a filter
|
|
104
|
+
// group nests it. A condition's value is data, not a field.
|
|
105
|
+
function __brpRequestFields(value, out) {
|
|
106
|
+
if (Array.isArray(value)) {
|
|
107
|
+
for (var i = 0; i < value.length; i++) __brpRequestFields(value[i], out);
|
|
108
|
+
} else if (value && typeof value === 'object') {
|
|
109
|
+
if (typeof value.source === 'string') out.push(value.source);
|
|
110
|
+
if (typeof value.field === 'string') out.push(value.field);
|
|
111
|
+
var keys = Object.keys(value);
|
|
112
|
+
for (var k = 0; k < keys.length; k++) {
|
|
113
|
+
if (keys[k] === 'destination' || keys[k] === 'value') continue;
|
|
114
|
+
if (value[keys[k]] && typeof value[keys[k]] === 'object') __brpRequestFields(value[keys[k]], out);
|
|
115
|
+
}
|
|
116
|
+
} else if (typeof value === 'string' && value) {
|
|
117
|
+
out.push(value);
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function __brpParse(value) {
|
|
123
|
+
if (typeof value !== 'string') return value;
|
|
124
|
+
try {
|
|
125
|
+
return JSON.parse(value);
|
|
126
|
+
} catch (e) {
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function __brpAssertVisibleFields(policy, query) {
|
|
132
|
+
var q = query || {};
|
|
133
|
+
var fields = [];
|
|
134
|
+
__brpRequestFields(__brpParse(q.filters), fields);
|
|
135
|
+
__brpRequestFields(__brpParse(q.sorts), fields);
|
|
136
|
+
__brpRequestFields(__brpParse(q.queryColumns), fields);
|
|
137
|
+
if (typeof q.sortBy === 'string') fields.push(q.sortBy);
|
|
138
|
+
for (var i = 0; i < fields.length; i++) {
|
|
139
|
+
if (__brpIsHidden(policy, fields[i])) {
|
|
140
|
+
var err = new Error('Forbidden: ' + __brpColumnName(fields[i]) + ' is not readable');
|
|
141
|
+
err.status = 403;
|
|
142
|
+
throw err;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function __brpVisibleColumns(policy, columns) {
|
|
148
|
+
return columns.filter(function(column) { return !__brpIsHidden(policy, column); });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// The rows without a hidden key, at any depth: a transformed row nests related
|
|
152
|
+
// rows (a post's related and adjacent posts) built the same way.
|
|
153
|
+
function __brpStripHidden(policy, value) {
|
|
154
|
+
if (Array.isArray(value)) {
|
|
155
|
+
return value.map(function(entry) { return __brpStripHidden(policy, entry); });
|
|
156
|
+
}
|
|
157
|
+
if (!value || typeof value !== 'object') return value;
|
|
158
|
+
var copy = {};
|
|
159
|
+
Object.keys(value).forEach(function(key) {
|
|
160
|
+
if (!__brpIsHidden(policy, key)) copy[key] = __brpStripHidden(policy, value[key]);
|
|
161
|
+
});
|
|
162
|
+
return copy;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Whether this request is answered exactly as an anonymous visitor's would be,
|
|
166
|
+
// so a response cached for one may serve the other. A read whose answer depends
|
|
167
|
+
// on who asks (\`viewDependent\`: a narrowed, protected or admin-only table) or
|
|
168
|
+
// that runs a raw statement differs for the store's own server code and its
|
|
169
|
+
// administrators, and must never share a cache entry with a visitor.
|
|
170
|
+
async function __brpSharesPublicView(req, viewDependent) {
|
|
171
|
+
var raw = !!(req && req.query && typeof req.query.rawQuery === 'string' && req.query.rawQuery.length > 0);
|
|
172
|
+
if (!viewDependent && !raw) return true;
|
|
173
|
+
return !(await __brpReader(req)).trusted;
|
|
174
|
+
}
|
|
175
|
+
`
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Whether a per-table read route's answer depends on who asks — the rows it
|
|
179
|
+
* narrows, the tables it guards — so its response cache must be split by view.
|
|
180
|
+
*/
|
|
181
|
+
export const isViewDependentTable = (
|
|
182
|
+
tableName: string,
|
|
183
|
+
protectedTables: ReadonlyArray<string>
|
|
184
|
+
): boolean => {
|
|
185
|
+
const parts = String(tableName || '')
|
|
186
|
+
.trim()
|
|
187
|
+
.toLowerCase()
|
|
188
|
+
.split('.')
|
|
189
|
+
const bare = parts[parts.length - 1].replace(/"/g, '')
|
|
190
|
+
return (
|
|
191
|
+
Object.prototype.hasOwnProperty.call(BROWSER_ROW_POLICIES, bare) ||
|
|
192
|
+
protectedTables.indexOf(bare) !== -1
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* The early return right after the table-access guard, plus the policy
|
|
198
|
+
* (`__rowPolicy`, null or a policy) the handler narrows the rest of its read by.
|
|
199
|
+
*/
|
|
200
|
+
export const generateBrowserRowAccessCall = (
|
|
201
|
+
tableName: string,
|
|
202
|
+
rawQueryExpr: string | null
|
|
203
|
+
): string => {
|
|
204
|
+
return ` const __rowAccess = await __brpAccess(req, ${JSON.stringify(tableName)}, ${
|
|
205
|
+
rawQueryExpr || 'null'
|
|
206
|
+
})
|
|
207
|
+
if (__rowAccess.denied) {
|
|
208
|
+
return res.status(__rowAccess.denied.status).json({ success: false, error: __rowAccess.denied.message, timestamp: Date.now() })
|
|
209
|
+
}
|
|
210
|
+
const __rowPolicy = __rowAccess.policy
|
|
211
|
+
`
|
|
212
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a per-table read route takes from its query string and splices into
|
|
3
|
+
* SQL: filter columns, sort columns and a row count. Values are bound as
|
|
4
|
+
* parameters; these three are not, so each is checked for the one shape it
|
|
5
|
+
* may have before it reaches the statement.
|
|
6
|
+
*/
|
|
7
|
+
export const generateRequestSqlGuardsCode = (): string => `
|
|
8
|
+
// A column name as this route accepts one from a request: plain, optionally
|
|
9
|
+
// quoted or table-qualified. Anything else - an expression, a sub-select - is
|
|
10
|
+
// SQL the caller wrote, and it would run as part of this statement.
|
|
11
|
+
const REQUEST_COLUMN_RE = /^"?[A-Za-z_][A-Za-z0-9_]*"?(?:\\."?[A-Za-z_][A-Za-z0-9_]*"?)?$/
|
|
12
|
+
function assertRequestColumn(field, label) {
|
|
13
|
+
if (typeof field !== 'string' || !REQUEST_COLUMN_RE.test(field)) {
|
|
14
|
+
const error = new Error('Invalid ' + label)
|
|
15
|
+
error.status = 400
|
|
16
|
+
throw error
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// A row count from a request, as a whole number or nothing.
|
|
21
|
+
function requestRowCount(value) {
|
|
22
|
+
const parsed = parseInt(value, 10)
|
|
23
|
+
return isNaN(parsed) || parsed < 0 ? undefined : parsed
|
|
24
|
+
}
|
|
25
|
+
`
|