@teleporthq/teleport-plugin-next-data-source 0.43.67 → 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-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__/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__/items-path.test.ts +150 -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__/rest-api-response-cache.test.ts +434 -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/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/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/postgresql.d.ts +1 -1
- package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
- package/dist/cjs/fetchers/postgresql.js +8 -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/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 +8 -5
- package/dist/cjs/fetchers/teleport.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/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/localized-columns.d.ts.map +1 -1
- package/dist/cjs/localized-columns.js +13 -0
- package/dist/cjs/localized-columns.js.map +1 -1
- package/dist/cjs/pagination-plugin.d.ts.map +1 -1
- package/dist/cjs/pagination-plugin.js +17 -5
- package/dist/cjs/pagination-plugin.js.map +1 -1
- 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 +47 -4
- package/dist/cjs/transformations/index.d.ts.map +1 -1
- package/dist/cjs/transformations/index.js +56 -18
- 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.map +1 -1
- package/dist/cjs/utils.js +1 -1
- 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/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/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/postgresql.d.ts +1 -1
- package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
- package/dist/esm/fetchers/postgresql.js +8 -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/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 +8 -5
- package/dist/esm/fetchers/teleport.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/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/localized-columns.d.ts.map +1 -1
- package/dist/esm/localized-columns.js +13 -0
- package/dist/esm/localized-columns.js.map +1 -1
- package/dist/esm/pagination-plugin.d.ts.map +1 -1
- package/dist/esm/pagination-plugin.js +17 -5
- package/dist/esm/pagination-plugin.js.map +1 -1
- 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 +47 -4
- package/dist/esm/transformations/index.d.ts.map +1 -1
- package/dist/esm/transformations/index.js +56 -18
- 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.map +1 -1
- package/dist/esm/utils.js +1 -1
- package/dist/esm/utils.js.map +1 -1
- package/package.json +5 -5
- package/src/array-overlap-sql.ts +41 -0
- package/src/data-source-fetchers.ts +41 -5
- package/src/fetchers/javascript.ts +9 -1
- package/src/fetchers/postgresql.ts +37 -10
- package/src/fetchers/raw-query.ts +60 -5
- package/src/fetchers/rest-api.ts +27 -5
- package/src/fetchers/teleport.ts +37 -13
- 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/items-path.ts +74 -0
- package/src/localized-columns.ts +13 -0
- package/src/pagination-plugin.ts +29 -7
- 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 +165 -22
- package/src/transformations/shared-utils.ts +6 -0
- package/src/utils.ts +26 -4
|
@@ -34,8 +34,9 @@ import {
|
|
|
34
34
|
import { validateDatabaseConfig } from './validation'
|
|
35
35
|
import { generateCountFetcher } from './count-fetchers'
|
|
36
36
|
import type { EntityTransformOptions } from './transformations'
|
|
37
|
-
import { DataCache } from '@teleporthq/teleport-shared'
|
|
37
|
+
import { DataCache, TableAccess } from '@teleporthq/teleport-shared'
|
|
38
38
|
import type { DataSourceServerCacheOptions } from './cache/types'
|
|
39
|
+
import { isViewDependentTable } from './fetchers/utils/browser-row-policy'
|
|
39
40
|
|
|
40
41
|
export { generateRawQueryFetcher, parseQueryTemplateVariables }
|
|
41
42
|
|
|
@@ -108,7 +109,7 @@ export function generateDataSourceFetcher(
|
|
|
108
109
|
if (!validation.isValid) {
|
|
109
110
|
throw new Error(`PostgreSQL/CockroachDB config validation failed: ${validation.error}`)
|
|
110
111
|
}
|
|
111
|
-
return generatePostgreSQLFetcher(config, tableName)
|
|
112
|
+
return generatePostgreSQLFetcher(config, tableName, transformOptions.trustedReaderRoles)
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
case 'mysql':
|
|
@@ -185,7 +186,7 @@ export function generateDataSourceFetcher(
|
|
|
185
186
|
`Supabase (PostgreSQL fallback) config validation failed: ${pgValidation.error}`
|
|
186
187
|
)
|
|
187
188
|
}
|
|
188
|
-
return generatePostgreSQLFetcher(config, tableName)
|
|
189
|
+
return generatePostgreSQLFetcher(config, tableName, transformOptions.trustedReaderRoles)
|
|
189
190
|
}
|
|
190
191
|
|
|
191
192
|
const validation = validateSupabaseConfig(config)
|
|
@@ -301,6 +302,9 @@ export function generateDataSourceFetcherWithCore(
|
|
|
301
302
|
const cachePreamble = cacheOptions
|
|
302
303
|
? `import { tqWithCache } from '${isApiRoute ? '../../' : '../'}tq-cache/server'`
|
|
303
304
|
: ''
|
|
305
|
+
// A module carrying the browser row policy (the Postgres-family routes) keys
|
|
306
|
+
// its cache by who is reading; see `__tqCacheByView` below.
|
|
307
|
+
const splitsCacheByView = handlerCode.indexOf('function __brpSharesPublicView(') !== -1
|
|
304
308
|
|
|
305
309
|
const cacheWiring = cacheOptions
|
|
306
310
|
? `
|
|
@@ -322,8 +326,35 @@ const __tqCountCache = ${JSON.stringify({
|
|
|
322
326
|
swr: cacheOptions.staleWhileRevalidate || 0,
|
|
323
327
|
})}
|
|
324
328
|
|
|
325
|
-
|
|
326
|
-
|
|
329
|
+
${
|
|
330
|
+
splitsCacheByView
|
|
331
|
+
? `// The store's own server code and its administrators may be served rows a
|
|
332
|
+
// visitor is not (see __brpSharesPublicView), so they read and fill a cache of
|
|
333
|
+
// their own — one never marked for a shared (CDN) cache either.
|
|
334
|
+
const __tqViewDependent = ${JSON.stringify(
|
|
335
|
+
isViewDependentTable(tableName, [
|
|
336
|
+
...TableAccess.PROTECTED_TABLES,
|
|
337
|
+
...Object.keys(TableAccess.PUBLIC_FEED_TABLES),
|
|
338
|
+
])
|
|
339
|
+
)}
|
|
340
|
+
function __tqCacheByView(fn, opts) {
|
|
341
|
+
const publicView = tqWithCache(fn, opts)
|
|
342
|
+
const trustedView = tqWithCache(fn, Object.assign({}, opts, {
|
|
343
|
+
scope: opts.scope + ':trusted',
|
|
344
|
+
versionScope: opts.versionScope || opts.scope,
|
|
345
|
+
sMaxAge: 0,
|
|
346
|
+
swr: 0,
|
|
347
|
+
}))
|
|
348
|
+
return async function (req, res) {
|
|
349
|
+
return (await __brpSharesPublicView(req, __tqViewDependent)) ? publicView(req, res) : trustedView(req, res)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const cachedHandler = __tqCacheByView(handler, __tqDataCache)
|
|
354
|
+
const cachedGetCount = __tqCacheByView(getCount, __tqCountCache)`
|
|
355
|
+
: `const cachedHandler = tqWithCache(handler, __tqDataCache)
|
|
356
|
+
const cachedGetCount = tqWithCache(getCount, __tqCountCache)`
|
|
357
|
+
}
|
|
327
358
|
`
|
|
328
359
|
: ''
|
|
329
360
|
|
|
@@ -349,9 +380,13 @@ export default { fetchData, fetchCount, handler: ${handlerExport}, getCount: ${c
|
|
|
349
380
|
return `${[...allImports, cachePreamble].filter(Boolean).join('\n')}
|
|
350
381
|
|
|
351
382
|
async function fetchData(params = {}) {
|
|
383
|
+
// A server-side call from this process (getStaticProps, a page-load
|
|
384
|
+
// workflow): the money-table guard trusts the marker, which no HTTP request
|
|
385
|
+
// can carry.
|
|
352
386
|
const req = {
|
|
353
387
|
query: params,
|
|
354
388
|
method: 'GET',
|
|
389
|
+
__tqServerCall: true,
|
|
355
390
|
}
|
|
356
391
|
|
|
357
392
|
let result = null
|
|
@@ -381,6 +416,7 @@ async function fetchCount(params = {}) {
|
|
|
381
416
|
const req = {
|
|
382
417
|
query: params,
|
|
383
418
|
method: 'GET',
|
|
419
|
+
__tqServerCall: true,
|
|
384
420
|
}
|
|
385
421
|
|
|
386
422
|
let result = null
|
|
@@ -56,11 +56,15 @@ ${generateCollectionPathHelperCode()}
|
|
|
56
56
|
|
|
57
57
|
export default async function handler(req, res) {
|
|
58
58
|
try {
|
|
59
|
-
const { limit, offset, page, perPage, query, queryColumns, sortBy, sortOrder, filters, sorts, collectionPath } = req.query
|
|
59
|
+
const { limit, offset, page, perPage, query, queryColumns, sortBy, sortOrder, filters, sorts, collectionPath, itemsPath } = req.query
|
|
60
60
|
|
|
61
61
|
const code = ${JSON.stringify(jsConfig.code)}
|
|
62
62
|
const executeCode = new Function('return ' + code)
|
|
63
63
|
let data = resolveCollectionPath(executeCode(), collectionPath)
|
|
64
|
+
const itemsEnvelope = openItemsEnvelope(data, itemsPath)
|
|
65
|
+
if (itemsEnvelope) {
|
|
66
|
+
data = itemsEnvelope.items
|
|
67
|
+
}
|
|
64
68
|
|
|
65
69
|
if (Array.isArray(data)) {
|
|
66
70
|
if (query && query.trim()) {
|
|
@@ -192,6 +196,10 @@ export default async function handler(req, res) {
|
|
|
192
196
|
}
|
|
193
197
|
}
|
|
194
198
|
|
|
199
|
+
if (itemsEnvelope) {
|
|
200
|
+
data = itemsEnvelope.close(data)
|
|
201
|
+
}
|
|
202
|
+
|
|
195
203
|
const safeData = JSON.parse(JSON.stringify(data, dateReplacer))
|
|
196
204
|
|
|
197
205
|
return res.status(200).json({
|
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
generateSortTiebreakSql,
|
|
12
12
|
} from '../product-price-sort'
|
|
13
13
|
import { generateProductFilterClauseHelper } from '../product-filter-fields'
|
|
14
|
+
import { generateReadGuardCall, generateTableAccessPreamble } from './utils/table-access-guard'
|
|
15
|
+
import { generateRequestSqlGuardsCode } from './utils/request-sql-guards'
|
|
16
|
+
import { generateArrayOverlapSqlCode } from '../array-overlap-sql'
|
|
14
17
|
|
|
15
18
|
interface PostgreSQLConfig {
|
|
16
19
|
connectionString?: string
|
|
@@ -27,7 +30,8 @@ interface PostgreSQLConfig {
|
|
|
27
30
|
|
|
28
31
|
export const generatePostgreSQLFetcher = (
|
|
29
32
|
config: Record<string, unknown>,
|
|
30
|
-
tableName: string
|
|
33
|
+
tableName: string,
|
|
34
|
+
trustedReaderRoles: ReadonlyArray<string> = []
|
|
31
35
|
): string => {
|
|
32
36
|
const pgConfig = config as PostgreSQLConfig
|
|
33
37
|
const schema = pgConfig.options?.schema
|
|
@@ -58,6 +62,8 @@ export const generatePostgreSQLFetcher = (
|
|
|
58
62
|
|
|
59
63
|
return `import { Client } from 'pg'
|
|
60
64
|
|
|
65
|
+
${generateTableAccessPreamble(trustedReaderRoles)}
|
|
66
|
+
|
|
61
67
|
const getClient = () => {
|
|
62
68
|
return new Client(${clientConfig})
|
|
63
69
|
}
|
|
@@ -67,9 +73,11 @@ ${generateSafeJSONParseCode()}
|
|
|
67
73
|
${generateFilterTreeHelpersCode()}
|
|
68
74
|
|
|
69
75
|
${generateSearchEscapeHelpersCode()}
|
|
76
|
+
${generateArrayOverlapSqlCode()}
|
|
70
77
|
|
|
71
78
|
// Builds one SQL clause for the whole filter tree, so an OR group nested in
|
|
72
79
|
// the root AND keeps its meaning instead of being flattened into ANDs.
|
|
80
|
+
${generateRequestSqlGuardsCode()}
|
|
73
81
|
const processFilters = (filters, conditions, queryParams, paramIndex) => {
|
|
74
82
|
if (!filters) return paramIndex
|
|
75
83
|
|
|
@@ -91,11 +99,13 @@ const processFilters = (filters, conditions, queryParams, paramIndex) => {
|
|
|
91
99
|
return '$' + paramIndex++
|
|
92
100
|
})
|
|
93
101
|
if (productClause !== null) return productClause
|
|
102
|
+
assertRequestColumn(field, 'filter field')
|
|
94
103
|
|
|
95
104
|
if (Array.isArray(value)) {
|
|
96
105
|
if (value.length === 0) return null
|
|
97
106
|
if (operand === 'array_overlap') {
|
|
98
|
-
|
|
107
|
+
// The row's list (see array-overlap-sql.ts) shares any value with the set.
|
|
108
|
+
const clause = arrayOverlapSql([field], '$' + paramIndex)
|
|
99
109
|
queryParams.push(value.map((entry) => String(entry)))
|
|
100
110
|
paramIndex++
|
|
101
111
|
return clause
|
|
@@ -113,7 +123,7 @@ const processFilters = (filters, conditions, queryParams, paramIndex) => {
|
|
|
113
123
|
// ?categoryFilter=a,b,c) expands to multiple ids; one id stays one.
|
|
114
124
|
const overlapValues = String(value).split(',').map((entry) => entry.trim()).filter(Boolean)
|
|
115
125
|
if (overlapValues.length === 0) return null
|
|
116
|
-
const clause =
|
|
126
|
+
const clause = arrayOverlapSql([field], '$' + paramIndex)
|
|
117
127
|
queryParams.push(overlapValues)
|
|
118
128
|
paramIndex++
|
|
119
129
|
return clause
|
|
@@ -148,6 +158,7 @@ ${generateSortFallbackFieldHelper(tableName)}
|
|
|
148
158
|
${generateProductFilterClauseHelper(tableName)}
|
|
149
159
|
|
|
150
160
|
export default async function handler(req, res) {
|
|
161
|
+
${generateReadGuardCall(tableName, null)}
|
|
151
162
|
const client = getClient()
|
|
152
163
|
|
|
153
164
|
try {
|
|
@@ -155,6 +166,8 @@ export default async function handler(req, res) {
|
|
|
155
166
|
${schema ? `await client.query('SET search_path TO ${schema}')` : ''}
|
|
156
167
|
|
|
157
168
|
const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset } = req.query
|
|
169
|
+
// A visitor never filters, sorts or searches by a column it is not served.
|
|
170
|
+
if (__rowPolicy) __brpAssertVisibleFields(__rowPolicy, req.query)
|
|
158
171
|
|
|
159
172
|
const conditions = []
|
|
160
173
|
const queryParams = []
|
|
@@ -189,6 +202,7 @@ export default async function handler(req, res) {
|
|
|
189
202
|
// Continue without search if we can't get columns
|
|
190
203
|
}
|
|
191
204
|
}
|
|
205
|
+
if (__rowPolicy) columns = __brpVisibleColumns(__rowPolicy, columns)
|
|
192
206
|
|
|
193
207
|
if (columns.length > 0) {
|
|
194
208
|
const pattern = '%' + escapeLikePattern(query) + '%'
|
|
@@ -204,6 +218,7 @@ export default async function handler(req, res) {
|
|
|
204
218
|
|
|
205
219
|
// Apply filters using helper function
|
|
206
220
|
paramIndex = processFilters(filters, conditions, queryParams, paramIndex)
|
|
221
|
+
if (__rowPolicy) conditions.push('(' + __rowPolicy.predicate + ')')
|
|
207
222
|
|
|
208
223
|
let sql = \`SELECT * FROM "${tableName}"\`
|
|
209
224
|
|
|
@@ -219,6 +234,7 @@ export default async function handler(req, res) {
|
|
|
219
234
|
const parsedSorts = safeJSONParse(sorts)
|
|
220
235
|
if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
|
|
221
236
|
const valid = parsedSorts.filter((sort) => sort && sort.field)
|
|
237
|
+
valid.forEach((sort) => assertRequestColumn(sort.field, 'sort field'))
|
|
222
238
|
const orderOf = (sort) => (sort.order || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'
|
|
223
239
|
const orderClauses = valid.map((sort) => \`\${sortFieldSql(sort.field)} \${orderOf(sort)}\`)
|
|
224
240
|
const plainClauses = valid.map((sort) => \`\${sortFallbackField(sort.field)} \${orderOf(sort)}\`)
|
|
@@ -236,16 +252,17 @@ export default async function handler(req, res) {
|
|
|
236
252
|
}
|
|
237
253
|
}
|
|
238
254
|
} else if (sortBy) {
|
|
255
|
+
assertRequestColumn(sortBy, 'sort field')
|
|
239
256
|
orderBySql = \` ORDER BY \${sortBy} \${(sortOrder || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'}\`
|
|
240
257
|
plainOrderBySql = orderBySql
|
|
241
258
|
}
|
|
242
259
|
const usedDiscountAwareSort = orderBySql !== plainOrderBySql
|
|
243
260
|
|
|
244
|
-
const limitValue = limit || perPage
|
|
261
|
+
const limitValue = requestRowCount(limit || perPage)
|
|
245
262
|
const offsetValue = offset !== undefined ? parseInt(offset) : (page && perPage ? (parseInt(page) - 1) * parseInt(perPage) : undefined)
|
|
246
263
|
|
|
247
264
|
let sqlTail = ''
|
|
248
|
-
if (limitValue) {
|
|
265
|
+
if (limitValue !== undefined) {
|
|
249
266
|
sqlTail += \` LIMIT \${limitValue}\`
|
|
250
267
|
}
|
|
251
268
|
|
|
@@ -279,14 +296,20 @@ export default async function handler(req, res) {
|
|
|
279
296
|
|
|
280
297
|
return res.status(200).json({
|
|
281
298
|
success: true,
|
|
282
|
-
data:
|
|
299
|
+
data: __taWithoutCredentials(
|
|
300
|
+
__rowPolicy ? __brpStripHidden(__rowPolicy, safeData) : safeData,
|
|
301
|
+
${JSON.stringify(tableName)}
|
|
302
|
+
),
|
|
283
303
|
timestamp: Date.now()
|
|
284
304
|
})
|
|
285
305
|
} catch (error) {
|
|
286
306
|
console.error('PostgreSQL fetch error:', error)
|
|
287
|
-
|
|
307
|
+
// A database error names tables, columns and values: it stays in the server
|
|
308
|
+
// log. Only an error this route raised for the caller (it carries a status)
|
|
309
|
+
// is repeated to it.
|
|
310
|
+
return res.status(error.status || 500).json({
|
|
288
311
|
success: false,
|
|
289
|
-
error: error.message
|
|
312
|
+
error: error.status ? error.message : 'Failed to fetch data',
|
|
290
313
|
timestamp: Date.now()
|
|
291
314
|
})
|
|
292
315
|
} finally {
|
|
@@ -311,11 +334,13 @@ export const generatePostgreSQLCountFetcher = (
|
|
|
311
334
|
|
|
312
335
|
return `
|
|
313
336
|
async function getCount(req, res) {
|
|
337
|
+
${generateReadGuardCall(tableName, null)}
|
|
314
338
|
const client = getClient()
|
|
315
339
|
|
|
316
340
|
try {
|
|
317
341
|
await client.connect()
|
|
318
342
|
const { query, queryColumns, filters } = req.query
|
|
343
|
+
if (__rowPolicy) __brpAssertVisibleFields(__rowPolicy, req.query)
|
|
319
344
|
const conditions = []
|
|
320
345
|
const queryParams = []
|
|
321
346
|
let paramIndex = 1
|
|
@@ -350,6 +375,7 @@ async function getCount(req, res) {
|
|
|
350
375
|
// Continue without search if we can't get columns
|
|
351
376
|
}
|
|
352
377
|
}
|
|
378
|
+
if (__rowPolicy) columns = __brpVisibleColumns(__rowPolicy, columns)
|
|
353
379
|
|
|
354
380
|
if (columns.length > 0) {
|
|
355
381
|
const pattern = '%' + escapeLikePattern(query) + '%'
|
|
@@ -367,6 +393,7 @@ async function getCount(req, res) {
|
|
|
367
393
|
|
|
368
394
|
// Apply filters using helper function
|
|
369
395
|
paramIndex = processFilters(filters, conditions, queryParams, paramIndex)
|
|
396
|
+
if (__rowPolicy) conditions.push('(' + __rowPolicy.predicate + ')')
|
|
370
397
|
|
|
371
398
|
let countSql = \`SELECT COUNT(*) FROM "${tableName}"\`
|
|
372
399
|
if (conditions.length > 0) {
|
|
@@ -383,9 +410,9 @@ async function getCount(req, res) {
|
|
|
383
410
|
})
|
|
384
411
|
} catch (error) {
|
|
385
412
|
console.error('Error getting count:', error)
|
|
386
|
-
return res.status(500).json({
|
|
413
|
+
return res.status(error.status || 500).json({
|
|
387
414
|
success: false,
|
|
388
|
-
error: error.message
|
|
415
|
+
error: error.status ? error.message : 'Failed to get count',
|
|
389
416
|
timestamp: Date.now()
|
|
390
417
|
})
|
|
391
418
|
} finally {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SessionCookieResolver } from '@teleporthq/teleport-shared'
|
|
1
2
|
import { replaceSecretReference } from '../utils'
|
|
2
3
|
|
|
3
4
|
interface PostgreSQLConfig {
|
|
@@ -13,9 +14,28 @@ interface PostgreSQLConfig {
|
|
|
13
14
|
|
|
14
15
|
const FORBIDDEN_KEYWORDS = ['CREATE', 'ALTER', 'DROP', 'TRUNCATE', 'RENAME', 'GRANT', 'REVOKE']
|
|
15
16
|
|
|
17
|
+
/** The param a `{{Current User.<field>}}` token becomes: `currentUser` + the field, capitalised. */
|
|
18
|
+
const CURRENT_USER_PARAM_PREFIX = 'currentUser'
|
|
19
|
+
|
|
20
|
+
const isCurrentUserParam = (param: string): boolean =>
|
|
21
|
+
param.length > CURRENT_USER_PARAM_PREFIX.length && param.indexOf(CURRENT_USER_PARAM_PREFIX) === 0
|
|
22
|
+
|
|
23
|
+
/** The user field a `currentUser<Field>` param stands for (`currentUserId` → `id`). */
|
|
24
|
+
const currentUserField = (param: string): string => {
|
|
25
|
+
const rest = param.slice(CURRENT_USER_PARAM_PREFIX.length)
|
|
26
|
+
return rest.charAt(0).toLowerCase() + rest.slice(1)
|
|
27
|
+
}
|
|
28
|
+
|
|
16
29
|
/**
|
|
17
30
|
* Generates an API route handler that executes a parameterized raw SQL query.
|
|
18
|
-
* Supports {{Current User.*}} substitution
|
|
31
|
+
* Supports {{Current User.*}} substitution — bound to the SESSION's user.
|
|
32
|
+
*
|
|
33
|
+
* ⛔ A `{{Current User.<field>}}` value is read from the signed-in session on
|
|
34
|
+
* the server, never from the request: the query string is whatever the caller
|
|
35
|
+
* wrote, so a route that bound `?currentUserId=` served any user's rows (their
|
|
36
|
+
* favourites, their orders) to anyone who named that user's id. A caller with
|
|
37
|
+
* no session gets no rows, exactly as when the value is missing. Other params
|
|
38
|
+
* (`currentPageEntityId`, the details page's route param) stay request values.
|
|
19
39
|
*
|
|
20
40
|
* @param config - PostgreSQL connection config
|
|
21
41
|
* @param query - Raw SQL query string with {{Current User.*}} patterns already replaced to $N placeholders
|
|
@@ -27,9 +47,43 @@ export const generateRawQueryFetcher = (
|
|
|
27
47
|
paramFields: string[]
|
|
28
48
|
): string => {
|
|
29
49
|
const pgConfig = config as PostgreSQLConfig
|
|
50
|
+
const userParams = paramFields.filter(isCurrentUserParam)
|
|
51
|
+
const requestParams = paramFields.filter((param) => !isCurrentUserParam(param))
|
|
52
|
+
|
|
53
|
+
const paramDestructure = [
|
|
54
|
+
requestParams.length > 0 ? `const { ${requestParams.join(', ')} } = req.query` : '',
|
|
55
|
+
userParams.length > 0 ? 'const __sessionUser = await sessionUser(req)' : '',
|
|
56
|
+
...userParams.map(
|
|
57
|
+
(param) =>
|
|
58
|
+
`const ${param} = sessionUserField(__sessionUser, ${JSON.stringify(
|
|
59
|
+
currentUserField(param)
|
|
60
|
+
)})`
|
|
61
|
+
),
|
|
62
|
+
]
|
|
63
|
+
.filter(Boolean)
|
|
64
|
+
.join('\n ')
|
|
65
|
+
|
|
66
|
+
const sessionHelpers =
|
|
67
|
+
userParams.length > 0
|
|
68
|
+
? `${SessionCookieResolver.generateCommonJsSessionTokenResolverCode()}
|
|
69
|
+
// The signed-in user this request's session names, or null.
|
|
70
|
+
async function sessionUser(req) {
|
|
71
|
+
try {
|
|
72
|
+
return await __tqSessionToken(req)
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
}
|
|
30
77
|
|
|
31
|
-
|
|
32
|
-
|
|
78
|
+
// One field of that user, as the session carries it (the token is seeded from
|
|
79
|
+
// the users row), or undefined. The id is the token's subject when not copied.
|
|
80
|
+
function sessionUserField(user, field) {
|
|
81
|
+
if (!user) return undefined
|
|
82
|
+
var value = field === 'id' && user.id == null ? user.sub : user[field]
|
|
83
|
+
return value == null || value === '' ? undefined : value
|
|
84
|
+
}
|
|
85
|
+
`
|
|
86
|
+
: ''
|
|
33
87
|
|
|
34
88
|
const paramValidation =
|
|
35
89
|
paramFields.length > 0
|
|
@@ -58,7 +112,7 @@ export const generateRawQueryFetcher = (
|
|
|
58
112
|
return `import { Client } from 'pg'
|
|
59
113
|
|
|
60
114
|
const FORBIDDEN_KEYWORDS = ${JSON.stringify(FORBIDDEN_KEYWORDS)}
|
|
61
|
-
|
|
115
|
+
${sessionHelpers}
|
|
62
116
|
const getClient = () => {
|
|
63
117
|
const connStr = process.env.TELEPORT_DB_CONNECTION_STRING
|
|
64
118
|
if (connStr) {
|
|
@@ -122,10 +176,11 @@ export default async function handler(req, res) {
|
|
|
122
176
|
await client.end()
|
|
123
177
|
}
|
|
124
178
|
} catch (error) {
|
|
179
|
+
// A database error names tables, columns and values: server log only.
|
|
125
180
|
console.error('Raw query fetch error:', error)
|
|
126
181
|
return res.status(500).json({
|
|
127
182
|
success: false,
|
|
128
|
-
error:
|
|
183
|
+
error: 'Failed to fetch data',
|
|
129
184
|
timestamp: Date.now()
|
|
130
185
|
})
|
|
131
186
|
}
|
package/src/fetchers/rest-api.ts
CHANGED
|
@@ -5,6 +5,11 @@ import {
|
|
|
5
5
|
generateSafeJSONParseCode,
|
|
6
6
|
generateFilterTreeHelpersCode,
|
|
7
7
|
} from '../utils'
|
|
8
|
+
import {
|
|
9
|
+
RESTAPICacheConfig,
|
|
10
|
+
generateRESTAPIResponseCacheCode,
|
|
11
|
+
resolveRESTAPICacheTTLSeconds,
|
|
12
|
+
} from './utils/rest-api-response-cache'
|
|
8
13
|
|
|
9
14
|
export const validateRESTAPIConfig = (
|
|
10
15
|
config: Record<string, unknown>
|
|
@@ -76,11 +81,16 @@ interface RESTAPIConfig {
|
|
|
76
81
|
authorization?: Authorization
|
|
77
82
|
bodyType?: string
|
|
78
83
|
body?: string
|
|
84
|
+
cache?: RESTAPICacheConfig
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
export const generateRESTAPIFetcher = (config: Record<string, unknown>): string => {
|
|
82
88
|
const restConfig = config as RESTAPIConfig
|
|
83
89
|
const authCode = generateAuthCode(restConfig.authorization || {})
|
|
90
|
+
// Every cache-only fragment below is empty when caching is off, so an
|
|
91
|
+
// uncached data source keeps generating byte-identical output.
|
|
92
|
+
const cacheTTLSeconds = resolveRESTAPICacheTTLSeconds(restConfig.cache)
|
|
93
|
+
const cached = cacheTTLSeconds !== undefined
|
|
84
94
|
|
|
85
95
|
return `import fetch from 'node-fetch'
|
|
86
96
|
|
|
@@ -92,11 +102,13 @@ ${generateSortFilterHelperCode()}
|
|
|
92
102
|
|
|
93
103
|
${generateFilterTreeHelpersCode()}
|
|
94
104
|
|
|
95
|
-
${generateCollectionPathHelperCode()}
|
|
105
|
+
${generateCollectionPathHelperCode()}${
|
|
106
|
+
cached ? `\n\n${generateRESTAPIResponseCacheCode(cacheTTLSeconds)}` : ''
|
|
107
|
+
}
|
|
96
108
|
|
|
97
109
|
export default async function handler(req, res) {
|
|
98
110
|
try {
|
|
99
|
-
const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset, collectionPath } = req.query
|
|
111
|
+
const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset, collectionPath, itemsPath } = req.query
|
|
100
112
|
|
|
101
113
|
const url = ${JSON.stringify(restConfig.url)}
|
|
102
114
|
const method = ${JSON.stringify(restConfig.method || 'GET')}
|
|
@@ -134,7 +146,7 @@ export default async function handler(req, res) {
|
|
|
134
146
|
: ''
|
|
135
147
|
}
|
|
136
148
|
|
|
137
|
-
const response = await fetch(url, options)
|
|
149
|
+
const response = await ${cached ? 'fetchRestApiWithCache' : 'fetch'}(url, options)
|
|
138
150
|
|
|
139
151
|
if (!response.ok) {
|
|
140
152
|
return res.status(response.status).json({
|
|
@@ -144,9 +156,13 @@ export default async function handler(req, res) {
|
|
|
144
156
|
})
|
|
145
157
|
}
|
|
146
158
|
|
|
147
|
-
let data = await response.json()
|
|
159
|
+
let data = ${cached ? 'JSON.parse(response.bodyText)' : 'await response.json()'}
|
|
148
160
|
|
|
149
161
|
data = resolveCollectionPath(data, collectionPath)
|
|
162
|
+
const itemsEnvelope = openItemsEnvelope(data, itemsPath)
|
|
163
|
+
if (itemsEnvelope) {
|
|
164
|
+
data = itemsEnvelope.items
|
|
165
|
+
}
|
|
150
166
|
|
|
151
167
|
if (Array.isArray(data)) {
|
|
152
168
|
if (query && query.trim()) {
|
|
@@ -278,9 +294,15 @@ export default async function handler(req, res) {
|
|
|
278
294
|
}
|
|
279
295
|
}
|
|
280
296
|
|
|
297
|
+
if (itemsEnvelope) {
|
|
298
|
+
data = itemsEnvelope.close(data)
|
|
299
|
+
}
|
|
300
|
+
|
|
281
301
|
const safeData = JSON.parse(JSON.stringify(data, dateReplacer))
|
|
282
302
|
|
|
283
|
-
|
|
303
|
+
${
|
|
304
|
+
cached ? 'applyRestApiCacheHeaders(req, res, response.fetchedAt)\n \n ' : ''
|
|
305
|
+
}return res.status(200).json({
|
|
284
306
|
success: true,
|
|
285
307
|
data: safeData,
|
|
286
308
|
timestamp: Date.now()
|