@teleporthq/teleport-plugin-next-data-source 0.44.14 → 0.44.17
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__/_helpers/exposure.ts +29 -0
- package/__tests__/_helpers/first-page-runtime.ts +12 -2
- package/__tests__/blog-post-contents.test.ts +3 -1
- package/__tests__/blog-post-page-enrichment.test.ts +2 -1
- package/__tests__/blog-post-seo-fields.test.ts +124 -0
- package/__tests__/browser-row-policy.test.ts +65 -5
- package/__tests__/data-source-cache-wrapping.test.ts +48 -7
- package/__tests__/external-read-routes-guard.test.ts +991 -0
- package/__tests__/null-filter-where.test.ts +19 -2
- package/__tests__/range-control.test.ts +29 -0
- package/__tests__/search-relevance.test.ts +42 -6
- package/__tests__/table-read-route-protected-tables.test.ts +54 -4
- package/dist/cjs/count-fetchers.d.ts.map +1 -1
- package/dist/cjs/count-fetchers.js +1 -1
- package/dist/cjs/count-fetchers.js.map +1 -1
- package/dist/cjs/data-source-fetchers.d.ts +4 -0
- package/dist/cjs/data-source-fetchers.d.ts.map +1 -1
- package/dist/cjs/data-source-fetchers.js +97 -20
- package/dist/cjs/data-source-fetchers.js.map +1 -1
- package/dist/cjs/fetchers/postgresql.d.ts +3 -1
- package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
- package/dist/cjs/fetchers/postgresql.js +11 -3
- package/dist/cjs/fetchers/postgresql.js.map +1 -1
- package/dist/cjs/fetchers/raw-query.d.ts +20 -1
- package/dist/cjs/fetchers/raw-query.d.ts.map +1 -1
- package/dist/cjs/fetchers/raw-query.js +37 -7
- 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 +2 -1
- package/dist/cjs/fetchers/redshift.js.map +1 -1
- package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
- package/dist/cjs/fetchers/teleport.js +5 -3
- package/dist/cjs/fetchers/teleport.js.map +1 -1
- package/dist/cjs/fetchers/utils/external-read-guard.d.ts +53 -0
- package/dist/cjs/fetchers/utils/external-read-guard.d.ts.map +1 -0
- package/dist/cjs/fetchers/utils/external-read-guard.js +126 -0
- package/dist/cjs/fetchers/utils/external-read-guard.js.map +1 -0
- package/dist/cjs/fetchers/utils/table-access-guard.d.ts +76 -12
- package/dist/cjs/fetchers/utils/table-access-guard.d.ts.map +1 -1
- package/dist/cjs/fetchers/utils/table-access-guard.js +123 -15
- package/dist/cjs/fetchers/utils/table-access-guard.js.map +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/pagination-controls.d.ts +4 -1
- package/dist/cjs/pagination-controls.d.ts.map +1 -1
- package/dist/cjs/pagination-controls.js +4 -1
- package/dist/cjs/pagination-controls.js.map +1 -1
- package/dist/cjs/pagination-plugin.js +1 -0
- package/dist/cjs/pagination-plugin.js.map +1 -1
- package/dist/cjs/range-control.d.ts +10 -5
- package/dist/cjs/range-control.d.ts.map +1 -1
- package/dist/cjs/range-control.js +23 -2
- package/dist/cjs/range-control.js.map +1 -1
- package/dist/cjs/transformations/blog-post.d.ts +5 -0
- package/dist/cjs/transformations/blog-post.d.ts.map +1 -1
- package/dist/cjs/transformations/blog-post.js +6 -2
- package/dist/cjs/transformations/blog-post.js.map +1 -1
- package/dist/cjs/transformations/index.d.ts +18 -2
- package/dist/cjs/transformations/index.d.ts.map +1 -1
- package/dist/cjs/transformations/index.js +26 -1
- package/dist/cjs/transformations/index.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/count-fetchers.d.ts.map +1 -1
- package/dist/esm/count-fetchers.js +1 -1
- package/dist/esm/count-fetchers.js.map +1 -1
- package/dist/esm/data-source-fetchers.d.ts +4 -0
- package/dist/esm/data-source-fetchers.d.ts.map +1 -1
- package/dist/esm/data-source-fetchers.js +97 -20
- package/dist/esm/data-source-fetchers.js.map +1 -1
- package/dist/esm/fetchers/postgresql.d.ts +3 -1
- package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
- package/dist/esm/fetchers/postgresql.js +12 -4
- package/dist/esm/fetchers/postgresql.js.map +1 -1
- package/dist/esm/fetchers/raw-query.d.ts +20 -1
- package/dist/esm/fetchers/raw-query.d.ts.map +1 -1
- package/dist/esm/fetchers/raw-query.js +38 -8
- 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 +2 -1
- package/dist/esm/fetchers/redshift.js.map +1 -1
- package/dist/esm/fetchers/teleport.d.ts.map +1 -1
- package/dist/esm/fetchers/teleport.js +6 -4
- package/dist/esm/fetchers/teleport.js.map +1 -1
- package/dist/esm/fetchers/utils/external-read-guard.d.ts +53 -0
- package/dist/esm/fetchers/utils/external-read-guard.d.ts.map +1 -0
- package/dist/esm/fetchers/utils/external-read-guard.js +118 -0
- package/dist/esm/fetchers/utils/external-read-guard.js.map +1 -0
- package/dist/esm/fetchers/utils/table-access-guard.d.ts +76 -12
- package/dist/esm/fetchers/utils/table-access-guard.d.ts.map +1 -1
- package/dist/esm/fetchers/utils/table-access-guard.js +119 -15
- package/dist/esm/fetchers/utils/table-access-guard.js.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/pagination-controls.d.ts +4 -1
- package/dist/esm/pagination-controls.d.ts.map +1 -1
- package/dist/esm/pagination-controls.js +4 -1
- package/dist/esm/pagination-controls.js.map +1 -1
- package/dist/esm/pagination-plugin.js +1 -0
- package/dist/esm/pagination-plugin.js.map +1 -1
- package/dist/esm/range-control.d.ts +10 -5
- package/dist/esm/range-control.d.ts.map +1 -1
- package/dist/esm/range-control.js +23 -2
- package/dist/esm/range-control.js.map +1 -1
- package/dist/esm/transformations/blog-post.d.ts +5 -0
- package/dist/esm/transformations/blog-post.d.ts.map +1 -1
- package/dist/esm/transformations/blog-post.js +6 -2
- package/dist/esm/transformations/blog-post.js.map +1 -1
- package/dist/esm/transformations/index.d.ts +18 -2
- package/dist/esm/transformations/index.d.ts.map +1 -1
- package/dist/esm/transformations/index.js +27 -2
- package/dist/esm/transformations/index.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/src/count-fetchers.ts +10 -2
- package/src/data-source-fetchers.ts +144 -23
- package/src/fetchers/postgresql.ts +38 -9
- package/src/fetchers/raw-query.ts +125 -13
- package/src/fetchers/redshift.ts +3 -0
- package/src/fetchers/teleport.ts +29 -8
- package/src/fetchers/utils/external-read-guard.ts +289 -0
- package/src/fetchers/utils/table-access-guard.ts +469 -20
- package/src/index.ts +1 -1
- package/src/pagination-controls.ts +4 -1
- package/src/pagination-plugin.ts +1 -0
- package/src/range-control.ts +40 -8
- package/src/transformations/blog-post.ts +107 -4
- package/src/transformations/index.ts +44 -1
package/src/fetchers/teleport.ts
CHANGED
|
@@ -17,7 +17,12 @@ import { generateRowOrderTiebreakHelper, rowIdentityColumn } from '../row-order'
|
|
|
17
17
|
import { generateProductFilterClauseHelper } from '../product-filter-fields'
|
|
18
18
|
import { collectLocalizedColumns, generateLocalizedColumnsHelper } from '../localized-columns'
|
|
19
19
|
import { REQUEST_LOCALE_PARAM } from '../request-locale'
|
|
20
|
-
import {
|
|
20
|
+
import { DataExposure } from '@teleporthq/teleport-shared'
|
|
21
|
+
import {
|
|
22
|
+
generateReadGuardCall,
|
|
23
|
+
generateTableAccessPreamble,
|
|
24
|
+
sqlRouteFieldPolicy,
|
|
25
|
+
} from './utils/table-access-guard'
|
|
21
26
|
import { generateRequestSqlGuardsCode } from './utils/request-sql-guards'
|
|
22
27
|
import { generateArrayOverlapSqlCode } from '../array-overlap-sql'
|
|
23
28
|
import { generateSearchSqlHelpersCode, PG_SEARCH_SQL } from '../search-relevance'
|
|
@@ -107,7 +112,14 @@ export const generateTeleportFetcher = (
|
|
|
107
112
|
|
|
108
113
|
return `${PG_CLIENT_IMPORT}
|
|
109
114
|
|
|
110
|
-
${generateTableAccessPreamble(
|
|
115
|
+
${generateTableAccessPreamble(
|
|
116
|
+
transformOptions.trustedReaderRoles || [],
|
|
117
|
+
transformOptions.routeExposure ||
|
|
118
|
+
DataExposure.readExposureFor(transformOptions.dataExposure, { tableName }),
|
|
119
|
+
sqlRouteFieldPolicy(config, tableName),
|
|
120
|
+
tableName,
|
|
121
|
+
transformOptions
|
|
122
|
+
)}
|
|
111
123
|
function normalizePostgresConnectionString(connectionString) {
|
|
112
124
|
if (!connectionString || typeof connectionString !== 'string') return connectionString;
|
|
113
125
|
if (/^postgresql:\\/(?!\\/)/i.test(connectionString)) {
|
|
@@ -140,9 +152,11 @@ const getClient = () => {
|
|
|
140
152
|
if (ssl === false && connStr) {
|
|
141
153
|
connStr = stripSslQueryParamsFromConnectionString(connStr);
|
|
142
154
|
}
|
|
155
|
+
// No query waits on the database longer than this (enforced by the client:
|
|
156
|
+
// a server-side statement_timeout does not survive a pooler).
|
|
143
157
|
if (connStr) {
|
|
144
158
|
return new Client(Object.assign(
|
|
145
|
-
{ connectionString: connStr },
|
|
159
|
+
{ connectionString: connStr, query_timeout: ${DataExposure.READ_QUERY_TIMEOUT_MS} },
|
|
146
160
|
ssl !== undefined ? { ssl: ssl } : {}
|
|
147
161
|
));
|
|
148
162
|
}
|
|
@@ -153,6 +167,7 @@ const getClient = () => {
|
|
|
153
167
|
user: ${userRef},
|
|
154
168
|
password: ${passwordRef},
|
|
155
169
|
database: ${databaseRef},
|
|
170
|
+
query_timeout: ${DataExposure.READ_QUERY_TIMEOUT_MS},
|
|
156
171
|
},
|
|
157
172
|
ssl !== undefined ? { ssl: ssl } : {}
|
|
158
173
|
));
|
|
@@ -233,7 +248,9 @@ const processFilters = (filters, conditions, queryParams, paramIndex) => {
|
|
|
233
248
|
return null
|
|
234
249
|
}
|
|
235
250
|
|
|
236
|
-
|
|
251
|
+
// 'like' reads a JSON-list cell for one member (a post's former slugs); the
|
|
252
|
+
// value is a bound parameter, so a pattern never reaches the SQL text.
|
|
253
|
+
const validOps = ['=', '!=', '>', '<', '>=', '<=', 'like']
|
|
237
254
|
const sqlOperator = validOps.includes(operand) ? operand : '='
|
|
238
255
|
const clause = \`\${field} \${sqlOperator} $\${paramIndex}\`
|
|
239
256
|
queryParams.push(value)
|
|
@@ -373,7 +390,8 @@ ${generateReadGuardCall(tableName, 'req.query && req.query.rawQuery')}
|
|
|
373
390
|
}
|
|
374
391
|
|
|
375
392
|
const schemaResult = await client.query(schemaQuery, schemaParams)
|
|
376
|
-
|
|
393
|
+
// A credential column is never searched: a search is an oracle for it.
|
|
394
|
+
columns = schemaResult.rows.map(row => row.column_name).filter((column) => !__taIsCredentialColumn(column))
|
|
377
395
|
} catch (schemaError) {
|
|
378
396
|
console.warn('Failed to fetch column names from information_schema:', schemaError.message)
|
|
379
397
|
}
|
|
@@ -438,8 +456,10 @@ ${generateReadGuardCall(tableName, 'req.query && req.query.rawQuery')}
|
|
|
438
456
|
}
|
|
439
457
|
const usedRewrittenSort = orderBySql !== plainOrderBySql
|
|
440
458
|
|
|
441
|
-
|
|
442
|
-
const
|
|
459
|
+
// Whole numbers only, and a browser's read capped - see __taReadWindow.
|
|
460
|
+
const __window = __taReadWindow(req)
|
|
461
|
+
const limitValue = __window.limit || __window.perPage
|
|
462
|
+
const offsetValue = __window.offset !== undefined ? __window.offset : (__window.page && __window.perPage ? (__window.page - 1) * __window.perPage : undefined)
|
|
443
463
|
|
|
444
464
|
let sqlTail = ''
|
|
445
465
|
if (limitValue !== undefined) {
|
|
@@ -569,7 +589,8 @@ ${generateReadGuardCall(tableName, null)}
|
|
|
569
589
|
}
|
|
570
590
|
|
|
571
591
|
const schemaResult = await client.query(schemaQuery, schemaParams)
|
|
572
|
-
|
|
592
|
+
// A credential column is never searched: a search is an oracle for it.
|
|
593
|
+
columns = schemaResult.rows.map(row => row.column_name).filter((column) => !__taIsCredentialColumn(column))
|
|
573
594
|
} catch (schemaError) {
|
|
574
595
|
console.warn('Failed to fetch column names from information_schema:', schemaError.message)
|
|
575
596
|
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import type { DataReadExposure } from '@teleporthq/teleport-types'
|
|
2
|
+
import { DataExposure, TableAccess } from '@teleporthq/teleport-shared'
|
|
3
|
+
import {
|
|
4
|
+
generateTableAccessPreamble,
|
|
5
|
+
RequestFieldPolicy,
|
|
6
|
+
SchemaColumn,
|
|
7
|
+
SessionReaderPlacement,
|
|
8
|
+
} from './table-access-guard'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The same guard for a read route over an EXTERNAL source (MySQL, MariaDB,
|
|
12
|
+
* Redshift, ClickHouse, Turso, Supabase, MongoDB, Firestore, Airtable, Redis,
|
|
13
|
+
* REST, Google Sheets, CSV, JavaScript) as the TeleportHQ / Postgres routes
|
|
14
|
+
* carry inline: the table's exposure, the protected-table net, a row window of
|
|
15
|
+
* whole numbers (a browser's capped), request fields of a safe shape, no
|
|
16
|
+
* credential column in a row, and a generic error to the client.
|
|
17
|
+
*
|
|
18
|
+
* The drivers differ in everything else, so the guard wraps the driver's
|
|
19
|
+
* handler rather than living inside fourteen of them: the driver runs only for
|
|
20
|
+
* a request the guard let through, with `limit` / `perPage` / `page` /
|
|
21
|
+
* `offset` replaced by the whole numbers the guard settled on — a driver that
|
|
22
|
+
* splices them into its SQL text splices digits — and its answer passes back
|
|
23
|
+
* through the guard, which strips credentials and replaces any error text.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** How a request may name a field for a driver. */
|
|
27
|
+
export type RequestFieldRule = 'identifier' | 'label' | 'any'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A SQL column: plain, quoted or table-qualified — what the Postgres routes
|
|
31
|
+
* accept (`request-sql-guards.ts`).
|
|
32
|
+
*/
|
|
33
|
+
const IDENTIFIER_FIELD_RE = /^"?[A-Za-z_][A-Za-z0-9_]*"?(?:\."?[A-Za-z_][A-Za-z0-9_]*"?)?$/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A field of a document store or a base whose names are labels: anything but
|
|
37
|
+
* the characters its query language gives a meaning (`{}` closes an Airtable
|
|
38
|
+
* field reference, `$` opens a MongoDB operator, quotes and parentheses open
|
|
39
|
+
* literals and calls).
|
|
40
|
+
*/
|
|
41
|
+
const LABEL_FIELD_RE = /^[^{}'"`\\(),;$]{1,128}$/
|
|
42
|
+
|
|
43
|
+
/** SQL drivers splice the field into their statement; document stores read it as a path or an operator. */
|
|
44
|
+
export const requestFieldRuleOf = (dataSourceType: string): RequestFieldRule => {
|
|
45
|
+
switch (dataSourceType) {
|
|
46
|
+
case 'mysql':
|
|
47
|
+
case 'tidb':
|
|
48
|
+
case 'mariadb':
|
|
49
|
+
case 'amazon-redshift':
|
|
50
|
+
case 'clickhouse':
|
|
51
|
+
case 'turso':
|
|
52
|
+
case 'supabase':
|
|
53
|
+
return 'identifier'
|
|
54
|
+
case 'mongodb':
|
|
55
|
+
case 'firestore':
|
|
56
|
+
case 'airtable':
|
|
57
|
+
return 'label'
|
|
58
|
+
default:
|
|
59
|
+
// REST, JavaScript, CSV, Google Sheets, Redis filter the rows in memory.
|
|
60
|
+
return 'any'
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The stores that read a `$`-key as an operator or a dotted key as a path. */
|
|
65
|
+
const DOCUMENT_STORES = new Set(['mongodb', 'firestore', 'redis'])
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* What a request may name for a driver (see `RequestFieldPolicy`): the
|
|
69
|
+
* export's schema of the table — required for a SQL driver, so a table without
|
|
70
|
+
* one answers every named field 400 — and the driver's own shape rule.
|
|
71
|
+
*/
|
|
72
|
+
export const driverFieldPolicy = (
|
|
73
|
+
dataSourceType: string,
|
|
74
|
+
schemaColumns: string[] | null
|
|
75
|
+
): RequestFieldPolicy => {
|
|
76
|
+
const rule = requestFieldRuleOf(dataSourceType)
|
|
77
|
+
return {
|
|
78
|
+
columns: rule === 'identifier' ? schemaColumns || [] : schemaColumns,
|
|
79
|
+
documentStore: DOCUMENT_STORES.has(dataSourceType),
|
|
80
|
+
...(rule === 'identifier' && { pattern: IDENTIFIER_FIELD_RE }),
|
|
81
|
+
...(rule === 'label' && { pattern: LABEL_FIELD_RE }),
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The stores whose search turns the text into a pattern of their own query
|
|
87
|
+
* language (MongoDB's `$regex`): the text is escaped first, so it is matched
|
|
88
|
+
* as text — never an anchored, character-by-character probe, never a pattern
|
|
89
|
+
* that takes the server's time.
|
|
90
|
+
*/
|
|
91
|
+
const PATTERN_SEARCH_STORES = new Set(['mongodb'])
|
|
92
|
+
|
|
93
|
+
/** A text type, as the sources name theirs (`text`, `varchar(255)`, `character varying`, `string`). */
|
|
94
|
+
const TEXT_COLUMN_TYPE = /char|text|string|clob|enum/i
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The columns a browser's search runs over when it names none (N3): the
|
|
98
|
+
* table's schema — for Supabase, whose PostgREST `ilike` refuses any other
|
|
99
|
+
* type, its text columns — else, for a source without one, the fields the
|
|
100
|
+
* project's own bindings read. Never a credential column: what a search
|
|
101
|
+
* matches, it lets the searcher test character by character.
|
|
102
|
+
*/
|
|
103
|
+
export const defaultSearchColumns = (
|
|
104
|
+
dataSourceType: string,
|
|
105
|
+
schema: SchemaColumn[] | null,
|
|
106
|
+
bindingFields: string[]
|
|
107
|
+
): string[] => {
|
|
108
|
+
const textOnly = dataSourceType === 'supabase'
|
|
109
|
+
const candidates = schema
|
|
110
|
+
? schema
|
|
111
|
+
.filter((column) => !textOnly || !column.type || TEXT_COLUMN_TYPE.test(column.type))
|
|
112
|
+
.map((column) => column.name)
|
|
113
|
+
: bindingFields
|
|
114
|
+
return Array.from(new Set(candidates)).filter((name) => !TableAccess.isCredentialColumnName(name))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The guard's own code, after the shared preamble. */
|
|
118
|
+
const generateDriverGuardCode = (
|
|
119
|
+
tableName: string,
|
|
120
|
+
dataSourceType: string,
|
|
121
|
+
searchColumns: string[]
|
|
122
|
+
): string => `
|
|
123
|
+
// GENERATED — see external-read-guard.ts in teleport-plugin-next-data-source.
|
|
124
|
+
var __TQ_DRIVER_TABLE = ${JSON.stringify(tableName)};
|
|
125
|
+
var __TQ_DRIVER_WINDOW_KEYS = ['limit', 'perPage', 'page', 'offset'];
|
|
126
|
+
var __TQ_DRIVER_TIMEOUT_MS = ${DataExposure.READ_QUERY_TIMEOUT_MS};
|
|
127
|
+
// What a browser's search runs over when it names no columns (the driver's own
|
|
128
|
+
// fallback reads every column, credentials included).
|
|
129
|
+
var __TQ_DRIVER_SEARCH_COLUMNS = ${JSON.stringify(searchColumns)};
|
|
130
|
+
var __TQ_DRIVER_PATTERN_SEARCH = ${PATTERN_SEARCH_STORES.has(dataSourceType) ? 'true' : 'false'};
|
|
131
|
+
|
|
132
|
+
async function __taGuardedDriverRead(req, res, run, kind) {
|
|
133
|
+
var failure = kind === 'count' ? 'Failed to get count' : 'Failed to fetch data';
|
|
134
|
+
var exposure = await __taGuardExposure(req);
|
|
135
|
+
if (exposure) {
|
|
136
|
+
return res.status(exposure.status).json({ success: false, error: exposure.message, timestamp: Date.now() });
|
|
137
|
+
}
|
|
138
|
+
var access = await __taGuardRead(req, __TQ_DRIVER_TABLE, null);
|
|
139
|
+
if (access) {
|
|
140
|
+
return res.status(access.status).json({ success: false, error: access.message, timestamp: Date.now() });
|
|
141
|
+
}
|
|
142
|
+
var window;
|
|
143
|
+
try {
|
|
144
|
+
window = __taReadWindow(req);
|
|
145
|
+
__taAssertReadRequest(req);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
return res.status(400).json({ success: false, error: error && error.status ? error.message : 'Invalid request', timestamp: Date.now() });
|
|
148
|
+
}
|
|
149
|
+
var internal = __taIsInternalRequest(req);
|
|
150
|
+
var query = Object.assign({}, (req && req.query) || {});
|
|
151
|
+
__TQ_DRIVER_WINDOW_KEYS.forEach(function(key) {
|
|
152
|
+
if (window[key] === undefined) delete query[key];
|
|
153
|
+
else query[key] = String(window[key]);
|
|
154
|
+
});
|
|
155
|
+
var searching = query.query !== undefined && query.query !== null && String(query.query) !== '';
|
|
156
|
+
if (searching && !internal && (query.queryColumns === undefined || query.queryColumns === null || query.queryColumns === '')) {
|
|
157
|
+
if (__TQ_DRIVER_SEARCH_COLUMNS.length === 0) {
|
|
158
|
+
return res.status(400).json({ success: false, error: 'Invalid search', timestamp: Date.now() });
|
|
159
|
+
}
|
|
160
|
+
query.queryColumns = JSON.stringify(__TQ_DRIVER_SEARCH_COLUMNS);
|
|
161
|
+
}
|
|
162
|
+
if (searching && __TQ_DRIVER_PATTERN_SEARCH) {
|
|
163
|
+
query.query = String(Array.isArray(query.query) ? query.query[0] : query.query).replace(/[.*+?^\${}()|[\\]\\\\]/g, '\\\\$&');
|
|
164
|
+
}
|
|
165
|
+
var forwarded = {
|
|
166
|
+
method: req && req.method,
|
|
167
|
+
headers: (req && req.headers) || {},
|
|
168
|
+
cookies: req && req.cookies,
|
|
169
|
+
// A browser's body never reaches the driver: a REST source configured for
|
|
170
|
+
// POST would send it upstream, under the site's credentials, in place of
|
|
171
|
+
// the configured one. The app's own server code may still supply one.
|
|
172
|
+
body: internal ? req && req.body : undefined,
|
|
173
|
+
query: query,
|
|
174
|
+
__tqServerCall: !!(req && req.__tqServerCall === true),
|
|
175
|
+
__tqOwnerScope: req && req.__tqOwnerScope,
|
|
176
|
+
};
|
|
177
|
+
// A row-owned page's read: the owner condition joins the request's filters.
|
|
178
|
+
__taApplyOwnerScope(forwarded);
|
|
179
|
+
var status = 200;
|
|
180
|
+
var payload = null;
|
|
181
|
+
var proxy = {
|
|
182
|
+
status: function(code) { status = code; return proxy; },
|
|
183
|
+
json: function(data) { payload = data; return proxy; },
|
|
184
|
+
setHeader: function(name, value) {
|
|
185
|
+
if (res && typeof res.setHeader === 'function') res.setHeader(name, value);
|
|
186
|
+
return proxy;
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
// No read waits on the source longer than __TQ_DRIVER_TIMEOUT_MS: a driver
|
|
190
|
+
// without a query timeout of its own is answered for when it overruns.
|
|
191
|
+
var timer = null;
|
|
192
|
+
try {
|
|
193
|
+
await Promise.race([
|
|
194
|
+
run(forwarded, proxy),
|
|
195
|
+
new Promise(function(_resolve, reject) {
|
|
196
|
+
timer = setTimeout(function() { reject(new Error('the read timed out')); }, __TQ_DRIVER_TIMEOUT_MS);
|
|
197
|
+
}),
|
|
198
|
+
]);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
status = 500;
|
|
201
|
+
payload = { success: false, error: error };
|
|
202
|
+
} finally {
|
|
203
|
+
if (timer) clearTimeout(timer);
|
|
204
|
+
}
|
|
205
|
+
if (status !== 200 || !payload || payload.success === false) {
|
|
206
|
+
// The driver's error names tables, columns and values: server log only.
|
|
207
|
+
var id = __taErrorId();
|
|
208
|
+
console.error('[data-source] reading ' + __TQ_DRIVER_TABLE + ' failed (' + id + '):', payload && payload.error);
|
|
209
|
+
return res.status(status >= 400 && status < 500 ? status : 500).json({ success: false, error: failure, id: id, timestamp: Date.now() });
|
|
210
|
+
}
|
|
211
|
+
if (Array.isArray(payload.data)) {
|
|
212
|
+
payload = Object.assign({}, payload, { data: __taWithoutCredentials(payload.data, __TQ_DRIVER_TABLE) });
|
|
213
|
+
}
|
|
214
|
+
return res.status(200).json(payload);
|
|
215
|
+
}
|
|
216
|
+
`
|
|
217
|
+
|
|
218
|
+
const UNGUARDED_HANDLER = '__tqUnguardedHandler'
|
|
219
|
+
const UNGUARDED_COUNT = '__tqUnguardedGetCount'
|
|
220
|
+
const DRIVER_HANDLER_DECLARATION = 'export default async function handler('
|
|
221
|
+
const COUNT_DECLARATION = 'async function getCount(req, res)'
|
|
222
|
+
|
|
223
|
+
/** The driver module's single-line imports, which must stay at the top of an ES module. */
|
|
224
|
+
const IMPORT_LINE = /^import\s.*$/gm
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* The driver's fetcher module with the guard around its handler: same
|
|
228
|
+
* imports, the preamble, the driver renamed, and a guarded `handler` exported
|
|
229
|
+
* in its place.
|
|
230
|
+
*/
|
|
231
|
+
export const guardExternalDriverModule = (
|
|
232
|
+
driverCode: string,
|
|
233
|
+
params: {
|
|
234
|
+
tableName: string
|
|
235
|
+
dataSourceType: string
|
|
236
|
+
exposure: DataReadExposure
|
|
237
|
+
trustedReaderRoles: ReadonlyArray<string>
|
|
238
|
+
/** The table's columns as the data source config lists them (null: no schema). */
|
|
239
|
+
schema?: SchemaColumn[] | null
|
|
240
|
+
/** Where the route sits and whether its session reader checks revocation. */
|
|
241
|
+
reader?: SessionReaderPlacement
|
|
242
|
+
}
|
|
243
|
+
): string => {
|
|
244
|
+
const schema = params.schema ?? null
|
|
245
|
+
const schemaColumns = schema ? schema.map((column) => column.name) : null
|
|
246
|
+
if (driverCode.split(DRIVER_HANDLER_DECLARATION).length !== 2) {
|
|
247
|
+
// A driver that is not one handler cannot be wrapped; refusing to generate
|
|
248
|
+
// it is the only answer that does not serve it unguarded.
|
|
249
|
+
throw new Error('a data source read route must declare exactly one handler')
|
|
250
|
+
}
|
|
251
|
+
const imports = driverCode.match(IMPORT_LINE) || []
|
|
252
|
+
const body = driverCode
|
|
253
|
+
.replace(IMPORT_LINE, '')
|
|
254
|
+
.trim()
|
|
255
|
+
.replace(DRIVER_HANDLER_DECLARATION, `async function ${UNGUARDED_HANDLER}(`)
|
|
256
|
+
|
|
257
|
+
return `${imports.join('\n')}
|
|
258
|
+
|
|
259
|
+
${generateTableAccessPreamble(
|
|
260
|
+
params.trustedReaderRoles,
|
|
261
|
+
params.exposure,
|
|
262
|
+
driverFieldPolicy(params.dataSourceType, schemaColumns),
|
|
263
|
+
params.tableName,
|
|
264
|
+
params.reader
|
|
265
|
+
)}${generateDriverGuardCode(
|
|
266
|
+
params.tableName,
|
|
267
|
+
params.dataSourceType,
|
|
268
|
+
defaultSearchColumns(params.dataSourceType, schema, params.exposure.fields || [])
|
|
269
|
+
)}
|
|
270
|
+
${body}
|
|
271
|
+
|
|
272
|
+
export default async function handler(req, res) {
|
|
273
|
+
return __taGuardedDriverRead(req, res, ${UNGUARDED_HANDLER}, 'data')
|
|
274
|
+
}
|
|
275
|
+
`
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** The driver's count handler behind the same guard (the guard code comes with the handler module). */
|
|
279
|
+
export const guardExternalDriverCount = (countCode: string): string => {
|
|
280
|
+
if (countCode.split(COUNT_DECLARATION).length !== 2) {
|
|
281
|
+
throw new Error('a data source count route must declare exactly one getCount')
|
|
282
|
+
}
|
|
283
|
+
return `${countCode.replace(COUNT_DECLARATION, `async function ${UNGUARDED_COUNT}(req, res)`)}
|
|
284
|
+
|
|
285
|
+
async function getCount(req, res) {
|
|
286
|
+
return __taGuardedDriverRead(req, res, ${UNGUARDED_COUNT}, 'count')
|
|
287
|
+
}
|
|
288
|
+
`
|
|
289
|
+
}
|