@teleporthq/teleport-plugin-next-data-source 0.44.15 → 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.
Files changed (125) hide show
  1. package/__tests__/_helpers/exposure.ts +29 -0
  2. package/__tests__/_helpers/first-page-runtime.ts +12 -2
  3. package/__tests__/blog-post-contents.test.ts +3 -1
  4. package/__tests__/blog-post-page-enrichment.test.ts +2 -1
  5. package/__tests__/blog-post-seo-fields.test.ts +124 -0
  6. package/__tests__/browser-row-policy.test.ts +65 -5
  7. package/__tests__/data-source-cache-wrapping.test.ts +48 -7
  8. package/__tests__/external-read-routes-guard.test.ts +991 -0
  9. package/__tests__/null-filter-where.test.ts +19 -2
  10. package/__tests__/range-control.test.ts +29 -0
  11. package/__tests__/search-relevance.test.ts +42 -6
  12. package/__tests__/table-read-route-protected-tables.test.ts +54 -4
  13. package/dist/cjs/count-fetchers.d.ts.map +1 -1
  14. package/dist/cjs/count-fetchers.js +1 -1
  15. package/dist/cjs/count-fetchers.js.map +1 -1
  16. package/dist/cjs/data-source-fetchers.d.ts +4 -0
  17. package/dist/cjs/data-source-fetchers.d.ts.map +1 -1
  18. package/dist/cjs/data-source-fetchers.js +97 -20
  19. package/dist/cjs/data-source-fetchers.js.map +1 -1
  20. package/dist/cjs/fetchers/postgresql.d.ts +3 -1
  21. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  22. package/dist/cjs/fetchers/postgresql.js +11 -3
  23. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  24. package/dist/cjs/fetchers/raw-query.d.ts +20 -1
  25. package/dist/cjs/fetchers/raw-query.d.ts.map +1 -1
  26. package/dist/cjs/fetchers/raw-query.js +37 -7
  27. package/dist/cjs/fetchers/raw-query.js.map +1 -1
  28. package/dist/cjs/fetchers/redshift.d.ts.map +1 -1
  29. package/dist/cjs/fetchers/redshift.js +2 -1
  30. package/dist/cjs/fetchers/redshift.js.map +1 -1
  31. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  32. package/dist/cjs/fetchers/teleport.js +5 -3
  33. package/dist/cjs/fetchers/teleport.js.map +1 -1
  34. package/dist/cjs/fetchers/utils/external-read-guard.d.ts +53 -0
  35. package/dist/cjs/fetchers/utils/external-read-guard.d.ts.map +1 -0
  36. package/dist/cjs/fetchers/utils/external-read-guard.js +126 -0
  37. package/dist/cjs/fetchers/utils/external-read-guard.js.map +1 -0
  38. package/dist/cjs/fetchers/utils/table-access-guard.d.ts +76 -12
  39. package/dist/cjs/fetchers/utils/table-access-guard.d.ts.map +1 -1
  40. package/dist/cjs/fetchers/utils/table-access-guard.js +123 -15
  41. package/dist/cjs/fetchers/utils/table-access-guard.js.map +1 -1
  42. package/dist/cjs/index.js +1 -1
  43. package/dist/cjs/pagination-controls.d.ts +4 -1
  44. package/dist/cjs/pagination-controls.d.ts.map +1 -1
  45. package/dist/cjs/pagination-controls.js +4 -1
  46. package/dist/cjs/pagination-controls.js.map +1 -1
  47. package/dist/cjs/pagination-plugin.js +1 -0
  48. package/dist/cjs/pagination-plugin.js.map +1 -1
  49. package/dist/cjs/range-control.d.ts +10 -5
  50. package/dist/cjs/range-control.d.ts.map +1 -1
  51. package/dist/cjs/range-control.js +23 -2
  52. package/dist/cjs/range-control.js.map +1 -1
  53. package/dist/cjs/transformations/blog-post.d.ts +5 -0
  54. package/dist/cjs/transformations/blog-post.d.ts.map +1 -1
  55. package/dist/cjs/transformations/blog-post.js +6 -2
  56. package/dist/cjs/transformations/blog-post.js.map +1 -1
  57. package/dist/cjs/transformations/index.d.ts +18 -2
  58. package/dist/cjs/transformations/index.d.ts.map +1 -1
  59. package/dist/cjs/transformations/index.js +26 -1
  60. package/dist/cjs/transformations/index.js.map +1 -1
  61. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  62. package/dist/esm/count-fetchers.d.ts.map +1 -1
  63. package/dist/esm/count-fetchers.js +1 -1
  64. package/dist/esm/count-fetchers.js.map +1 -1
  65. package/dist/esm/data-source-fetchers.d.ts +4 -0
  66. package/dist/esm/data-source-fetchers.d.ts.map +1 -1
  67. package/dist/esm/data-source-fetchers.js +97 -20
  68. package/dist/esm/data-source-fetchers.js.map +1 -1
  69. package/dist/esm/fetchers/postgresql.d.ts +3 -1
  70. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  71. package/dist/esm/fetchers/postgresql.js +12 -4
  72. package/dist/esm/fetchers/postgresql.js.map +1 -1
  73. package/dist/esm/fetchers/raw-query.d.ts +20 -1
  74. package/dist/esm/fetchers/raw-query.d.ts.map +1 -1
  75. package/dist/esm/fetchers/raw-query.js +38 -8
  76. package/dist/esm/fetchers/raw-query.js.map +1 -1
  77. package/dist/esm/fetchers/redshift.d.ts.map +1 -1
  78. package/dist/esm/fetchers/redshift.js +2 -1
  79. package/dist/esm/fetchers/redshift.js.map +1 -1
  80. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  81. package/dist/esm/fetchers/teleport.js +6 -4
  82. package/dist/esm/fetchers/teleport.js.map +1 -1
  83. package/dist/esm/fetchers/utils/external-read-guard.d.ts +53 -0
  84. package/dist/esm/fetchers/utils/external-read-guard.d.ts.map +1 -0
  85. package/dist/esm/fetchers/utils/external-read-guard.js +118 -0
  86. package/dist/esm/fetchers/utils/external-read-guard.js.map +1 -0
  87. package/dist/esm/fetchers/utils/table-access-guard.d.ts +76 -12
  88. package/dist/esm/fetchers/utils/table-access-guard.d.ts.map +1 -1
  89. package/dist/esm/fetchers/utils/table-access-guard.js +119 -15
  90. package/dist/esm/fetchers/utils/table-access-guard.js.map +1 -1
  91. package/dist/esm/index.js +1 -1
  92. package/dist/esm/pagination-controls.d.ts +4 -1
  93. package/dist/esm/pagination-controls.d.ts.map +1 -1
  94. package/dist/esm/pagination-controls.js +4 -1
  95. package/dist/esm/pagination-controls.js.map +1 -1
  96. package/dist/esm/pagination-plugin.js +1 -0
  97. package/dist/esm/pagination-plugin.js.map +1 -1
  98. package/dist/esm/range-control.d.ts +10 -5
  99. package/dist/esm/range-control.d.ts.map +1 -1
  100. package/dist/esm/range-control.js +23 -2
  101. package/dist/esm/range-control.js.map +1 -1
  102. package/dist/esm/transformations/blog-post.d.ts +5 -0
  103. package/dist/esm/transformations/blog-post.d.ts.map +1 -1
  104. package/dist/esm/transformations/blog-post.js +6 -2
  105. package/dist/esm/transformations/blog-post.js.map +1 -1
  106. package/dist/esm/transformations/index.d.ts +18 -2
  107. package/dist/esm/transformations/index.d.ts.map +1 -1
  108. package/dist/esm/transformations/index.js +27 -2
  109. package/dist/esm/transformations/index.js.map +1 -1
  110. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  111. package/package.json +5 -5
  112. package/src/count-fetchers.ts +10 -2
  113. package/src/data-source-fetchers.ts +144 -23
  114. package/src/fetchers/postgresql.ts +38 -9
  115. package/src/fetchers/raw-query.ts +125 -13
  116. package/src/fetchers/redshift.ts +3 -0
  117. package/src/fetchers/teleport.ts +29 -8
  118. package/src/fetchers/utils/external-read-guard.ts +289 -0
  119. package/src/fetchers/utils/table-access-guard.ts +469 -20
  120. package/src/index.ts +1 -1
  121. package/src/pagination-controls.ts +4 -1
  122. package/src/pagination-plugin.ts +1 -0
  123. package/src/range-control.ts +40 -8
  124. package/src/transformations/blog-post.ts +107 -4
  125. package/src/transformations/index.ts +44 -1
@@ -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 { generateReadGuardCall, generateTableAccessPreamble } from './utils/table-access-guard'
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(transformOptions.trustedReaderRoles || [])}
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
- const validOps = ['=', '!=', '>', '<', '>=', '<=']
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
- columns = schemaResult.rows.map(row => row.column_name)
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
- const limitValue = requestRowCount(limit || perPage)
442
- const offsetValue = offset !== undefined ? parseInt(offset) : (page && perPage ? (parseInt(page) - 1) * parseInt(perPage) : undefined)
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
- columns = schemaResult.rows.map(row => row.column_name)
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
+ }