@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,122 @@
|
|
|
1
|
+
import * as types from '@babel/types'
|
|
2
|
+
import generator from '@babel/generator'
|
|
3
|
+
import {
|
|
4
|
+
ChunkType,
|
|
5
|
+
FileType,
|
|
6
|
+
ComponentStructure,
|
|
7
|
+
ChunkDefinition,
|
|
8
|
+
} from '@teleporthq/teleport-types'
|
|
9
|
+
import { createNextArrayMapperPaginationPlugin } from '../src/pagination-plugin'
|
|
10
|
+
import { SEARCH_CLEAR_ATTR } from '../src/search-clear'
|
|
11
|
+
|
|
12
|
+
// tslint:disable:no-any
|
|
13
|
+
|
|
14
|
+
const element = (
|
|
15
|
+
tag: string,
|
|
16
|
+
attrs: Array<[string, string]>,
|
|
17
|
+
children: types.JSXElement['children'] = []
|
|
18
|
+
): types.JSXElement =>
|
|
19
|
+
types.jsxElement(
|
|
20
|
+
types.jsxOpeningElement(
|
|
21
|
+
types.jsxIdentifier(tag),
|
|
22
|
+
attrs.map(([name, value]) =>
|
|
23
|
+
types.jsxAttribute(types.jsxIdentifier(name), types.stringLiteral(value))
|
|
24
|
+
),
|
|
25
|
+
false
|
|
26
|
+
),
|
|
27
|
+
types.jsxClosingElement(types.jsxIdentifier(tag)),
|
|
28
|
+
children,
|
|
29
|
+
false
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
const searchInput = (): types.JSXElement => element('input', [['className', 'search-input']])
|
|
33
|
+
const clearButton = (): types.JSXElement =>
|
|
34
|
+
element('button', [
|
|
35
|
+
['type', 'button'],
|
|
36
|
+
[SEARCH_CLEAR_ATTR, 'true'],
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A component whose JSX is the field the editor builds — `<div><input
|
|
41
|
+
* class=search-input/><button data-tq-search-clear/></div>` — plus, when
|
|
42
|
+
* asked, a stray marked button somewhere else on the page.
|
|
43
|
+
*/
|
|
44
|
+
const makeComponentChunk = (withStrayButton: boolean): ChunkDefinition => {
|
|
45
|
+
const field = element('div', [['data-tq-search-field', 'true']], [searchInput(), clearButton()])
|
|
46
|
+
const root = element(
|
|
47
|
+
'div',
|
|
48
|
+
[],
|
|
49
|
+
withStrayButton ? [field, element('div', [], [clearButton()])] : [field]
|
|
50
|
+
)
|
|
51
|
+
const body = types.blockStatement([types.returnStatement(root)])
|
|
52
|
+
const arrow = types.arrowFunctionExpression([types.identifier('props')], body)
|
|
53
|
+
const declaration = types.variableDeclaration('const', [
|
|
54
|
+
types.variableDeclarator(types.identifier('TestComponent'), arrow),
|
|
55
|
+
])
|
|
56
|
+
return {
|
|
57
|
+
name: 'jsx-component',
|
|
58
|
+
type: ChunkType.AST,
|
|
59
|
+
fileType: FileType.JS,
|
|
60
|
+
linkAfter: [],
|
|
61
|
+
content: declaration,
|
|
62
|
+
meta: {},
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const makeUidlNode = (): any => ({
|
|
67
|
+
type: 'data-source-list',
|
|
68
|
+
content: {
|
|
69
|
+
renderPropIdentifier: 'items',
|
|
70
|
+
resourceDefinition: {
|
|
71
|
+
dataSourceId: 'ds1',
|
|
72
|
+
tableName: 'products',
|
|
73
|
+
dataSourceType: 'postgresql',
|
|
74
|
+
},
|
|
75
|
+
resource: { params: { queryColumns: { content: ['name'] } } },
|
|
76
|
+
nodes: {
|
|
77
|
+
success: {
|
|
78
|
+
type: 'cms-list-repeater',
|
|
79
|
+
content: {
|
|
80
|
+
renderPropIdentifier: 'product',
|
|
81
|
+
paginated: true,
|
|
82
|
+
perPage: 20,
|
|
83
|
+
searchEnabled: true,
|
|
84
|
+
searchDebounce: 300,
|
|
85
|
+
nodes: { list: { type: 'element', content: { elementType: 'div' } } },
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const runPlugin = async (withStrayButton = false): Promise<string> => {
|
|
93
|
+
const chunk = makeComponentChunk(withStrayButton)
|
|
94
|
+
const structure: ComponentStructure = {
|
|
95
|
+
uidl: { name: 'TestComponent', node: makeUidlNode() },
|
|
96
|
+
chunks: [chunk],
|
|
97
|
+
dependencies: {},
|
|
98
|
+
options: { dataSources: {}, extractedResources: {} },
|
|
99
|
+
} as never
|
|
100
|
+
await createNextArrayMapperPaginationPlugin()(structure)
|
|
101
|
+
return generator(chunk.content as types.Node).code
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const collapse = (code: string): string => code.replace(/\s+/g, ' ')
|
|
105
|
+
|
|
106
|
+
describe('pagination plugin — search clear button', () => {
|
|
107
|
+
it('empties the query on click and renders only while something is typed', async () => {
|
|
108
|
+
const flat = collapse(await runPlugin())
|
|
109
|
+
expect(flat).toContain('onClick={() => setDs_0_searchQuery("")}')
|
|
110
|
+
expect(flat).toContain('{ds_0_searchQuery ? <button type="button" data-tq-search-clear="true"')
|
|
111
|
+
expect(flat).toContain(': null}')
|
|
112
|
+
// The input beside it is the one the button clears.
|
|
113
|
+
expect(flat).toContain('value={ds_0_searchQuery}')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('leaves a marked button that shares no parent with a search input alone', async () => {
|
|
117
|
+
const flat = collapse(await runPlugin(true))
|
|
118
|
+
expect(flat.match(/setDs_0_searchQuery\(""\)/g)).toHaveLength(1)
|
|
119
|
+
// The stray one keeps its markup, unwired and unconditional.
|
|
120
|
+
expect(flat).toContain('<div><button type="button" data-tq-search-clear="true"></button></div>')
|
|
121
|
+
})
|
|
122
|
+
})
|
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
/* tslint:disable:function-constructor */
|
|
2
|
+
import type { UIDLAuthentication } from '@teleporthq/teleport-types'
|
|
3
|
+
import { TableAccess } from '@teleporthq/teleport-shared'
|
|
4
|
+
import { generateDataSourceFetcherWithCore } from '../src/data-source-fetchers'
|
|
5
|
+
import { buildProductTransformOptions } from '../src/transformations'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Every table a page binds gets a read route (`/api/teleport-<table>-<source>`
|
|
9
|
+
* plus a `-count` twin) that serves the whole table, and any single SELECT
|
|
10
|
+
* through `?rawQuery=`, to whoever asks. For gift cards and voucher codes that
|
|
11
|
+
* is a list of cash. The route now refuses those tables to anyone but a server
|
|
12
|
+
* segment carrying the app secret or a signed-in member of a trusted role —
|
|
13
|
+
* the generated admin — and the verdict is a status code from the executed
|
|
14
|
+
* route, not a substring of its source.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const SECRET = 'server-secret'
|
|
18
|
+
const ADMIN_ROLES = ['admin']
|
|
19
|
+
|
|
20
|
+
interface RouteResult {
|
|
21
|
+
status: number
|
|
22
|
+
body: any
|
|
23
|
+
queries: string[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface FakeSession {
|
|
27
|
+
role?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type RouteRunner = (params: {
|
|
31
|
+
handler: 'handler' | 'getCount'
|
|
32
|
+
query?: Record<string, string>
|
|
33
|
+
headers?: Record<string, string>
|
|
34
|
+
session?: FakeSession | null
|
|
35
|
+
}) => Promise<RouteResult>
|
|
36
|
+
|
|
37
|
+
type BootedRoute = RouteRunner & {
|
|
38
|
+
serverCall: (name: 'fetchData' | 'fetchCount') => Promise<unknown>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function bootTableRoute(
|
|
42
|
+
tableName: string,
|
|
43
|
+
trustedReaderRoles: string[],
|
|
44
|
+
options: { appSecret?: string | null; rows?: any[] } = {}
|
|
45
|
+
): BootedRoute {
|
|
46
|
+
const appSecret = options.appSecret === undefined ? SECRET : options.appSecret
|
|
47
|
+
const dataSource = {
|
|
48
|
+
id: 'ds-1',
|
|
49
|
+
name: 'db',
|
|
50
|
+
type: 'teleport' as const,
|
|
51
|
+
config: {
|
|
52
|
+
host: 'teleporthq.secrets.TELEPORT_DB_HOST',
|
|
53
|
+
port: 5432,
|
|
54
|
+
user: 'teleporthq.secrets.TELEPORT_DB_USER',
|
|
55
|
+
password: 'teleporthq.secrets.TELEPORT_DB_PASSWORD',
|
|
56
|
+
database: 'teleporthq.secrets.TELEPORT_DB_NAME',
|
|
57
|
+
selectedTables: { [tableName]: { columns: [{ name: 'id', type: 'uuid' }] } },
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
const esm = generateDataSourceFetcherWithCore(dataSource as never, tableName, false, {
|
|
61
|
+
trustedReaderRoles,
|
|
62
|
+
})
|
|
63
|
+
// The route is an ES module; run it as a script with the same bindings.
|
|
64
|
+
const code = esm
|
|
65
|
+
.replace("import { Client } from 'pg'", "const { Client } = require('pg')")
|
|
66
|
+
.replace(/^export default async function handler/m, 'async function handler')
|
|
67
|
+
.replace(/^export \{[^}]*\}\s*$/m, '')
|
|
68
|
+
.replace(/^export default \{[^}]*\}\s*$/m, '')
|
|
69
|
+
.concat(
|
|
70
|
+
'\nmodule.exports = { handler: handler, getCount: getCount, fetchData: fetchData, fetchCount: fetchCount }'
|
|
71
|
+
)
|
|
72
|
+
expect(code).not.toMatch(/^export /m)
|
|
73
|
+
|
|
74
|
+
const queries: string[] = []
|
|
75
|
+
let session: FakeSession | null = null
|
|
76
|
+
class FakeClient {
|
|
77
|
+
public async connect(): Promise<void> {
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
public async end(): Promise<void> {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
public async query(sql: string): Promise<{ rows: any[]; rowCount: number }> {
|
|
84
|
+
queries.push(sql)
|
|
85
|
+
if (/information_schema/i.test(sql)) {
|
|
86
|
+
return { rows: [], rowCount: 0 }
|
|
87
|
+
}
|
|
88
|
+
if (/^SELECT COUNT/i.test(sql)) {
|
|
89
|
+
return { rows: [{ count: '0' }], rowCount: 1 }
|
|
90
|
+
}
|
|
91
|
+
const rows = options.rows || []
|
|
92
|
+
return { rows, rowCount: rows.length }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const fakeRequire = (name: string): any => {
|
|
96
|
+
if (name === 'pg') {
|
|
97
|
+
return { Client: FakeClient }
|
|
98
|
+
}
|
|
99
|
+
if (name === 'next-auth/jwt') {
|
|
100
|
+
return { getToken: async () => (session ? { sub: 'u1', role: session.role } : null) }
|
|
101
|
+
}
|
|
102
|
+
throw new Error('unexpected require: ' + name)
|
|
103
|
+
}
|
|
104
|
+
const moduleObj: { exports: any } = { exports: {} }
|
|
105
|
+
new Function('require', 'module', 'exports', 'process', 'console', code)(
|
|
106
|
+
fakeRequire,
|
|
107
|
+
moduleObj,
|
|
108
|
+
moduleObj.exports,
|
|
109
|
+
{
|
|
110
|
+
env: {
|
|
111
|
+
...(appSecret ? { NEXTAUTH_SECRET: appSecret } : {}),
|
|
112
|
+
TELEPORT_DB_HOST: 'h',
|
|
113
|
+
TELEPORT_DB_USER: 'u',
|
|
114
|
+
TELEPORT_DB_PASSWORD: 'p',
|
|
115
|
+
TELEPORT_DB_NAME: 'd',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{ error: () => undefined, warn: () => undefined, log: () => undefined }
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
const serverCall = async (name: 'fetchData' | 'fetchCount'): Promise<unknown> => {
|
|
122
|
+
queries.length = 0
|
|
123
|
+
session = null
|
|
124
|
+
return moduleObj.exports[name]({})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const run: RouteRunner = async ({
|
|
128
|
+
handler,
|
|
129
|
+
query = {},
|
|
130
|
+
headers = {},
|
|
131
|
+
session: requestSession = null,
|
|
132
|
+
}) => {
|
|
133
|
+
queries.length = 0
|
|
134
|
+
session = requestSession
|
|
135
|
+
let status = 0
|
|
136
|
+
let responseBody: any = null
|
|
137
|
+
const res = {
|
|
138
|
+
status(statusCode: number) {
|
|
139
|
+
status = statusCode
|
|
140
|
+
return this
|
|
141
|
+
},
|
|
142
|
+
json(payload: any) {
|
|
143
|
+
responseBody = payload
|
|
144
|
+
return this
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
const req = {
|
|
148
|
+
method: 'GET',
|
|
149
|
+
query,
|
|
150
|
+
headers: {
|
|
151
|
+
cookie: requestSession ? 'next-auth.session-token=signed' : '',
|
|
152
|
+
...headers,
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
await moduleObj.exports[handler](req, res)
|
|
156
|
+
return { status, body: responseBody, queries: queries.slice() }
|
|
157
|
+
}
|
|
158
|
+
const booted = run as BootedRoute
|
|
159
|
+
booted.serverCall = serverCall
|
|
160
|
+
return booted
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const internal = { 'x-internal-data-secret': SECRET }
|
|
164
|
+
|
|
165
|
+
describe('per-table read routes — money and customer tables are read only by the admin or the server', () => {
|
|
166
|
+
const giftCards = bootTableRoute('teleport_gift_cards', ADMIN_ROLES)
|
|
167
|
+
const vouchers = bootTableRoute('teleport_vouchers', ADMIN_ROLES)
|
|
168
|
+
const products = bootTableRoute('teleport_products', ADMIN_ROLES)
|
|
169
|
+
// The customers' own records: only the generated admin lists them from a
|
|
170
|
+
// browser; a buyer's order page reads through a server segment.
|
|
171
|
+
const orders = bootTableRoute('teleport_orders', ADMIN_ROLES)
|
|
172
|
+
const users = bootTableRoute('users', ADMIN_ROLES)
|
|
173
|
+
const reviews = bootTableRoute('teleport_product_reviews', ADMIN_ROLES)
|
|
174
|
+
|
|
175
|
+
it('refuses a guest before the database is touched, on the list and the count', async () => {
|
|
176
|
+
for (const route of [giftCards, vouchers, orders, users, reviews]) {
|
|
177
|
+
const list = await route({ handler: 'handler' })
|
|
178
|
+
expect(list.status).toBe(401)
|
|
179
|
+
expect(list.queries).toEqual([])
|
|
180
|
+
const count = await route({ handler: 'getCount' })
|
|
181
|
+
expect(count.status).toBe(401)
|
|
182
|
+
expect(count.queries).toEqual([])
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('refuses a signed-in shopper, and serves a signed-in administrator', async () => {
|
|
187
|
+
for (const route of [giftCards, orders, users]) {
|
|
188
|
+
expect((await route({ handler: 'handler', session: { role: 'user' } })).status).toBe(403)
|
|
189
|
+
expect((await route({ handler: 'getCount', session: { role: 'user' } })).status).toBe(403)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const admin = await giftCards({ handler: 'handler', session: { role: 'admin' } })
|
|
193
|
+
expect(admin.status).toBe(200)
|
|
194
|
+
expect(admin.queries.some((sql) => /FROM teleport_gift_cards/i.test(sql))).toBe(true)
|
|
195
|
+
expect((await giftCards({ handler: 'getCount', session: { role: 'admin' } })).status).toBe(200)
|
|
196
|
+
expect((await orders({ handler: 'handler', session: { role: 'admin' } })).status).toBe(200)
|
|
197
|
+
expect((await users({ handler: 'getCount', session: { role: 'admin' } })).status).toBe(200)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('serves a server segment that presents the app secret', async () => {
|
|
201
|
+
expect((await giftCards({ handler: 'handler', headers: internal })).status).toBe(200)
|
|
202
|
+
expect((await vouchers({ handler: 'getCount', headers: internal })).status).toBe(200)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it("serves the module's own server-side fetchData, which getStaticProps and page-load workflows call", async () => {
|
|
206
|
+
await expect(giftCards.serverCall('fetchData')).resolves.toEqual([])
|
|
207
|
+
await expect(vouchers.serverCall('fetchCount')).resolves.toBe(0)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('refuses a browser every raw statement, whatever it names, once the app has a secret', async () => {
|
|
211
|
+
// Generated pages never send one: their raw queries run in getStaticProps
|
|
212
|
+
// (the module's own fetchData) or in server segments. So no statement scan
|
|
213
|
+
// has to be right about what a browser sends — admin sessions included.
|
|
214
|
+
for (const rawQuery of [
|
|
215
|
+
'SELECT code FROM teleport_gift_cards',
|
|
216
|
+
"SELECT 1 FROM teleport_products WHERE name = 'teleport_gift_cards'",
|
|
217
|
+
'SELECT code AS "x\'", balance FROM teleport_gift_cards -- \'',
|
|
218
|
+
]) {
|
|
219
|
+
for (const session of [null, { role: 'user' }, { role: 'admin' }]) {
|
|
220
|
+
const result = await products({ handler: 'handler', query: { rawQuery }, session })
|
|
221
|
+
expect(result.status).toBe(403)
|
|
222
|
+
expect(result.queries).toEqual([])
|
|
223
|
+
}
|
|
224
|
+
expect(
|
|
225
|
+
(await products({ handler: 'handler', query: { rawQuery }, headers: internal })).status
|
|
226
|
+
).toBe(200)
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('refuses a column the query string made up, before the database is touched', async () => {
|
|
231
|
+
// Filter and sort columns are spliced into the statement (values are
|
|
232
|
+
// bound): a sub-select there would read any table, and the error message
|
|
233
|
+
// Postgres returns would carry what it read.
|
|
234
|
+
const smuggled = "CAST((SELECT string_agg(code, ',') FROM teleport_gift_cards) AS int)"
|
|
235
|
+
for (const query of [
|
|
236
|
+
{ filters: JSON.stringify([{ source: smuggled, operand: '=', destination: 1 }]) },
|
|
237
|
+
{ sorts: JSON.stringify([{ field: smuggled, order: 'asc' }]) },
|
|
238
|
+
{ sortBy: smuggled },
|
|
239
|
+
]) {
|
|
240
|
+
const result = await products({ handler: 'handler', query })
|
|
241
|
+
expect(result.status).toBe(400)
|
|
242
|
+
expect(result.queries).toEqual([])
|
|
243
|
+
}
|
|
244
|
+
// A row count is a number or nothing: the sub-select never reaches SQL.
|
|
245
|
+
const limited = await products({
|
|
246
|
+
handler: 'handler',
|
|
247
|
+
query: { limit: '(SELECT 1 FROM teleport_vouchers)' },
|
|
248
|
+
})
|
|
249
|
+
expect(limited.status).toBe(200)
|
|
250
|
+
expect(limited.queries.join('\n')).not.toMatch(/teleport_vouchers|LIMIT/)
|
|
251
|
+
const count = await products({
|
|
252
|
+
handler: 'getCount',
|
|
253
|
+
query: { filters: JSON.stringify([{ source: smuggled, operand: '=', destination: 1 }]) },
|
|
254
|
+
})
|
|
255
|
+
expect(count.status).toBe(400)
|
|
256
|
+
expect(count.queries).toEqual([])
|
|
257
|
+
// Real columns still filter and sort.
|
|
258
|
+
const plain = await products({
|
|
259
|
+
handler: 'handler',
|
|
260
|
+
query: {
|
|
261
|
+
filters: JSON.stringify([{ source: 'status', operand: '=', destination: 'active' }]),
|
|
262
|
+
sorts: JSON.stringify([{ field: 'created_at', order: 'desc' }]),
|
|
263
|
+
limit: '12',
|
|
264
|
+
},
|
|
265
|
+
})
|
|
266
|
+
expect(plain.status).toBe(200)
|
|
267
|
+
expect(plain.queries.join('\n')).toMatch(/WHERE status = \$1/)
|
|
268
|
+
expect(plain.queries.join('\n')).toMatch(/LIMIT 12/)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('keeps scanning a raw statement in an app with no secret to tell callers apart', async () => {
|
|
272
|
+
// No authentication, hence no store and no admin: the old scan still
|
|
273
|
+
// stands between a raw statement and any protected table.
|
|
274
|
+
const open = bootTableRoute('teleport_products', ADMIN_ROLES, { appSecret: null })
|
|
275
|
+
for (const rawQuery of [
|
|
276
|
+
'SELECT code FROM teleport_gift_cards',
|
|
277
|
+
'SELECT * FROM "teleport_vouchers"',
|
|
278
|
+
'WITH c AS (SELECT id FROM public.teleport_voucher_redemptions) SELECT * FROM c',
|
|
279
|
+
"SELECT 1 /* teleport_products */ FROM Teleport_Gift_Card_Transactions -- 'x'",
|
|
280
|
+
// The rendered gift-card email carries the full code.
|
|
281
|
+
'SELECT body_html FROM teleport_sent_emails',
|
|
282
|
+
// ⛔ A dollar-quoted body holding a lone apostrophe used to open a
|
|
283
|
+
// "literal" that swallowed the rest of the statement, table name
|
|
284
|
+
// included — Postgres runs it happily.
|
|
285
|
+
"SELECT $$it's$$, code FROM teleport_gift_cards",
|
|
286
|
+
"SELECT $tag$don't$tag$ AS a, balance FROM teleport_gift_cards",
|
|
287
|
+
// An escape string, and an unterminated literal (unreadable ⇒ refused).
|
|
288
|
+
"SELECT E'\\'' AS a, code FROM teleport_gift_cards",
|
|
289
|
+
"SELECT 'unterminated, code FROM teleport_products",
|
|
290
|
+
// Spellings a name scan cannot see: a Unicode-escaped identifier (\\005f
|
|
291
|
+
// is the underscore), and functions that run SQL text or dump tables.
|
|
292
|
+
'SELECT code FROM U&"teleport\\005fgift_cards"',
|
|
293
|
+
"SELECT query_to_xml('select code from teleport_gift_cards', true, false, '')",
|
|
294
|
+
"SELECT database_to_xml(true, false, '')",
|
|
295
|
+
// Quote and comment markers inside a quoted identifier, `$` and `E`
|
|
296
|
+
// inside an identifier, and the planner statistics.
|
|
297
|
+
'SELECT code AS "x\'", balance FROM teleport_gift_cards -- \'',
|
|
298
|
+
'SELECT 1 AS "--", code, balance FROM teleport_gift_cards',
|
|
299
|
+
'SELECT code AS a$b$, balance FROM teleport_gift_cards AS g$b$',
|
|
300
|
+
"SELECT CASE WHEN true THEN 'a' ELSE'\\' END, code FROM teleport_vouchers -- '",
|
|
301
|
+
"SELECT histogram_bounds::text FROM pg_stats WHERE tablename = 'teleport_vouchers'",
|
|
302
|
+
]) {
|
|
303
|
+
const guest = await open({ handler: 'handler', query: { rawQuery } })
|
|
304
|
+
expect(guest.status).toBe(401)
|
|
305
|
+
expect(guest.queries).toEqual([])
|
|
306
|
+
}
|
|
307
|
+
// A readable statement that names nothing protected still passes, dollar
|
|
308
|
+
// quotes and nested comments included.
|
|
309
|
+
for (const rawQuery of [
|
|
310
|
+
"SELECT 1 FROM team_members WHERE name = 'teleport_gift_cards'",
|
|
311
|
+
"SELECT $$it's fine$$ /* a /* nested */ note */ FROM team_members",
|
|
312
|
+
]) {
|
|
313
|
+
expect((await open({ handler: 'handler', query: { rawQuery } })).status).toBe(200)
|
|
314
|
+
}
|
|
315
|
+
// A statement over a table a visitor only reads narrowed (the published
|
|
316
|
+
// posts, the active products) cannot be narrowed, so it is refused whole.
|
|
317
|
+
for (const rawQuery of [
|
|
318
|
+
"SELECT 1 FROM teleport_products WHERE name = 'teleport_gift_cards'",
|
|
319
|
+
'SELECT title, author_email FROM teleport_blog_posts',
|
|
320
|
+
]) {
|
|
321
|
+
const refused = await open({ handler: 'handler', query: { rawQuery } })
|
|
322
|
+
expect(refused.status).toBe(403)
|
|
323
|
+
expect(refused.queries).toEqual([])
|
|
324
|
+
}
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
it('leaves every other read exactly as it was', async () => {
|
|
328
|
+
expect((await products({ handler: 'handler' })).status).toBe(200)
|
|
329
|
+
expect((await products({ handler: 'getCount' })).status).toBe(200)
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it('serves the automatic-discount table to no browser but the admin — its one public shape is the data API feed', async () => {
|
|
333
|
+
const discounts = bootTableRoute('teleport_discounts', ADMIN_ROLES)
|
|
334
|
+
const guest = await discounts({ handler: 'handler' })
|
|
335
|
+
expect(guest.status).toBe(401)
|
|
336
|
+
expect(guest.queries).toEqual([])
|
|
337
|
+
expect((await discounts({ handler: 'getCount', session: { role: 'user' } })).status).toBe(403)
|
|
338
|
+
expect((await discounts({ handler: 'handler', session: { role: 'admin' } })).status).toBe(200)
|
|
339
|
+
// Nor through another table's raw statement.
|
|
340
|
+
const rawQuery = "SELECT name, usage_count FROM teleport_discounts WHERE status = 'draft'"
|
|
341
|
+
expect((await products({ handler: 'handler', query: { rawQuery } })).status).toBe(403)
|
|
342
|
+
expect(
|
|
343
|
+
(await products({ handler: 'handler', query: { rawQuery }, session: { role: 'user' } }))
|
|
344
|
+
.status
|
|
345
|
+
).toBe(403)
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
it('admits nobody from a browser when the project has no admin panel', async () => {
|
|
349
|
+
const noAdmin = bootTableRoute('teleport_gift_cards', [])
|
|
350
|
+
expect((await noAdmin({ handler: 'handler', session: { role: 'admin' } })).status).toBe(403)
|
|
351
|
+
expect((await noAdmin({ handler: 'handler', headers: internal })).status).toBe(200)
|
|
352
|
+
})
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
const ADMIN_PANEL_AUTH: UIDLAuthentication = {
|
|
356
|
+
enabled: true,
|
|
357
|
+
dataSourceId: 'ds-1',
|
|
358
|
+
dataSourceType: 'teleport',
|
|
359
|
+
passwordAuthEnabled: true,
|
|
360
|
+
providers: [],
|
|
361
|
+
roles: ['admin', 'user'],
|
|
362
|
+
tables: {},
|
|
363
|
+
pageProtection: {},
|
|
364
|
+
folderProtection: {
|
|
365
|
+
f1: {
|
|
366
|
+
requiresAuth: true,
|
|
367
|
+
allowedRoles: ['admin'],
|
|
368
|
+
folderName: 'admin-panel',
|
|
369
|
+
parentId: null,
|
|
370
|
+
children: {},
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
authPages: {},
|
|
374
|
+
callbackBaseUrl: '',
|
|
375
|
+
envKeys: {},
|
|
376
|
+
customUserProperties: [],
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
describe('per-table read routes — the users table never serves its credentials', () => {
|
|
380
|
+
const userRow = {
|
|
381
|
+
id: 'u1',
|
|
382
|
+
name: 'Ada',
|
|
383
|
+
password: '$2b$10$hash',
|
|
384
|
+
access_token: 'ya29.token',
|
|
385
|
+
refresh_token: '1//refresh',
|
|
386
|
+
id_token: 'eyJ.id',
|
|
387
|
+
session_state: 's',
|
|
388
|
+
token_type: 'Bearer',
|
|
389
|
+
scope: 'email',
|
|
390
|
+
expires_at: 1790000000,
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
it("keeps the profile and drops the password and tokens, for a browser and for a page's props", async () => {
|
|
394
|
+
// Whatever fetchData returns is serialized into the page, so the server
|
|
395
|
+
// path strips them too.
|
|
396
|
+
const users = bootTableRoute('users', ADMIN_ROLES, { rows: [userRow] })
|
|
397
|
+
const read = await users({ handler: 'handler', session: { role: 'admin' } })
|
|
398
|
+
expect(read.status).toBe(200)
|
|
399
|
+
expect(read.body.data).toEqual([{ id: 'u1', name: 'Ada', __rowNumber: 1 }])
|
|
400
|
+
const props = (await users.serverCall('fetchData')) as Array<Record<string, unknown>>
|
|
401
|
+
expect(props.map((row) => Object.keys(row).sort())).toEqual([['__rowNumber', 'id', 'name']])
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
it('drops the unambiguous credential names from a raw statement, whatever table it read', async () => {
|
|
405
|
+
const products = bootTableRoute('teleport_products', ADMIN_ROLES, {
|
|
406
|
+
rows: [{ id: 'p1', expires_at: '2026-12-31', password: 'x', access_token: 'y' }],
|
|
407
|
+
})
|
|
408
|
+
const result = await products({
|
|
409
|
+
handler: 'handler',
|
|
410
|
+
query: { rawQuery: 'SELECT p.*, u.password FROM teleport_products p' },
|
|
411
|
+
headers: internal,
|
|
412
|
+
})
|
|
413
|
+
expect(result.body.data).toEqual([{ id: 'p1', expires_at: '2026-12-31' }])
|
|
414
|
+
})
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
describe('buildProductTransformOptions — the roles reach the emitted module', () => {
|
|
418
|
+
it("reads the project's auth under BOTH names, because a ProjectUIDL spells it `authentication`", () => {
|
|
419
|
+
// The project plugins (data-source utility, global-state API routes, page
|
|
420
|
+
// state fetchers) hand this the UIDL itself.
|
|
421
|
+
expect(buildProductTransformOptions({ authentication: ADMIN_PANEL_AUTH })).toMatchObject({
|
|
422
|
+
trustedReaderRoles: ['admin'],
|
|
423
|
+
})
|
|
424
|
+
expect(buildProductTransformOptions({ auth: ADMIN_PANEL_AUTH })).toMatchObject({
|
|
425
|
+
trustedReaderRoles: ['admin'],
|
|
426
|
+
})
|
|
427
|
+
expect(buildProductTransformOptions({})).toMatchObject({ trustedReaderRoles: [] })
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
it("emits a money table's route with a role list its admin panel can pass", async () => {
|
|
431
|
+
const options = buildProductTransformOptions({ authentication: ADMIN_PANEL_AUTH })
|
|
432
|
+
const giftCards = bootTableRoute('teleport_gift_cards', options.trustedReaderRoles || [])
|
|
433
|
+
expect((await giftCards({ handler: 'handler', session: { role: 'admin' } })).status).toBe(200)
|
|
434
|
+
expect((await giftCards({ handler: 'handler', session: { role: 'user' } })).status).toBe(403)
|
|
435
|
+
})
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
describe('resolveTrustedReaderRoles', () => {
|
|
439
|
+
it('reads the roles off the admin folder and the admin pages, and nothing else', () => {
|
|
440
|
+
const roles = TableAccess.resolveTrustedReaderRoles({
|
|
441
|
+
enabled: true,
|
|
442
|
+
dataSourceId: 'ds-1',
|
|
443
|
+
dataSourceType: 'teleport',
|
|
444
|
+
passwordAuthEnabled: true,
|
|
445
|
+
providers: [],
|
|
446
|
+
roles: ['admin', 'staff', 'user'],
|
|
447
|
+
tables: {},
|
|
448
|
+
pageProtection: {
|
|
449
|
+
p1: {
|
|
450
|
+
requiresAuth: true,
|
|
451
|
+
allowedRoles: ['admin'],
|
|
452
|
+
pageName: 'dashboard',
|
|
453
|
+
route: '/admin/dashboard',
|
|
454
|
+
},
|
|
455
|
+
p2: {
|
|
456
|
+
requiresAuth: true,
|
|
457
|
+
allowedRoles: ['staff'],
|
|
458
|
+
pageName: 'orders',
|
|
459
|
+
route: '/admin/orders',
|
|
460
|
+
},
|
|
461
|
+
p3: { requiresAuth: true, allowedRoles: ['user'], pageName: 'profile', route: '/profile' },
|
|
462
|
+
p4: {
|
|
463
|
+
requiresAuth: true,
|
|
464
|
+
allowedRoles: ['user'],
|
|
465
|
+
pageName: 'administration',
|
|
466
|
+
route: '/administration',
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
folderProtection: {
|
|
470
|
+
f1: {
|
|
471
|
+
requiresAuth: true,
|
|
472
|
+
allowedRoles: ['admin'],
|
|
473
|
+
folderName: 'admin-panel',
|
|
474
|
+
parentId: null,
|
|
475
|
+
children: {},
|
|
476
|
+
},
|
|
477
|
+
f2: {
|
|
478
|
+
requiresAuth: true,
|
|
479
|
+
allowedRoles: ['user'],
|
|
480
|
+
folderName: 'members',
|
|
481
|
+
parentId: null,
|
|
482
|
+
children: {},
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
authPages: {},
|
|
486
|
+
callbackBaseUrl: '',
|
|
487
|
+
envKeys: {},
|
|
488
|
+
customUserProperties: [],
|
|
489
|
+
})
|
|
490
|
+
expect(roles).toEqual(['admin', 'staff'])
|
|
491
|
+
expect(TableAccess.resolveTrustedReaderRoles(undefined)).toEqual([])
|
|
492
|
+
})
|
|
493
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SQL an `array_overlap` filter compiles to: "the row's list shares any
|
|
3
|
+
* value with these".
|
|
4
|
+
*
|
|
5
|
+
* A list column is TEXT in the platform tables, and holds whatever wrote it:
|
|
6
|
+
*
|
|
7
|
+
* - a JSON array — `["react","css"]` — which is what the editor writes;
|
|
8
|
+
* - a comma-separated list — `react, css` — which is what a person types
|
|
9
|
+
* into the generated admin's plain text field;
|
|
10
|
+
* - a PostgreSQL array literal — `{react,css}` — from a native array column.
|
|
11
|
+
*
|
|
12
|
+
* Casting every cell to `jsonb` (what this used to do) threw on the second
|
|
13
|
+
* kind, and one hand-typed row failed the WHOLE listing query — every visitor
|
|
14
|
+
* saw an empty blog. Each form is now read as the text array it means, and a
|
|
15
|
+
* blank cell as no list at all (it matches nothing, fails nothing).
|
|
16
|
+
*
|
|
17
|
+
* Emitted as plain source with no backtick, no `${` and no backslash, because
|
|
18
|
+
* the fetchers interpolate it into their own template literals.
|
|
19
|
+
*/
|
|
20
|
+
export declare const generateArrayOverlapSqlCode: () => string;
|
|
21
|
+
//# sourceMappingURL=array-overlap-sql.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"array-overlap-sql.d.ts","sourceRoot":"","sources":["../../src/array-overlap-sql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,2BAA2B,QAAO,MAqB9C,CAAA"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateArrayOverlapSqlCode = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The SQL an `array_overlap` filter compiles to: "the row's list shares any
|
|
6
|
+
* value with these".
|
|
7
|
+
*
|
|
8
|
+
* A list column is TEXT in the platform tables, and holds whatever wrote it:
|
|
9
|
+
*
|
|
10
|
+
* - a JSON array — `["react","css"]` — which is what the editor writes;
|
|
11
|
+
* - a comma-separated list — `react, css` — which is what a person types
|
|
12
|
+
* into the generated admin's plain text field;
|
|
13
|
+
* - a PostgreSQL array literal — `{react,css}` — from a native array column.
|
|
14
|
+
*
|
|
15
|
+
* Casting every cell to `jsonb` (what this used to do) threw on the second
|
|
16
|
+
* kind, and one hand-typed row failed the WHOLE listing query — every visitor
|
|
17
|
+
* saw an empty blog. Each form is now read as the text array it means, and a
|
|
18
|
+
* blank cell as no list at all (it matches nothing, fails nothing).
|
|
19
|
+
*
|
|
20
|
+
* Emitted as plain source with no backtick, no `${` and no backslash, because
|
|
21
|
+
* the fetchers interpolate it into their own template literals.
|
|
22
|
+
*/
|
|
23
|
+
var generateArrayOverlapSqlCode = function () { return "\nfunction listColumnSql(column) {\n var cell = \"BTRIM(COALESCE(\" + column + \"::text, ''))\"\n return (\n '(CASE' +\n \" WHEN \" + cell + \" LIKE '[%' THEN ARRAY(SELECT jsonb_array_elements_text(\" + cell + '::jsonb))' +\n \" WHEN \" + cell + \" LIKE '{%' THEN \" + cell + '::text[]' +\n \" ELSE regexp_split_to_array(NULLIF(\" + cell + \", ''), '[[:space:]]*,[[:space:]]*')\" +\n ' END)'\n )\n}\n\n// Any of the columns (a translatable list and its per-language copies) sharing\n// a value with the parameter.\nfunction arrayOverlapSql(columns, param) {\n var clauses = []\n for (var i = 0; i < columns.length; i++) {\n clauses.push(listColumnSql(columns[i]) + ' && ' + param + '::text[]')\n }\n return clauses.length === 1 ? clauses[0] : '(' + clauses.join(' OR ') + ')'\n}\n"; };
|
|
24
|
+
exports.generateArrayOverlapSqlCode = generateArrayOverlapSqlCode;
|
|
25
|
+
//# sourceMappingURL=array-overlap-sql.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"array-overlap-sql.js","sourceRoot":"","sources":["../../src/array-overlap-sql.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACI,IAAM,2BAA2B,GAAG,cAAc,OAAA,uyBAqBxD,EArBwD,CAqBxD,CAAA;AArBY,QAAA,2BAA2B,+BAqBvC"}
|