@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
|
@@ -4,6 +4,9 @@ import { generateSortFallbackFieldHelper, generateSortFieldSqlHelper, generateSo
|
|
|
4
4
|
import { generateProductFilterClauseHelper } from '../product-filter-fields';
|
|
5
5
|
import { collectLocalizedColumns, generateLocalizedColumnsHelper } from '../localized-columns';
|
|
6
6
|
import { REQUEST_LOCALE_PARAM } from '../request-locale';
|
|
7
|
+
import { generateReadGuardCall, generateTableAccessPreamble } from './utils/table-access-guard';
|
|
8
|
+
import { generateRequestSqlGuardsCode } from './utils/request-sql-guards';
|
|
9
|
+
import { generateArrayOverlapSqlCode } from '../array-overlap-sql';
|
|
7
10
|
var DEFAULT_ENV_KEYS = {
|
|
8
11
|
host: 'TELEPORT_DB_HOST',
|
|
9
12
|
port: 'TELEPORT_DB_PORT',
|
|
@@ -49,18 +52,18 @@ export var generateTeleportFetcher = function (config, tableName, transformOptio
|
|
|
49
52
|
? "key: ".concat(resolveEnvReference(dbConfig.sslConfig.key, 'TELEPORT_DB_SSL_KEY'), ",")
|
|
50
53
|
: '', "\n rejectUnauthorized: false\n }")
|
|
51
54
|
: "(process.env.".concat(DEFAULT_ENV_KEYS.ssl, " === 'false' ? false : process.env.").concat(DEFAULT_ENV_KEYS.ssl, " === 'true' ? { rejectUnauthorized: false } : undefined)");
|
|
52
|
-
return "import { Client } from 'pg'\n\nfunction normalizePostgresConnectionString(connectionString) {\n if (!connectionString || typeof connectionString !== 'string') return connectionString;\n if (/^postgresql:\\/(?!\\/)/i.test(connectionString)) {\n return connectionString.replace(/^postgresql:\\//i, 'postgresql://');\n }\n return connectionString;\n}\n\nfunction stripSslQueryParamsFromConnectionString(connectionString) {\n if (!connectionString || typeof connectionString !== 'string') return connectionString;\n try {\n var u = new URL(connectionString.replace(/^postgresql:/i, 'postgres:'));\n u.searchParams.delete('sslmode');\n u.searchParams.delete('ssl');\n u.searchParams.delete('sslrootcert');\n u.searchParams.delete('sslcert');\n u.searchParams.delete('sslkey');\n return u.toString().replace(/^postgres:/i, 'postgresql:');\n } catch (e) {\n return connectionString;\n }\n}\n\nconst getClient = () => {\n var ssl = ".concat(sslCode, ";\n var connStr = process.env.TELEPORT_DB_CONNECTION_STRING;\n if (connStr) {\n connStr = normalizePostgresConnectionString(connStr);\n }\n if (ssl === false && connStr) {\n connStr = stripSslQueryParamsFromConnectionString(connStr);\n }\n if (connStr) {\n return new Client(Object.assign(\n { connectionString: connStr },\n ssl !== undefined ? { ssl: ssl } : {}\n ));\n }\n return new Client(Object.assign(\n {\n host: ").concat(hostRef, ",\n port: parseInt(").concat(portRef, " || '5432', 10),\n user: ").concat(userRef, ",\n password: ").concat(passwordRef, ",\n database: ").concat(databaseRef, ",\n },\n ssl !== undefined ? { ssl: ssl } : {}\n ));\n}\n\n").concat(generateSafeJSONParseCode(), "\n\n").concat(generateFilterTreeHelpersCode(), "\n\n").concat(generateSearchEscapeHelpersCode(), "\n").concat(getTransformationCode(tableName, transformOptions), "\n").concat(getTransformWrapperCode(tableName, transformOptions), "\nconst processFilters = (filters, conditions, queryParams, paramIndex) => {\n if (!filters) return paramIndex\n \n const filterTree = normalizeFilterTree(safeJSONParse(filters))\n if (!filterTree) return paramIndex\n \n const buildCondition = (condition) => {\n const field = condition.source\n const normalizedCondition = normalizeInOperand(condition.operand, condition.destination)\n if (normalizedCondition === null) return null\n const value = normalizedCondition.value\n const operand = normalizedCondition.operand\n \n // The products table's virtual fields (effective price, rating bucket,\n // on-sale flag, variant axes) are whole clauses, not columns - so they are\n // answered before any branch below can interpolate the field name raw.\n const productClause = productFilterClause(field, operand, value, (param) => {\n queryParams.push(param)\n return '$' + paramIndex++\n })\n if (productClause !== null) return productClause\n\n // A translatable column is compared against the value in every language,\n // so a localized slug resolves under any locale - see localized-columns.ts.\n const localizedClause = localizedEqualityClause(field, operand, value, (param) => {\n queryParams.push(param)\n return '$' + paramIndex++\n })\n if (localizedClause !== null) return localizedClause\n \n if (Array.isArray(value)) {\n if (value.length === 0) return null\n if (operand === 'array_overlap') {\n // Row column is a JSON array (e.g. category_ids). Match when it\n // shares any element with the destination set. jsonb ?| (function\n // form) avoids the '?' placeholder-token ambiguity some drivers hit.\n const clause = `jsonb_exists_any(NULLIF(${field}, '')::jsonb, $${paramIndex}::text[])`\n queryParams.push(value.map((entry) => String(entry)))\n paramIndex++\n return clause\n }\n const placeholders = value.map(() => `$${paramIndex++}`)\n queryParams.push(...value)\n return operand === '!='\n ? `${field} NOT IN (${placeholders.join(', ')})`\n : `${field} IN (${placeholders.join(', ')})`\n }\n \n if (operand === 'array_overlap') {\n if (value === '' || value === null || value === undefined) return null\n // A single comma-joined string (the multi-select Category Filter's\n // ?categoryFilter=a,b,c) expands to multiple ids; one id stays one.\n const overlapValues = String(value).split(',').map((entry) => entry.trim()).filter(Boolean)\n if (overlapValues.length === 0) return null\n const clause = `jsonb_exists_any(NULLIF(${field}, '')::jsonb, $${paramIndex}::text[])`\n queryParams.push(overlapValues)\n paramIndex++\n return clause\n }\n \n if (value === null) {\n if (operand === '=') return `${field} IS NULL`\n if (operand === '!=') return `${field} IS NOT NULL`\n return null\n }\n \n const validOps = ['=', '!=', '>', '<', '>=', '<=']\n const sqlOperator = validOps.includes(operand) ? operand : '='\n const clause = `${field} ${sqlOperator} $${paramIndex}`\n queryParams.push(value)\n paramIndex++\n return clause\n }\n \n const clause = buildFilterTreeClause(filterTree, buildCondition)\n if (clause) conditions.push(clause)\n \n return paramIndex\n}\n\n").concat(generateDateFormatterCode(), "\n\n").concat(generateSortFieldSqlHelper(tableName), "\n\n").concat(generateSortFallbackFieldHelper(tableName), "\n\n").concat(generateProductFilterClauseHelper(tableName), "\n\n").concat(generateLocalizedColumnsHelper(localizedColumns), "\n\n// Matches DDL / dangerous statements the raw-query branch should refuse.\n// Keep this list conservative \u2014 anything destructive or schema-changing is\n// out of scope for a client-triggered fetch. SELECT and CTEs (WITH ...) are\n// the only shapes consumers legitimately need.\nconst BLOCKED_RAW_QUERY_PATTERNS = [\n /\\bcreate\\s+(?:temporary\\s+|temp\\s+|unlogged\\s+|global\\s+|local\\s+)?table\\b/i,\n /\\bcreate\\s+(?:unique\\s+)?index\\b/i,\n /\\bcreate\\s+(?:or\\s+replace\\s+)?(?:materialized\\s+)?view\\b/i,\n /\\bcreate\\s+(?:or\\s+replace\\s+)?trigger\\b/i,\n /\\bcreate\\s+(?:or\\s+replace\\s+)?(?:aggregate\\s+)?function\\b/i,\n /\\bcreate\\s+(?:or\\s+replace\\s+)?procedure\\b/i,\n /\\bcreate\\s+(?:database|schema|sequence|extension|type|role|user)\\b/i,\n /\\bdrop\\s+(?:table|view|index|schema|database|sequence|trigger|function|procedure|role|user|extension|type|materialized)\\b/i,\n /\\balter\\s+(?:table|view|index|schema|database|sequence|role|user|system)\\b/i,\n /\\btruncate\\b/i,\n /\\bgrant\\b/i,\n /\\brevoke\\b/i,\n /\\binsert\\b/i,\n /\\bupdate\\b/i,\n /\\bdelete\\b/i,\n /\\bcopy\\b/i,\n /\\bvacuum\\b/i,\n /\\breindex\\b/i,\n /\\bcluster\\b/i,\n]\n\nfunction assertRawQuerySafe(rawQuery) {\n if (typeof rawQuery !== 'string' || rawQuery.length === 0) {\n throw new Error('rawQuery must be a non-empty string')\n }\n // Reject multi-statement payloads \u2014 only single SELECT / WITH statements.\n // A trailing semicolon is tolerated but any content after it fails.\n var trimmed = rawQuery.trim().replace(/;\\s*$/, '')\n if (trimmed.indexOf(';') !== -1) {\n throw new Error('rawQuery must contain exactly one statement')\n }\n for (var i = 0; i < BLOCKED_RAW_QUERY_PATTERNS.length; i++) {\n if (BLOCKED_RAW_QUERY_PATTERNS[i].test(trimmed)) {\n throw new Error('rawQuery contains a blocked statement')\n }\n }\n}\n\nexport default async function handler(req, res) {\n const client = getClient()\n\n try {\n await client.connect()\n ").concat(schema ? "await client.query('SET search_path TO ".concat(schema, "')") : '', "\n\n // If the caller supplied a rawQuery, run it verbatim (after a safety\n // guard). The schema-driven branch below builds `SELECT * FROM ").concat(tableName, "`\n // with optional filters \u2014 it's the default read path and ignores\n // rawQuery. Page-load workflows and other consumers that need a JOIN\n // or a user-scoped filter pass their fully-rendered SQL here via\n // `fetchData({ rawQuery })` and expect it to execute verbatim.\n if (req.query && typeof req.query.rawQuery === 'string' && req.query.rawQuery.length > 0) {\n assertRawQuerySafe(req.query.rawQuery)\n const rawResult = await client.query(req.query.rawQuery)\n const rawRows = Array.isArray(rawResult?.rows) ? rawResult.rows : []\n const rawPlain = rawRows.map((row) =>\n row && typeof row.toJSON === 'function' ? row.toJSON() : row\n )\n const rawSafe = JSON.parse(JSON.stringify(rawPlain, dateReplacer))\n return res.status(200).json({\n success: true,\n data: rawSafe,\n timestamp: Date.now()\n })\n }\n\n const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset } = req.query\n // The language the page is in - what a translatable sort orders by.\n const requestLocale = typeof req.query.").concat(REQUEST_LOCALE_PARAM, " === 'string' ? req.query.").concat(REQUEST_LOCALE_PARAM, " : null\n \n const conditions = []\n const queryParams = []\n let paramIndex = 1\n \n if (query) {\n let columns = []\n \n if (queryColumns) {\n const parsed = safeJSONParse(queryColumns)\n columns = Array.isArray(parsed) ? parsed : (parsed ? [parsed] : [])\n } else {\n try {\n const schemaQuery = `\n SELECT column_name \n FROM information_schema.columns \n WHERE table_name = $1\n ").concat(schema ? "AND table_schema = $2" : '', "\n ORDER BY ordinal_position\n `\n const schemaParams = ").concat(schema
|
|
55
|
+
return "import { Client } from 'pg'\n\n".concat(generateTableAccessPreamble(transformOptions.trustedReaderRoles || []), "\nfunction normalizePostgresConnectionString(connectionString) {\n if (!connectionString || typeof connectionString !== 'string') return connectionString;\n if (/^postgresql:\\/(?!\\/)/i.test(connectionString)) {\n return connectionString.replace(/^postgresql:\\//i, 'postgresql://');\n }\n return connectionString;\n}\n\nfunction stripSslQueryParamsFromConnectionString(connectionString) {\n if (!connectionString || typeof connectionString !== 'string') return connectionString;\n try {\n var u = new URL(connectionString.replace(/^postgresql:/i, 'postgres:'));\n u.searchParams.delete('sslmode');\n u.searchParams.delete('ssl');\n u.searchParams.delete('sslrootcert');\n u.searchParams.delete('sslcert');\n u.searchParams.delete('sslkey');\n return u.toString().replace(/^postgres:/i, 'postgresql:');\n } catch (e) {\n return connectionString;\n }\n}\n\nconst getClient = () => {\n var ssl = ").concat(sslCode, ";\n var connStr = process.env.TELEPORT_DB_CONNECTION_STRING;\n if (connStr) {\n connStr = normalizePostgresConnectionString(connStr);\n }\n if (ssl === false && connStr) {\n connStr = stripSslQueryParamsFromConnectionString(connStr);\n }\n if (connStr) {\n return new Client(Object.assign(\n { connectionString: connStr },\n ssl !== undefined ? { ssl: ssl } : {}\n ));\n }\n return new Client(Object.assign(\n {\n host: ").concat(hostRef, ",\n port: parseInt(").concat(portRef, " || '5432', 10),\n user: ").concat(userRef, ",\n password: ").concat(passwordRef, ",\n database: ").concat(databaseRef, ",\n },\n ssl !== undefined ? { ssl: ssl } : {}\n ));\n}\n\n").concat(generateSafeJSONParseCode(), "\n\n").concat(generateFilterTreeHelpersCode(), "\n\n").concat(generateSearchEscapeHelpersCode(), "\n").concat(generateArrayOverlapSqlCode(), "\n").concat(getTransformationCode(tableName, transformOptions), "\n").concat(getTransformWrapperCode(tableName, transformOptions), "\n").concat(generateRequestSqlGuardsCode(), "\nconst processFilters = (filters, conditions, queryParams, paramIndex) => {\n if (!filters) return paramIndex\n \n const filterTree = normalizeFilterTree(safeJSONParse(filters))\n if (!filterTree) return paramIndex\n \n const buildCondition = (condition) => {\n const field = condition.source\n const normalizedCondition = normalizeInOperand(condition.operand, condition.destination)\n if (normalizedCondition === null) return null\n const value = normalizedCondition.value\n const operand = normalizedCondition.operand\n \n // The products table's virtual fields (effective price, rating bucket,\n // on-sale flag, variant axes) are whole clauses, not columns - so they are\n // answered before any branch below can interpolate the field name raw.\n const productClause = productFilterClause(field, operand, value, (param) => {\n queryParams.push(param)\n return '$' + paramIndex++\n })\n if (productClause !== null) return productClause\n\n // A translatable column is compared against the value in every language,\n // so a localized slug resolves under any locale - see localized-columns.ts.\n const localizedClause = localizedEqualityClause(field, operand, value, (param) => {\n queryParams.push(param)\n return '$' + paramIndex++\n })\n if (localizedClause !== null) return localizedClause\n assertRequestColumn(field, 'filter field')\n \n if (Array.isArray(value)) {\n if (value.length === 0) return null\n if (operand === 'array_overlap') {\n // The row's list (see array-overlap-sql.ts) shares any value with the set.\n const clause = arrayOverlapSql((localizedColumnList(field) || [field]), '$' + paramIndex)\n queryParams.push(value.map((entry) => String(entry)))\n paramIndex++\n return clause\n }\n const placeholders = value.map(() => `$${paramIndex++}`)\n queryParams.push(...value)\n return operand === '!='\n ? `${field} NOT IN (${placeholders.join(', ')})`\n : `${field} IN (${placeholders.join(', ')})`\n }\n \n if (operand === 'array_overlap') {\n if (value === '' || value === null || value === undefined) return null\n // A single comma-joined string (the multi-select Category Filter's\n // ?categoryFilter=a,b,c) expands to multiple ids; one id stays one.\n const overlapValues = String(value).split(',').map((entry) => entry.trim()).filter(Boolean)\n if (overlapValues.length === 0) return null\n const clause = arrayOverlapSql((localizedColumnList(field) || [field]), '$' + paramIndex)\n queryParams.push(overlapValues)\n paramIndex++\n return clause\n }\n \n if (value === null) {\n if (operand === '=') return `${field} IS NULL`\n if (operand === '!=') return `${field} IS NOT NULL`\n return null\n }\n \n const validOps = ['=', '!=', '>', '<', '>=', '<=']\n const sqlOperator = validOps.includes(operand) ? operand : '='\n const clause = `${field} ${sqlOperator} $${paramIndex}`\n queryParams.push(value)\n paramIndex++\n return clause\n }\n \n const clause = buildFilterTreeClause(filterTree, buildCondition)\n if (clause) conditions.push(clause)\n \n return paramIndex\n}\n\n").concat(generateDateFormatterCode(), "\n\n").concat(generateSortFieldSqlHelper(tableName), "\n\n").concat(generateSortFallbackFieldHelper(tableName), "\n\n").concat(generateProductFilterClauseHelper(tableName), "\n\n").concat(generateLocalizedColumnsHelper(localizedColumns), "\n\n// Matches DDL / dangerous statements the raw-query branch should refuse.\n// Keep this list conservative \u2014 anything destructive or schema-changing is\n// out of scope for a client-triggered fetch. SELECT and CTEs (WITH ...) are\n// the only shapes consumers legitimately need.\nconst BLOCKED_RAW_QUERY_PATTERNS = [\n /\\bcreate\\s+(?:temporary\\s+|temp\\s+|unlogged\\s+|global\\s+|local\\s+)?table\\b/i,\n /\\bcreate\\s+(?:unique\\s+)?index\\b/i,\n /\\bcreate\\s+(?:or\\s+replace\\s+)?(?:materialized\\s+)?view\\b/i,\n /\\bcreate\\s+(?:or\\s+replace\\s+)?trigger\\b/i,\n /\\bcreate\\s+(?:or\\s+replace\\s+)?(?:aggregate\\s+)?function\\b/i,\n /\\bcreate\\s+(?:or\\s+replace\\s+)?procedure\\b/i,\n /\\bcreate\\s+(?:database|schema|sequence|extension|type|role|user)\\b/i,\n /\\bdrop\\s+(?:table|view|index|schema|database|sequence|trigger|function|procedure|role|user|extension|type|materialized)\\b/i,\n /\\balter\\s+(?:table|view|index|schema|database|sequence|role|user|system)\\b/i,\n /\\btruncate\\b/i,\n /\\bgrant\\b/i,\n /\\brevoke\\b/i,\n /\\binsert\\b/i,\n /\\bupdate\\b/i,\n /\\bdelete\\b/i,\n /\\bcopy\\b/i,\n /\\bvacuum\\b/i,\n /\\breindex\\b/i,\n /\\bcluster\\b/i,\n]\n\nfunction assertRawQuerySafe(rawQuery) {\n if (typeof rawQuery !== 'string' || rawQuery.length === 0) {\n throw new Error('rawQuery must be a non-empty string')\n }\n // Reject multi-statement payloads \u2014 only single SELECT / WITH statements.\n // A trailing semicolon is tolerated but any content after it fails.\n var trimmed = rawQuery.trim().replace(/;\\s*$/, '')\n if (trimmed.indexOf(';') !== -1) {\n throw new Error('rawQuery must contain exactly one statement')\n }\n for (var i = 0; i < BLOCKED_RAW_QUERY_PATTERNS.length; i++) {\n if (BLOCKED_RAW_QUERY_PATTERNS[i].test(trimmed)) {\n throw new Error('rawQuery contains a blocked statement')\n }\n }\n}\n\nexport default async function handler(req, res) {\n").concat(generateReadGuardCall(tableName, 'req.query && req.query.rawQuery'), "\n const client = getClient()\n\n try {\n await client.connect()\n ").concat(schema ? "await client.query('SET search_path TO ".concat(schema, "')") : '', "\n\n // If the caller supplied a rawQuery, run it verbatim (after a safety\n // guard). The schema-driven branch below builds `SELECT * FROM ").concat(tableName, "`\n // with optional filters \u2014 it's the default read path and ignores\n // rawQuery. Page-load workflows and other consumers that need a JOIN\n // or a user-scoped filter pass their fully-rendered SQL here via\n // `fetchData({ rawQuery })` and expect it to execute verbatim.\n if (req.query && typeof req.query.rawQuery === 'string' && req.query.rawQuery.length > 0) {\n assertRawQuerySafe(req.query.rawQuery)\n const rawResult = await client.query(req.query.rawQuery)\n const rawRows = Array.isArray(rawResult?.rows) ? rawResult.rows : []\n const rawPlain = rawRows.map((row) =>\n row && typeof row.toJSON === 'function' ? row.toJSON() : row\n )\n const rawSafe = JSON.parse(JSON.stringify(rawPlain, dateReplacer))\n return res.status(200).json({\n success: true,\n // Every row here ends up in a browser (a page's props or a client\n // refetch), so no credential column survives, whatever joined it.\n data: __taWithoutCredentials(rawSafe, null),\n timestamp: Date.now()\n })\n }\n\n const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset } = req.query\n // A visitor never filters, sorts or searches by a column it is not served.\n if (__rowPolicy) __brpAssertVisibleFields(__rowPolicy, req.query)\n // The language the page is in - what a translatable sort orders by.\n const requestLocale = typeof req.query.").concat(REQUEST_LOCALE_PARAM, " === 'string' ? req.query.").concat(REQUEST_LOCALE_PARAM, " : null\n \n const conditions = []\n const queryParams = []\n let paramIndex = 1\n \n if (query) {\n let columns = []\n \n if (queryColumns) {\n const parsed = safeJSONParse(queryColumns)\n columns = Array.isArray(parsed) ? parsed : (parsed ? [parsed] : [])\n } else {\n try {\n const schemaQuery = `\n SELECT column_name \n FROM information_schema.columns \n WHERE table_name = $1\n ").concat(schema ? "AND table_schema = $2" : '', "\n ORDER BY ordinal_position\n `\n const schemaParams = ").concat(schema
|
|
53
56
|
? "[".concat(JSON.stringify(tableName), ", ").concat(JSON.stringify(schema), "]")
|
|
54
|
-
: "[".concat(JSON.stringify(tableName), "]"), "\n \n const schemaResult = await client.query(schemaQuery, schemaParams)\n columns = schemaResult.rows.map(row => row.column_name)\n } catch (schemaError) {\n console.warn('Failed to fetch column names from information_schema:', schemaError.message)\n }\n }\n \n if (columns.length > 0) {\n const pattern = '%' + escapeLikePattern(query) + '%'\n const placeholder = '$' + paramIndex\n paramIndex++\n queryParams.push(pattern)\n const searchConditions = columns.map(\n (col) => '\"' + sanitizeSearchIdentifier(col) + '\"::text ILIKE ' + placeholder + \" ESCAPE '|'\"\n )\n conditions.push('(' + searchConditions.join(' OR ') + ')')\n }\n }\n \n paramIndex = processFilters(filters, conditions, queryParams, paramIndex)\n \n let sql = `SELECT * FROM ").concat(tableName, "`\n \n if (conditions.length > 0) {\n sql += ` WHERE ${conditions.join(' AND ')}`\n }\n \n // Handle sorts - new array format. Two ORDER BY clauses are built: the one\n // actually used (a translatable column in the request's language, the\n // products table's price rewrites), and a plain-column twin kept only as\n // the fallback below.\n let orderBySql = ''\n let plainOrderBySql = ''\n if (sorts) {\n const parsedSorts = safeJSONParse(sorts)\n if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {\n const valid = parsedSorts.filter((sort) => sort && sort.field)\n const orderOf = (sort) => (sort.order || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'\n const orderClauses = valid.map((sort) => `${localizedSortFieldSql(sort.field, requestLocale) || sortFieldSql(sort.field)} ${orderOf(sort)}`)\n const plainClauses = valid.map((sort) => `${sortFallbackField(sort.field)} ${orderOf(sort)}`)\n\n if (orderClauses.length > 0) {\n // A deterministic tiebreaker keeps pagination stable: \"sort by\n // discount\" ties every undiscounted product at zero, and equal rows\n // with no defined order can repeat across pages or vanish between them.\n orderBySql = ` ORDER BY ${orderClauses.join(', ')}").concat(generateSortTiebreakSql(tableName), "`\n plainOrderBySql = ` ORDER BY ${plainClauses.join(', ')}").concat(generateSortTiebreakSql(tableName), "`\n }\n }\n } else if (sortBy) {\n const sortByDirection = (sortOrder || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'\n orderBySql = ` ORDER BY ${localizedSortFieldSql(sortBy, requestLocale) || sortBy} ${sortByDirection}`\n plainOrderBySql = ` ORDER BY ${sortBy} ${sortByDirection}`\n }\n const usedDiscountAwareSort = orderBySql !== plainOrderBySql\n\n const limitValue = limit || perPage\n const offsetValue = offset !== undefined ? parseInt(offset) : (page && perPage ? (parseInt(page) - 1) * parseInt(perPage) : undefined)\n\n let sqlTail = ''\n if (limitValue) {\n sqlTail += ` LIMIT ${limitValue}`\n }\n \n if (offsetValue !== undefined) {\n sqlTail += ` OFFSET ${offsetValue}`\n }\n\n const baseSql = sql\n sql = baseSql + orderBySql + sqlTail\n\n // The discount-aware price ordering is an inline sub-select over a JSON\n // column. It is written to be unraisable, but it runs inside ORDER BY for\n // the whole table - so if a database ever rejects it, fall back to ordering\n // by the stored list price rather than serving an empty products page. The\n // translatable-column ordering takes the same exit: a per-language column\n // the schema had at generation time may have been dropped since.\n let result\n try {\n result = await client.query(sql, queryParams)\n } catch (sortError) {\n if (!usedDiscountAwareSort) throw sortError\n console.warn(\n 'Rewritten sort failed; falling back to the plain column:',\n sortError && sortError.message\n )\n result = await client.query(baseSql + plainOrderBySql + sqlTail, queryParams)\n }\n const rows = Array.isArray(result?.rows) ? result.rows : []\n const plainRows = rows.map((row) =>\n row && typeof row.toJSON === 'function' ? row.toJSON() : row\n )\n const safeData = JSON.parse(JSON.stringify(plainRows, dateReplacer))\n ").concat(getTransformExpression(tableName)
|
|
57
|
+
: "[".concat(JSON.stringify(tableName), "]"), "\n \n const schemaResult = await client.query(schemaQuery, schemaParams)\n columns = schemaResult.rows.map(row => row.column_name)\n } catch (schemaError) {\n console.warn('Failed to fetch column names from information_schema:', schemaError.message)\n }\n }\n if (__rowPolicy) columns = __brpVisibleColumns(__rowPolicy, columns)\n \n if (columns.length > 0) {\n const pattern = '%' + escapeLikePattern(query) + '%'\n const placeholder = '$' + paramIndex\n paramIndex++\n queryParams.push(pattern)\n const searchConditions = columns.map(\n (col) => '\"' + sanitizeSearchIdentifier(col) + '\"::text ILIKE ' + placeholder + \" ESCAPE '|'\"\n )\n conditions.push('(' + searchConditions.join(' OR ') + ')')\n }\n }\n \n paramIndex = processFilters(filters, conditions, queryParams, paramIndex)\n if (__rowPolicy) conditions.push('(' + __rowPolicy.predicate + ')')\n \n let sql = `SELECT * FROM ").concat(tableName, "`\n \n if (conditions.length > 0) {\n sql += ` WHERE ${conditions.join(' AND ')}`\n }\n \n // Handle sorts - new array format. Two ORDER BY clauses are built: the one\n // actually used (a translatable column in the request's language, the\n // products table's price rewrites), and a plain-column twin kept only as\n // the fallback below.\n let orderBySql = ''\n let plainOrderBySql = ''\n if (sorts) {\n const parsedSorts = safeJSONParse(sorts)\n if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {\n const valid = parsedSorts.filter((sort) => sort && sort.field)\n valid.forEach((sort) => assertRequestColumn(sort.field, 'sort field'))\n const orderOf = (sort) => (sort.order || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'\n const orderClauses = valid.map((sort) => `${localizedSortFieldSql(sort.field, requestLocale) || sortFieldSql(sort.field)} ${orderOf(sort)}`)\n const plainClauses = valid.map((sort) => `${sortFallbackField(sort.field)} ${orderOf(sort)}`)\n\n if (orderClauses.length > 0) {\n // A deterministic tiebreaker keeps pagination stable: \"sort by\n // discount\" ties every undiscounted product at zero, and equal rows\n // with no defined order can repeat across pages or vanish between them.\n orderBySql = ` ORDER BY ${orderClauses.join(', ')}").concat(generateSortTiebreakSql(tableName), "`\n plainOrderBySql = ` ORDER BY ${plainClauses.join(', ')}").concat(generateSortTiebreakSql(tableName), "`\n }\n }\n } else if (sortBy) {\n assertRequestColumn(sortBy, 'sort field')\n const sortByDirection = (sortOrder || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'\n orderBySql = ` ORDER BY ${localizedSortFieldSql(sortBy, requestLocale) || sortBy} ${sortByDirection}`\n plainOrderBySql = ` ORDER BY ${sortBy} ${sortByDirection}`\n }\n const usedDiscountAwareSort = orderBySql !== plainOrderBySql\n\n const limitValue = requestRowCount(limit || perPage)\n const offsetValue = offset !== undefined ? parseInt(offset) : (page && perPage ? (parseInt(page) - 1) * parseInt(perPage) : undefined)\n\n let sqlTail = ''\n if (limitValue !== undefined) {\n sqlTail += ` LIMIT ${limitValue}`\n }\n \n if (offsetValue !== undefined) {\n sqlTail += ` OFFSET ${offsetValue}`\n }\n\n const baseSql = sql\n sql = baseSql + orderBySql + sqlTail\n\n // The discount-aware price ordering is an inline sub-select over a JSON\n // column. It is written to be unraisable, but it runs inside ORDER BY for\n // the whole table - so if a database ever rejects it, fall back to ordering\n // by the stored list price rather than serving an empty products page. The\n // translatable-column ordering takes the same exit: a per-language column\n // the schema had at generation time may have been dropped since.\n let result\n try {\n result = await client.query(sql, queryParams)\n } catch (sortError) {\n if (!usedDiscountAwareSort) throw sortError\n console.warn(\n 'Rewritten sort failed; falling back to the plain column:',\n sortError && sortError.message\n )\n result = await client.query(baseSql + plainOrderBySql + sqlTail, queryParams)\n }\n const rows = Array.isArray(result?.rows) ? result.rows : []\n const plainRows = rows.map((row) =>\n row && typeof row.toJSON === 'function' ? row.toJSON() : row\n )\n const safeData = JSON.parse(JSON.stringify(plainRows, dateReplacer))\n ").concat(getTransformExpression(tableName)
|
|
55
58
|
? "const transformedData = ".concat(getTransformExpression(tableName))
|
|
56
|
-
: '', "\n\n // Stamp each row with a globally-correct 1-based `__rowNumber` (the page\n // offset is applied server-side above), so list index columns keep counting\n // across pages instead of restarting at 1. Renderer-internal field \u2014 it is\n // filtered from ordinary field bindings and only read by the index column.\n const __finalData = ").concat(getTransformExpression(tableName) ? 'transformedData' : 'safeData', "\n const __rowNumberStart = (typeof offsetValue === 'number' && !isNaN(offsetValue)) ? offsetValue : 0\n const __numberedData = Array.isArray(__finalData)\n ? __finalData.map((__row, __i) =>\n __row && typeof __row === 'object' && !Array.isArray(__row)\n ? Object.assign({}, __row, { __rowNumber: __rowNumberStart + __i + 1 })\n : __row)\n : __finalData\n\n return res.status(200).json({\n success: true,\n data: __numberedData,\n timestamp: Date.now()\n })\n } catch (error) {\n console.error('Teleport DB fetch error:', error)\n return res.status(500).json({\n success: false,\n error: error.message
|
|
59
|
+
: '', "\n\n // Stamp each row with a globally-correct 1-based `__rowNumber` (the page\n // offset is applied server-side above), so list index columns keep counting\n // across pages instead of restarting at 1. Renderer-internal field \u2014 it is\n // filtered from ordinary field bindings and only read by the index column.\n const __finalData = ").concat(getTransformExpression(tableName) ? 'transformedData' : 'safeData', "\n const __rowNumberStart = (typeof offsetValue === 'number' && !isNaN(offsetValue)) ? offsetValue : 0\n const __numberedData = Array.isArray(__finalData)\n ? __finalData.map((__row, __i) =>\n __row && typeof __row === 'object' && !Array.isArray(__row)\n ? Object.assign({}, __row, { __rowNumber: __rowNumberStart + __i + 1 })\n : __row)\n : __finalData\n\n return res.status(200).json({\n success: true,\n data: __taWithoutCredentials(\n __rowPolicy ? __brpStripHidden(__rowPolicy, __numberedData) : __numberedData,\n ").concat(JSON.stringify(tableName), "\n ),\n timestamp: Date.now()\n })\n } catch (error) {\n console.error('Teleport DB fetch error:', error)\n // A database error names tables, columns and values: it stays in the server\n // log. Only an error this route raised for the caller (it carries a status)\n // is repeated to it.\n return res.status(error.status || 500).json({\n success: false,\n error: error.status ? error.message : 'Failed to fetch data',\n timestamp: Date.now()\n })\n } finally {\n if (client) {\n try {\n await client.end()\n } catch (error) {\n console.error('Error closing database client:', error)\n }\n }\n }\n}\n");
|
|
57
60
|
};
|
|
58
61
|
export var generateTeleportCountFetcher = function (config, tableName) {
|
|
59
62
|
var _a;
|
|
60
63
|
var dbConfig = config;
|
|
61
64
|
var hasSchema = !!((_a = dbConfig.options) === null || _a === void 0 ? void 0 : _a.schema);
|
|
62
|
-
return "\nasync function getCount(req, res) {\n const client = getClient()\n\n try {\n await client.connect()\n const { query, queryColumns, filters } = req.query\n const conditions = []\n const queryParams = []\n let paramIndex = 1\n\n if (query) {\n let columns = []\n \n if (queryColumns) {\n const parsed = safeJSONParse(queryColumns)\n columns = Array.isArray(parsed) ? parsed : [parsed]\n } else {\n try {\n const schemaQuery = `\n SELECT column_name \n FROM information_schema.columns \n WHERE table_name = $1\n ".concat(hasSchema ? "AND table_schema = $2" : '', "\n ORDER BY ordinal_position\n `\n const schemaParams = ").concat(hasSchema
|
|
65
|
+
return "\nasync function getCount(req, res) {\n".concat(generateReadGuardCall(tableName, null), "\n const client = getClient()\n\n try {\n await client.connect()\n const { query, queryColumns, filters } = req.query\n if (__rowPolicy) __brpAssertVisibleFields(__rowPolicy, req.query)\n const conditions = []\n const queryParams = []\n let paramIndex = 1\n\n if (query) {\n let columns = []\n \n if (queryColumns) {\n const parsed = safeJSONParse(queryColumns)\n columns = Array.isArray(parsed) ? parsed : [parsed]\n } else {\n try {\n const schemaQuery = `\n SELECT column_name \n FROM information_schema.columns \n WHERE table_name = $1\n ").concat(hasSchema ? "AND table_schema = $2" : '', "\n ORDER BY ordinal_position\n `\n const schemaParams = ").concat(hasSchema
|
|
63
66
|
? "[".concat(JSON.stringify(tableName), ", ").concat(JSON.stringify(dbConfig.options.schema), "]")
|
|
64
|
-
: "[".concat(JSON.stringify(tableName), "]"), "\n \n const schemaResult = await client.query(schemaQuery, schemaParams)\n columns = schemaResult.rows.map(row => row.column_name)\n } catch (schemaError) {\n console.warn('Failed to fetch column names from information_schema:', schemaError.message)\n }\n }\n \n if (columns.length > 0) {\n const pattern = '%' + escapeLikePattern(query) + '%'\n const placeholder = '$' + paramIndex\n paramIndex++\n queryParams.push(pattern)\n const searchConditions = columns\n .map(\n (col) => '\"' + sanitizeSearchIdentifier(col) + '\"::text ILIKE ' + placeholder + \" ESCAPE '|'\"\n )\n .join(' OR ')\n conditions.push('(' + searchConditions + ')')\n }\n }\n\n paramIndex = processFilters(filters, conditions, queryParams, paramIndex)\n\n let countSql = `SELECT COUNT(*) FROM ").concat(tableName, "`\n if (conditions.length > 0) {\n countSql += ` WHERE ${conditions.join(' AND ')}`\n }\n\n const result = await client.query(countSql, queryParams)\n const count = parseInt(result.rows[0].count, 10)\n\n return res.status(200).json({\n success: true,\n count: count,\n timestamp: Date.now()\n })\n } catch (error) {\n console.error('Error getting count:', error)\n return res.status(500).json({\n success: false,\n error: error.message
|
|
67
|
+
: "[".concat(JSON.stringify(tableName), "]"), "\n \n const schemaResult = await client.query(schemaQuery, schemaParams)\n columns = schemaResult.rows.map(row => row.column_name)\n } catch (schemaError) {\n console.warn('Failed to fetch column names from information_schema:', schemaError.message)\n }\n }\n if (__rowPolicy) columns = __brpVisibleColumns(__rowPolicy, columns)\n \n if (columns.length > 0) {\n const pattern = '%' + escapeLikePattern(query) + '%'\n const placeholder = '$' + paramIndex\n paramIndex++\n queryParams.push(pattern)\n const searchConditions = columns\n .map(\n (col) => '\"' + sanitizeSearchIdentifier(col) + '\"::text ILIKE ' + placeholder + \" ESCAPE '|'\"\n )\n .join(' OR ')\n conditions.push('(' + searchConditions + ')')\n }\n }\n\n paramIndex = processFilters(filters, conditions, queryParams, paramIndex)\n if (__rowPolicy) conditions.push('(' + __rowPolicy.predicate + ')')\n\n let countSql = `SELECT COUNT(*) FROM ").concat(tableName, "`\n if (conditions.length > 0) {\n countSql += ` WHERE ${conditions.join(' AND ')}`\n }\n\n const result = await client.query(countSql, queryParams)\n const count = parseInt(result.rows[0].count, 10)\n\n return res.status(200).json({\n success: true,\n count: count,\n timestamp: Date.now()\n })\n } catch (error) {\n console.error('Error getting count:', error)\n return res.status(error.status || 500).json({\n success: false,\n error: error.status ? error.message : 'Failed to get count',\n timestamp: Date.now()\n })\n } finally {\n if (client) {\n try {\n await client.end()\n } catch (error) {\n console.error('Error closing database client:', error)\n }\n }\n }\n}\n");
|
|
65
68
|
};
|
|
66
69
|
//# sourceMappingURL=teleport.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"teleport.js","sourceRoot":"","sources":["../../../src/fetchers/teleport.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,yBAAyB,EACzB,6BAA6B,EAC7B,+BAA+B,GAChC,MAAM,UAAU,CAAA;AACjB,OAAO,EACL,qBAAqB,EACrB,sBAAsB,EACtB,uBAAuB,GAExB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,+BAA+B,EAC/B,0BAA0B,EAC1B,uBAAuB,GACxB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,iCAAiC,EAAE,MAAM,0BAA0B,CAAA;AAC5E,OAAO,EAAE,uBAAuB,EAAE,8BAA8B,EAAE,MAAM,sBAAsB,CAAA;AAC9F,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"teleport.js","sourceRoot":"","sources":["../../../src/fetchers/teleport.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a per-table read route (`/api/teleport-<table>-<source>` and its count
|
|
3
|
+
* twin) serves a BROWSER of the tables whose unpublished rows sit beside the
|
|
4
|
+
* public ones. (The tables it serves a browser not at all — the AI assistant's
|
|
5
|
+
* among them — are `TableAccess.PROTECTED_TABLES`, guarded by `__taGuardRead`.)
|
|
6
|
+
*
|
|
7
|
+
* The route answers whoever asks, with the whole table. For the blog that was
|
|
8
|
+
* every draft, scheduled and archived post — and each author's email address;
|
|
9
|
+
* for the catalogue, every draft and inactive product. The storefront's own
|
|
10
|
+
* bindings ask for the public rows, but the route is a URL anyone can call
|
|
11
|
+
* without them. So a caller that is not the store itself (its server code, or a
|
|
12
|
+
* signed-in administrator — the generated admin edits drafts through these
|
|
13
|
+
* routes) is served only the rows the storefront publishes, never a hidden
|
|
14
|
+
* column, and may neither filter, sort nor search by a hidden column: a filter
|
|
15
|
+
* is an oracle for a column it never returns.
|
|
16
|
+
*/
|
|
17
|
+
export interface BrowserRowPolicy {
|
|
18
|
+
/** SQL every browser read of the table is narrowed with. */
|
|
19
|
+
predicate: string;
|
|
20
|
+
/** Keys never served to a browser, wherever they sit in a row (raw and transformed names). */
|
|
21
|
+
hiddenColumns: ReadonlyArray<string>;
|
|
22
|
+
}
|
|
23
|
+
export declare const BROWSER_ROW_POLICIES: Readonly<Record<string, BrowserRowPolicy>>;
|
|
24
|
+
/**
|
|
25
|
+
* ES5 source inlined after the table-access preamble (it uses `__ta*` and
|
|
26
|
+
* `__tqSessionToken`). Declares `__brpAccess(req, tableName, rawQuery)`, which
|
|
27
|
+
* resolves to `{ denied, policy }`: `denied` a `{ status, message }` refusal or
|
|
28
|
+
* null, `policy` the narrowing to apply to this read or null (the caller reads
|
|
29
|
+
* the table whole). Plus the helpers that apply a policy to a request and to
|
|
30
|
+
* its rows.
|
|
31
|
+
*/
|
|
32
|
+
export declare const generateBrowserRowPolicyCode: () => string;
|
|
33
|
+
/**
|
|
34
|
+
* Whether a per-table read route's answer depends on who asks — the rows it
|
|
35
|
+
* narrows, the tables it guards — so its response cache must be split by view.
|
|
36
|
+
*/
|
|
37
|
+
export declare const isViewDependentTable: (tableName: string, protectedTables: ReadonlyArray<string>) => boolean;
|
|
38
|
+
/**
|
|
39
|
+
* The early return right after the table-access guard, plus the policy
|
|
40
|
+
* (`__rowPolicy`, null or a policy) the handler narrows the rest of its read by.
|
|
41
|
+
*/
|
|
42
|
+
export declare const generateBrowserRowAccessCall: (tableName: string, rawQueryExpr: string | null) => string;
|
|
43
|
+
//# sourceMappingURL=browser-row-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-row-policy.d.ts","sourceRoot":"","sources":["../../../../src/fetchers/utils/browser-row-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAA;IACjB,8FAA8F;IAC9F,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;CACrC;AAED,eAAO,MAAM,oBAAoB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAa3E,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B,QACnC,MA8HL,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,oBAAoB,cACpB,MAAM,mBACA,cAAc,MAAM,CAAC,KACrC,OAUF,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,4BAA4B,cAC5B,MAAM,gBACH,MAAM,GAAG,IAAI,KAC1B,MASF,CAAA"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a per-table read route (`/api/teleport-<table>-<source>` and its count
|
|
3
|
+
* twin) serves a BROWSER of the tables whose unpublished rows sit beside the
|
|
4
|
+
* public ones. (The tables it serves a browser not at all — the AI assistant's
|
|
5
|
+
* among them — are `TableAccess.PROTECTED_TABLES`, guarded by `__taGuardRead`.)
|
|
6
|
+
*
|
|
7
|
+
* The route answers whoever asks, with the whole table. For the blog that was
|
|
8
|
+
* every draft, scheduled and archived post — and each author's email address;
|
|
9
|
+
* for the catalogue, every draft and inactive product. The storefront's own
|
|
10
|
+
* bindings ask for the public rows, but the route is a URL anyone can call
|
|
11
|
+
* without them. So a caller that is not the store itself (its server code, or a
|
|
12
|
+
* signed-in administrator — the generated admin edits drafts through these
|
|
13
|
+
* routes) is served only the rows the storefront publishes, never a hidden
|
|
14
|
+
* column, and may neither filter, sort nor search by a hidden column: a filter
|
|
15
|
+
* is an oracle for a column it never returns.
|
|
16
|
+
*/
|
|
17
|
+
export var BROWSER_ROW_POLICIES = {
|
|
18
|
+
// ONLY `published` is public — the blog's own rule (see the GUI's
|
|
19
|
+
// `blog-post-status.ts`): drafts, scheduled and archived posts are not.
|
|
20
|
+
teleport_blog_posts: {
|
|
21
|
+
predicate: "status = 'published'",
|
|
22
|
+
hiddenColumns: ['author_email', 'authorEmail'],
|
|
23
|
+
},
|
|
24
|
+
// The storefront sells `active` products; a draft or inactive one is the
|
|
25
|
+
// merchant's, spelled in any case the admin form stored it in.
|
|
26
|
+
teleport_products: {
|
|
27
|
+
predicate: "LOWER(TRIM(status)) = 'active'",
|
|
28
|
+
hiddenColumns: [],
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* ES5 source inlined after the table-access preamble (it uses `__ta*` and
|
|
33
|
+
* `__tqSessionToken`). Declares `__brpAccess(req, tableName, rawQuery)`, which
|
|
34
|
+
* resolves to `{ denied, policy }`: `denied` a `{ status, message }` refusal or
|
|
35
|
+
* null, `policy` the narrowing to apply to this read or null (the caller reads
|
|
36
|
+
* the table whole). Plus the helpers that apply a policy to a request and to
|
|
37
|
+
* its rows.
|
|
38
|
+
*/
|
|
39
|
+
export var generateBrowserRowPolicyCode = function () { return "// GENERATED \u2014 see browser-row-policy.ts in teleport-plugin-next-data-source.\nvar __BRP_POLICIES = ".concat(JSON.stringify(BROWSER_ROW_POLICIES), ";\n\n// Who is asking: the store's own server code and its administrators read every\n// row; anyone else is a visitor.\nasync function __brpReader(req) {\n if (__taIsInternalRequest(req)) return { trusted: true };\n var token = null;\n try {\n token = await __tqSessionToken(req);\n } catch (e) {\n token = null;\n }\n var role = token ? __taRoleOf(token) : null;\n return { trusted: !!role && __TA_TRUSTED_READER_ROLES.indexOf(role) !== -1 };\n}\n\nfunction __brpMentionsAny(rawQuery, tables) {\n for (var i = 0; i < tables.length; i++) {\n if (__taSqlMentionsTable(rawQuery, tables[i])) return true;\n }\n return false;\n}\n\n// A raw statement cannot be narrowed, so a visitor's statement that names a\n// narrowed table is refused whole. The session is only decoded when the read\n// touches one of these tables.\nasync function __brpAccess(req, tableName, rawQuery) {\n var raw = typeof rawQuery === 'string' && rawQuery.length > 0 ? rawQuery : null;\n var bare = __taNormalizeTable(tableName);\n var rawNarrowed = !!raw && __brpMentionsAny(raw, Object.keys(__BRP_POLICIES));\n var policy = raw ? null : (Object.prototype.hasOwnProperty.call(__BRP_POLICIES, bare) ? __BRP_POLICIES[bare] : null);\n if (!rawNarrowed && !policy) return { denied: null, policy: null };\n var reader = await __brpReader(req);\n if (reader.trusted) return { denied: null, policy: null };\n if (rawNarrowed) {\n return { denied: { status: 403, message: 'Forbidden: raw queries over this table are only available to server-side calls' }, policy: null };\n }\n return { denied: null, policy: policy };\n}\n\nfunction __brpColumnName(field) {\n var parts = String(field == null ? '' : field).replace(/\"/g, '').trim().split('.');\n return parts[parts.length - 1];\n}\n\nfunction __brpIsHidden(policy, field) {\n var name = __brpColumnName(field).toLowerCase();\n for (var i = 0; i < policy.hiddenColumns.length; i++) {\n if (policy.hiddenColumns[i].toLowerCase() === name) return true;\n }\n return false;\n}\n\n// Every field a request filters, sorts or searches by, however deep a filter\n// group nests it. A condition's value is data, not a field.\nfunction __brpRequestFields(value, out) {\n if (Array.isArray(value)) {\n for (var i = 0; i < value.length; i++) __brpRequestFields(value[i], out);\n } else if (value && typeof value === 'object') {\n if (typeof value.source === 'string') out.push(value.source);\n if (typeof value.field === 'string') out.push(value.field);\n var keys = Object.keys(value);\n for (var k = 0; k < keys.length; k++) {\n if (keys[k] === 'destination' || keys[k] === 'value') continue;\n if (value[keys[k]] && typeof value[keys[k]] === 'object') __brpRequestFields(value[keys[k]], out);\n }\n } else if (typeof value === 'string' && value) {\n out.push(value);\n }\n return out;\n}\n\nfunction __brpParse(value) {\n if (typeof value !== 'string') return value;\n try {\n return JSON.parse(value);\n } catch (e) {\n return value;\n }\n}\n\nfunction __brpAssertVisibleFields(policy, query) {\n var q = query || {};\n var fields = [];\n __brpRequestFields(__brpParse(q.filters), fields);\n __brpRequestFields(__brpParse(q.sorts), fields);\n __brpRequestFields(__brpParse(q.queryColumns), fields);\n if (typeof q.sortBy === 'string') fields.push(q.sortBy);\n for (var i = 0; i < fields.length; i++) {\n if (__brpIsHidden(policy, fields[i])) {\n var err = new Error('Forbidden: ' + __brpColumnName(fields[i]) + ' is not readable');\n err.status = 403;\n throw err;\n }\n }\n}\n\nfunction __brpVisibleColumns(policy, columns) {\n return columns.filter(function(column) { return !__brpIsHidden(policy, column); });\n}\n\n// The rows without a hidden key, at any depth: a transformed row nests related\n// rows (a post's related and adjacent posts) built the same way.\nfunction __brpStripHidden(policy, value) {\n if (Array.isArray(value)) {\n return value.map(function(entry) { return __brpStripHidden(policy, entry); });\n }\n if (!value || typeof value !== 'object') return value;\n var copy = {};\n Object.keys(value).forEach(function(key) {\n if (!__brpIsHidden(policy, key)) copy[key] = __brpStripHidden(policy, value[key]);\n });\n return copy;\n}\n\n// Whether this request is answered exactly as an anonymous visitor's would be,\n// so a response cached for one may serve the other. A read whose answer depends\n// on who asks (`viewDependent`: a narrowed, protected or admin-only table) or\n// that runs a raw statement differs for the store's own server code and its\n// administrators, and must never share a cache entry with a visitor.\nasync function __brpSharesPublicView(req, viewDependent) {\n var raw = !!(req && req.query && typeof req.query.rawQuery === 'string' && req.query.rawQuery.length > 0);\n if (!viewDependent && !raw) return true;\n return !(await __brpReader(req)).trusted;\n}\n"); };
|
|
40
|
+
/**
|
|
41
|
+
* Whether a per-table read route's answer depends on who asks — the rows it
|
|
42
|
+
* narrows, the tables it guards — so its response cache must be split by view.
|
|
43
|
+
*/
|
|
44
|
+
export var isViewDependentTable = function (tableName, protectedTables) {
|
|
45
|
+
var parts = String(tableName || '')
|
|
46
|
+
.trim()
|
|
47
|
+
.toLowerCase()
|
|
48
|
+
.split('.');
|
|
49
|
+
var bare = parts[parts.length - 1].replace(/"/g, '');
|
|
50
|
+
return (Object.prototype.hasOwnProperty.call(BROWSER_ROW_POLICIES, bare) ||
|
|
51
|
+
protectedTables.indexOf(bare) !== -1);
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* The early return right after the table-access guard, plus the policy
|
|
55
|
+
* (`__rowPolicy`, null or a policy) the handler narrows the rest of its read by.
|
|
56
|
+
*/
|
|
57
|
+
export var generateBrowserRowAccessCall = function (tableName, rawQueryExpr) {
|
|
58
|
+
return " const __rowAccess = await __brpAccess(req, ".concat(JSON.stringify(tableName), ", ").concat(rawQueryExpr || 'null', ")\n if (__rowAccess.denied) {\n return res.status(__rowAccess.denied.status).json({ success: false, error: __rowAccess.denied.message, timestamp: Date.now() })\n }\n const __rowPolicy = __rowAccess.policy\n");
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=browser-row-policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-row-policy.js","sourceRoot":"","sources":["../../../../src/fetchers/utils/browser-row-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AASH,MAAM,CAAC,IAAM,oBAAoB,GAA+C;IAC9E,kEAAkE;IAClE,wEAAwE;IACxE,mBAAmB,EAAE;QACnB,SAAS,EAAE,sBAAsB;QACjC,aAAa,EAAE,CAAC,cAAc,EAAE,aAAa,CAAC;KAC/C;IACD,yEAAyE;IACzE,+DAA+D;IAC/D,iBAAiB,EAAE;QACjB,SAAS,EAAE,gCAAgC;QAC3C,aAAa,EAAE,EAAE;KAClB;CACF,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,IAAM,4BAA4B,GACvC,cAAc,OAAA,mHACO,IAAI,CAAC,SAAS,CAAC,oBAAoB,CAAC,y1JA6H1D,EA9He,CA8Hf,CAAA;AAED;;;GAGG;AACH,MAAM,CAAC,IAAM,oBAAoB,GAAG,UAClC,SAAiB,EACjB,eAAsC;IAEtC,IAAM,KAAK,GAAG,MAAM,CAAC,SAAS,IAAI,EAAE,CAAC;SAClC,IAAI,EAAE;SACN,WAAW,EAAE;SACb,KAAK,CAAC,GAAG,CAAC,CAAA;IACb,IAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;IACtD,OAAO,CACL,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,oBAAoB,EAAE,IAAI,CAAC;QAChE,eAAe,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CACrC,CAAA;AACH,CAAC,CAAA;AAED;;;GAGG;AACH,MAAM,CAAC,IAAM,4BAA4B,GAAG,UAC1C,SAAiB,EACjB,YAA2B;IAE3B,OAAO,uDAAgD,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,eAC9E,YAAY,IAAI,MAAM,yNAMzB,CAAA;AACD,CAAC,CAAA"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a per-table read route takes from its query string and splices into
|
|
3
|
+
* SQL: filter columns, sort columns and a row count. Values are bound as
|
|
4
|
+
* parameters; these three are not, so each is checked for the one shape it
|
|
5
|
+
* may have before it reaches the statement.
|
|
6
|
+
*/
|
|
7
|
+
export declare const generateRequestSqlGuardsCode: () => string;
|
|
8
|
+
//# sourceMappingURL=request-sql-guards.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-sql-guards.d.ts","sourceRoot":"","sources":["../../../../src/fetchers/utils/request-sql-guards.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,4BAA4B,QAAO,MAkB/C,CAAA"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a per-table read route takes from its query string and splices into
|
|
3
|
+
* SQL: filter columns, sort columns and a row count. Values are bound as
|
|
4
|
+
* parameters; these three are not, so each is checked for the one shape it
|
|
5
|
+
* may have before it reaches the statement.
|
|
6
|
+
*/
|
|
7
|
+
export var generateRequestSqlGuardsCode = function () { return "\n// A column name as this route accepts one from a request: plain, optionally\n// quoted or table-qualified. Anything else - an expression, a sub-select - is\n// SQL the caller wrote, and it would run as part of this statement.\nconst REQUEST_COLUMN_RE = /^\"?[A-Za-z_][A-Za-z0-9_]*\"?(?:\\.\"?[A-Za-z_][A-Za-z0-9_]*\"?)?$/\nfunction assertRequestColumn(field, label) {\n if (typeof field !== 'string' || !REQUEST_COLUMN_RE.test(field)) {\n const error = new Error('Invalid ' + label)\n error.status = 400\n throw error\n }\n}\n\n// A row count from a request, as a whole number or nothing.\nfunction requestRowCount(value) {\n const parsed = parseInt(value, 10)\n return isNaN(parsed) || parsed < 0 ? undefined : parsed\n}\n"; };
|
|
8
|
+
//# sourceMappingURL=request-sql-guards.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-sql-guards.js","sourceRoot":"","sources":["../../../../src/fetchers/utils/request-sql-guards.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,IAAM,4BAA4B,GAAG,cAAc,OAAA,quBAkBzD,EAlByD,CAkBzD,CAAA"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The REST API data source's response cache: `config.cache` as the editor
|
|
3
|
+
* stores it on the data source.
|
|
4
|
+
*/
|
|
5
|
+
export interface RESTAPICacheConfig {
|
|
6
|
+
enabled?: boolean;
|
|
7
|
+
ttlSeconds?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The window, in whole seconds, a REST API data source caches its upstream
|
|
11
|
+
* response for — or `undefined` when caching is off, which keeps the emitted
|
|
12
|
+
* fetcher exactly what it was before this option existed.
|
|
13
|
+
*/
|
|
14
|
+
export declare const resolveRESTAPICacheTTLSeconds: (cache: RESTAPICacheConfig | undefined) => number | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Runtime helpers of a cached REST API fetcher.
|
|
17
|
+
*
|
|
18
|
+
* The cache sits under the handler, around the upstream `fetch` only: every
|
|
19
|
+
* caller — the API route, `getStaticProps` through `fetchData`, the count
|
|
20
|
+
* route, the global-state route — asks the same URL, and search, filters, sort
|
|
21
|
+
* and paging are applied to the cached payload afterwards. One upstream call
|
|
22
|
+
* per window therefore serves every page, filter and visitor.
|
|
23
|
+
*
|
|
24
|
+
* - The store lives on `globalThis`, so every data-source module in the
|
|
25
|
+
* process (each API route is its own bundle) shares it. On a serverless host
|
|
26
|
+
* that is one store per warm instance; the `s-maxage` header below is what
|
|
27
|
+
* carries the window to the CDN in front of all of them.
|
|
28
|
+
* - Entries are keyed by the whole upstream request (method, URL, headers,
|
|
29
|
+
* body) and hold the response TEXT: every reader parses its own copy, so a
|
|
30
|
+
* handler that sorts in place can never reorder another request's rows.
|
|
31
|
+
* - Freshness is judged by the READER's window, so two data sources calling
|
|
32
|
+
* the same URL with different windows each get what they asked for.
|
|
33
|
+
* - Only a 2xx JSON response is kept. Errors, non-JSON bodies and network
|
|
34
|
+
* failures reach the caller as before and are retried by the next request.
|
|
35
|
+
*
|
|
36
|
+
* Emitted inside a template literal: no backticks or template placeholders in
|
|
37
|
+
* here, not even in comments.
|
|
38
|
+
*/
|
|
39
|
+
export declare const generateRESTAPIResponseCacheCode: (ttlSeconds: number) => string;
|
|
40
|
+
//# sourceMappingURL=rest-api-response-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rest-api-response-cache.d.ts","sourceRoot":"","sources":["../../../../src/fetchers/utils/rest-api-response-cache.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAoBD;;;;GAIG;AACH,eAAO,MAAM,6BAA6B,UACjC,kBAAkB,GAAG,SAAS,KACpC,MAAM,GAAG,SAYX,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,gCAAgC,eAAgB,MAAM,KAAG,MAiHrE,CAAA"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { DataCache } from '@teleporthq/teleport-shared';
|
|
2
|
+
/** A year: past that, the window only ever ends with the next deployment. */
|
|
3
|
+
var MAX_TTL_SECONDS = 31536000;
|
|
4
|
+
/**
|
|
5
|
+
* Bounds for the one store every REST API data source in a server process
|
|
6
|
+
* shares. Entries are raw JSON text, so the character budget is close to the
|
|
7
|
+
* memory the cache holds.
|
|
8
|
+
*/
|
|
9
|
+
var MAX_ENTRIES = 100;
|
|
10
|
+
var MAX_CHARS = 33554432;
|
|
11
|
+
/**
|
|
12
|
+
* How long a request may wait on an upstream call another request started.
|
|
13
|
+
* An older call is treated as stuck and a new one is made, so one hung socket
|
|
14
|
+
* cannot hold every later visitor.
|
|
15
|
+
*/
|
|
16
|
+
var IN_FLIGHT_JOIN_MS = 10000;
|
|
17
|
+
/**
|
|
18
|
+
* The window, in whole seconds, a REST API data source caches its upstream
|
|
19
|
+
* response for — or `undefined` when caching is off, which keeps the emitted
|
|
20
|
+
* fetcher exactly what it was before this option existed.
|
|
21
|
+
*/
|
|
22
|
+
export var resolveRESTAPICacheTTLSeconds = function (cache) {
|
|
23
|
+
if (!cache || typeof cache !== 'object' || cache.enabled !== true) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
var ttlSeconds = cache.ttlSeconds;
|
|
27
|
+
var seconds = typeof ttlSeconds === 'number' && Number.isFinite(ttlSeconds) && ttlSeconds > 0
|
|
28
|
+
? Math.max(1, Math.floor(ttlSeconds))
|
|
29
|
+
: DataCache.DEFAULT_CACHE_TTL_SECONDS;
|
|
30
|
+
return Math.min(seconds, MAX_TTL_SECONDS);
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Runtime helpers of a cached REST API fetcher.
|
|
34
|
+
*
|
|
35
|
+
* The cache sits under the handler, around the upstream `fetch` only: every
|
|
36
|
+
* caller — the API route, `getStaticProps` through `fetchData`, the count
|
|
37
|
+
* route, the global-state route — asks the same URL, and search, filters, sort
|
|
38
|
+
* and paging are applied to the cached payload afterwards. One upstream call
|
|
39
|
+
* per window therefore serves every page, filter and visitor.
|
|
40
|
+
*
|
|
41
|
+
* - The store lives on `globalThis`, so every data-source module in the
|
|
42
|
+
* process (each API route is its own bundle) shares it. On a serverless host
|
|
43
|
+
* that is one store per warm instance; the `s-maxage` header below is what
|
|
44
|
+
* carries the window to the CDN in front of all of them.
|
|
45
|
+
* - Entries are keyed by the whole upstream request (method, URL, headers,
|
|
46
|
+
* body) and hold the response TEXT: every reader parses its own copy, so a
|
|
47
|
+
* handler that sorts in place can never reorder another request's rows.
|
|
48
|
+
* - Freshness is judged by the READER's window, so two data sources calling
|
|
49
|
+
* the same URL with different windows each get what they asked for.
|
|
50
|
+
* - Only a 2xx JSON response is kept. Errors, non-JSON bodies and network
|
|
51
|
+
* failures reach the caller as before and are retried by the next request.
|
|
52
|
+
*
|
|
53
|
+
* Emitted inside a template literal: no backticks or template placeholders in
|
|
54
|
+
* here, not even in comments.
|
|
55
|
+
*/
|
|
56
|
+
export var generateRESTAPIResponseCacheCode = function (ttlSeconds) {
|
|
57
|
+
return "const REST_API_CACHE_TTL_MS = ".concat(ttlSeconds * 1000, "\nconst REST_API_CACHE_MAX_ENTRIES = ").concat(MAX_ENTRIES, "\nconst REST_API_CACHE_MAX_CHARS = ").concat(MAX_CHARS, "\nconst REST_API_IN_FLIGHT_JOIN_MS = ").concat(IN_FLIGHT_JOIN_MS, "\n\n// One store per server process, shared by every REST API data source module.\nif (!globalThis.__tqRestApiCache) {\n globalThis.__tqRestApiCache = { entries: new Map(), inFlight: new Map(), chars: 0 }\n}\nconst restApiCache = globalThis.__tqRestApiCache\n\nfunction restApiCacheKey(url, options) {\n return JSON.stringify([options.method, url, options.headers, options.body === undefined ? null : options.body])\n}\n\nfunction forgetRestApiResponse(key) {\n const entry = restApiCache.entries.get(key)\n if (entry) {\n restApiCache.entries.delete(key)\n restApiCache.chars -= entry.bodyText.length\n }\n}\n\nfunction rememberRestApiResponse(key, entry) {\n forgetRestApiResponse(key)\n if (entry.bodyText.length > REST_API_CACHE_MAX_CHARS) {\n return\n }\n restApiCache.entries.set(key, entry)\n restApiCache.chars += entry.bodyText.length\n // A Map iterates in insertion order and every hit re-inserts its entry, so\n // the first key is always the least recently used one.\n while (\n restApiCache.entries.size > REST_API_CACHE_MAX_ENTRIES ||\n restApiCache.chars > REST_API_CACHE_MAX_CHARS\n ) {\n forgetRestApiResponse(restApiCache.entries.keys().next().value)\n }\n}\n\nasync function requestRestApi(url, options) {\n const response = await fetch(url, options)\n if (!response.ok) {\n return { ok: false, status: response.status, statusText: response.statusText }\n }\n const bodyText = await response.text()\n // Parsed once here so a body that is not JSON fails this request, as\n // response.json() would, instead of being cached and failing every request\n // until the window ends.\n JSON.parse(bodyText)\n return {\n ok: true,\n status: response.status,\n statusText: response.statusText,\n bodyText,\n fetchedAt: Date.now(),\n }\n}\n\nfunction fetchRestApiWithCache(url, options) {\n const key = restApiCacheKey(url, options)\n const now = Date.now()\n\n const cached = restApiCache.entries.get(key)\n if (cached && now >= cached.fetchedAt && now - cached.fetchedAt < REST_API_CACHE_TTL_MS) {\n restApiCache.entries.delete(key)\n restApiCache.entries.set(key, cached)\n return Promise.resolve(cached)\n }\n\n // Concurrent misses share one upstream call instead of each making their own.\n const pending = restApiCache.inFlight.get(key)\n if (pending && now - pending.startedAt < REST_API_IN_FLIGHT_JOIN_MS) {\n return pending.promise\n }\n\n const flight = { startedAt: now, promise: null }\n const release = () => {\n if (restApiCache.inFlight.get(key) === flight) {\n restApiCache.inFlight.delete(key)\n }\n }\n flight.promise = requestRestApi(url, options).then(\n (result) => {\n release()\n if (result.ok) {\n rememberRestApiResponse(key, result)\n }\n return result\n },\n (error) => {\n release()\n throw error\n }\n )\n restApiCache.inFlight.set(key, flight)\n return flight.promise\n}\n\n// Lets the CDN answer this exact URL until the upstream response it was built\n// from expires, so neither layer serves it longer than the window. Only a real\n// GET response gets the header: fetchData, fetchCount and tqWithCache call the\n// handler with a bare { status, json } object.\nfunction applyRestApiCacheHeaders(req, res, fetchedAt) {\n if (!req || req.method !== 'GET' || !res || typeof res.setHeader !== 'function') {\n return\n }\n const remainingSeconds = Math.floor((fetchedAt + REST_API_CACHE_TTL_MS - Date.now()) / 1000)\n if (remainingSeconds > 0) {\n res.setHeader('Cache-Control', 'public, max-age=0, s-maxage=' + remainingSeconds)\n }\n}");
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=rest-api-response-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rest-api-response-cache.js","sourceRoot":"","sources":["../../../../src/fetchers/utils/rest-api-response-cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAA;AAWvD,6EAA6E;AAC7E,IAAM,eAAe,GAAG,QAAU,CAAA;AAElC;;;;GAIG;AACH,IAAM,WAAW,GAAG,GAAG,CAAA;AACvB,IAAM,SAAS,GAAG,QAAU,CAAA;AAE5B;;;;GAIG;AACH,IAAM,iBAAiB,GAAG,KAAM,CAAA;AAEhC;;;;GAIG;AACH,MAAM,CAAC,IAAM,6BAA6B,GAAG,UAC3C,KAAqC;IAErC,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,KAAK,IAAI,EAAE;QACjE,OAAO,SAAS,CAAA;KACjB;IAEO,IAAA,UAAU,GAAK,KAAK,WAAV,CAAU;IAC5B,IAAM,OAAO,GACX,OAAO,UAAU,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,UAAU,GAAG,CAAC;QAC7E,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QACrC,CAAC,CAAC,SAAS,CAAC,yBAAyB,CAAA;IAEzC,OAAO,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,eAAe,CAAC,CAAA;AAC3C,CAAC,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,CAAC,IAAM,gCAAgC,GAAG,UAAC,UAAkB;IACjE,OAAO,wCAAiC,UAAU,GAAG,IAAI,kDACtB,WAAW,gDACb,SAAS,kDACP,iBAAiB,yjHA4GpD,CAAA;AACF,CAAC,CAAA"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The money-table guard for a generated per-table read route
|
|
3
|
+
* (`/api/teleport-<table>-<source>` and its `-count` twin).
|
|
4
|
+
*
|
|
5
|
+
* Every table a page binds gets such a route, and the route serves the whole
|
|
6
|
+
* table — plus any single SELECT through `?rawQuery=` — to whoever asks. For a
|
|
7
|
+
* catalogue that is the product; for gift cards and voucher codes it is theft.
|
|
8
|
+
* The guard refuses a protected table (see `TableAccess.PROTECTED_TABLES`)
|
|
9
|
+
* unless the caller is a server segment presenting the app secret or a
|
|
10
|
+
* signed-in member of a trusted role — the generated admin, whose list pages
|
|
11
|
+
* are the one browser reader these tables have. It is followed by the
|
|
12
|
+
* browser row policy (see `browser-row-policy.ts`): unpublished rows and
|
|
13
|
+
* private columns of the public tables, and the admin-only tables.
|
|
14
|
+
*/
|
|
15
|
+
/** Declares the session resolver and the `__ta*` guard once per module. */
|
|
16
|
+
export declare const generateTableAccessPreamble: (trustedReaderRoles: ReadonlyArray<string>) => string;
|
|
17
|
+
/**
|
|
18
|
+
* The early return at the top of a handler, before the client connects:
|
|
19
|
+
* `tableName` is the module's table, `rawQueryExpr` the request's raw statement
|
|
20
|
+
* (or `null` where the handler accepts none).
|
|
21
|
+
*/
|
|
22
|
+
export declare const generateReadGuardCall: (tableName: string, rawQueryExpr: string | null) => string;
|
|
23
|
+
//# sourceMappingURL=table-access-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"table-access-guard.d.ts","sourceRoot":"","sources":["../../../../src/fetchers/utils/table-access-guard.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;GAaG;AAEH,2EAA2E;AAC3E,eAAO,MAAM,2BAA2B,uBAAwB,cAAc,MAAM,CAAC,KAAG,MAIvF,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,cAAe,MAAM,gBAAgB,MAAM,GAAG,IAAI,KAAG,MAQtF,CAAA"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { SessionCookieResolver, TableAccess } from '@teleporthq/teleport-shared';
|
|
2
|
+
import { generateBrowserRowAccessCall, generateBrowserRowPolicyCode } from './browser-row-policy';
|
|
3
|
+
/**
|
|
4
|
+
* The money-table guard for a generated per-table read route
|
|
5
|
+
* (`/api/teleport-<table>-<source>` and its `-count` twin).
|
|
6
|
+
*
|
|
7
|
+
* Every table a page binds gets such a route, and the route serves the whole
|
|
8
|
+
* table — plus any single SELECT through `?rawQuery=` — to whoever asks. For a
|
|
9
|
+
* catalogue that is the product; for gift cards and voucher codes it is theft.
|
|
10
|
+
* The guard refuses a protected table (see `TableAccess.PROTECTED_TABLES`)
|
|
11
|
+
* unless the caller is a server segment presenting the app secret or a
|
|
12
|
+
* signed-in member of a trusted role — the generated admin, whose list pages
|
|
13
|
+
* are the one browser reader these tables have. It is followed by the
|
|
14
|
+
* browser row policy (see `browser-row-policy.ts`): unpublished rows and
|
|
15
|
+
* private columns of the public tables, and the admin-only tables.
|
|
16
|
+
*/
|
|
17
|
+
/** Declares the session resolver and the `__ta*` guard once per module. */
|
|
18
|
+
export var generateTableAccessPreamble = function (trustedReaderRoles) {
|
|
19
|
+
return "".concat(SessionCookieResolver.generateCommonJsSessionTokenResolverCode(), "\n").concat(TableAccess.generateTableAccessHelperCode({ trustedReaderRoles: trustedReaderRoles }), "\n").concat(generateBrowserRowPolicyCode());
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* The early return at the top of a handler, before the client connects:
|
|
23
|
+
* `tableName` is the module's table, `rawQueryExpr` the request's raw statement
|
|
24
|
+
* (or `null` where the handler accepts none).
|
|
25
|
+
*/
|
|
26
|
+
export var generateReadGuardCall = function (tableName, rawQueryExpr) {
|
|
27
|
+
return " const __access = await __taGuardRead(req, ".concat(JSON.stringify(tableName), ", ").concat(rawQueryExpr || 'null', ")\n if (__access) {\n return res.status(__access.status).json({ success: false, error: __access.message, timestamp: Date.now() })\n }\n").concat(generateBrowserRowAccessCall(tableName, rawQueryExpr));
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=table-access-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"table-access-guard.js","sourceRoot":"","sources":["../../../../src/fetchers/utils/table-access-guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAA;AAChF,OAAO,EAAE,4BAA4B,EAAE,4BAA4B,EAAE,MAAM,sBAAsB,CAAA;AAEjG;;;;;;;;;;;;;GAaG;AAEH,2EAA2E;AAC3E,MAAM,CAAC,IAAM,2BAA2B,GAAG,UAAC,kBAAyC;IACnF,OAAO,UAAG,qBAAqB,CAAC,wCAAwC,EAAE,eAC1E,WAAW,CAAC,6BAA6B,CAAC,EAAE,kBAAkB,oBAAA,EAAE,CAAC,eACjE,4BAA4B,EAAE,CAAE,CAAA;AAClC,CAAC,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,IAAM,qBAAqB,GAAG,UAAC,SAAiB,EAAE,YAA2B;IAClF,OAAO,sDAA+C,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,eAC7E,YAAY,IAAI,MAAM,yJAKxB,4BAA4B,CAAC,SAAS,EAAE,YAAY,CAAC,CAAE,CAAA;AACzD,CAAC,CAAA"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as types from '@babel/types';
|
|
2
|
+
export declare const extractItemsPath: (source: unknown, dataSourceIdentifier: string, dataSourceType: string) => string[] | undefined;
|
|
3
|
+
/**
|
|
4
|
+
* Push `<key>: '["templates"]'` onto a params object. Fetch calls send it as
|
|
5
|
+
* `itemsPath` (process the inner array, keep the wrapper the page reads back
|
|
6
|
+
* through); count calls send it as `collectionPath`, so the count handler
|
|
7
|
+
* measures the inner array instead of the wrapper.
|
|
8
|
+
*/
|
|
9
|
+
export declare const appendItemsPathParam: (paramsProps: types.ObjectProperty[], itemsPath: string[] | undefined, key: 'itemsPath' | 'collectionPath') => void;
|
|
10
|
+
//# sourceMappingURL=items-path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"items-path.d.ts","sourceRoot":"","sources":["../../src/items-path.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAsBrC,eAAO,MAAM,gBAAgB,WACnB,OAAO,wBACO,MAAM,kBACZ,MAAM,KACrB,MAAM,EAAE,GAAG,SA4Bb,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,gBAClB,MAAM,cAAc,EAAE,aACxB,MAAM,EAAE,GAAG,SAAS,OAC1B,WAAW,GAAG,gBAAgB,KAClC,IAOF,CAAA"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as types from '@babel/types';
|
|
2
|
+
/**
|
|
3
|
+
* Where a paginated/searchable list's rows sit inside the payload its data
|
|
4
|
+
* source returns, read off the repeater's `source` expression.
|
|
5
|
+
*
|
|
6
|
+
* A REST API or JavaScript source may answer `{ templates: [...], total }`, and
|
|
7
|
+
* the editor binds the list to `All_Templates_data_data?.templates || []`. The
|
|
8
|
+
* generated fetcher only searches, filters, sorts and slices a payload that IS
|
|
9
|
+
* an array, so without this every row came back on every page (the canvas
|
|
10
|
+
* slices on the client and looked right) and the count measured the wrapper as
|
|
11
|
+
* 0 rows, disabling "Next".
|
|
12
|
+
*
|
|
13
|
+
* Returns undefined when the list is bound to the payload itself, or when the
|
|
14
|
+
* source is anything other than a plain property chain off the data source's
|
|
15
|
+
* render prop — an expression this cannot read is left exactly as before.
|
|
16
|
+
*/
|
|
17
|
+
var SEGMENT_RE = /^(?:\?\.([A-Za-z_$][\w$]*)|\.([A-Za-z_$][\w$]*)|(?:\?\.)?\[\s*(?:"([^"]*)"|'([^']*)')\s*\])/;
|
|
18
|
+
var PATH_AWARE_SOURCE_TYPES = new Set(['rest-api', 'javascript']);
|
|
19
|
+
export var extractItemsPath = function (source, dataSourceIdentifier, dataSourceType) {
|
|
20
|
+
var _a, _b, _c;
|
|
21
|
+
if (typeof source !== 'string' || !dataSourceIdentifier) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
if (!PATH_AWARE_SOURCE_TYPES.has(dataSourceType)) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
var rest = source
|
|
28
|
+
.trim()
|
|
29
|
+
.replace(/\s*\|\|\s*\[\s*\]$/, '')
|
|
30
|
+
.trim();
|
|
31
|
+
if (!rest.startsWith(dataSourceIdentifier)) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
rest = rest.slice(dataSourceIdentifier.length);
|
|
35
|
+
var path = [];
|
|
36
|
+
while (rest.length > 0) {
|
|
37
|
+
var match = rest.match(SEGMENT_RE);
|
|
38
|
+
if (!match) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
path.push((_c = (_b = (_a = match[1]) !== null && _a !== void 0 ? _a : match[2]) !== null && _b !== void 0 ? _b : match[3]) !== null && _c !== void 0 ? _c : match[4]);
|
|
42
|
+
rest = rest.slice(match[0].length);
|
|
43
|
+
}
|
|
44
|
+
return path.length > 0 ? path : undefined;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Push `<key>: '["templates"]'` onto a params object. Fetch calls send it as
|
|
48
|
+
* `itemsPath` (process the inner array, keep the wrapper the page reads back
|
|
49
|
+
* through); count calls send it as `collectionPath`, so the count handler
|
|
50
|
+
* measures the inner array instead of the wrapper.
|
|
51
|
+
*/
|
|
52
|
+
export var appendItemsPathParam = function (paramsProps, itemsPath, key) {
|
|
53
|
+
if (!itemsPath || itemsPath.length === 0) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
paramsProps.push(types.objectProperty(types.identifier(key), types.stringLiteral(JSON.stringify(itemsPath))));
|
|
57
|
+
};
|
|
58
|
+
//# sourceMappingURL=items-path.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"items-path.js","sourceRoot":"","sources":["../../src/items-path.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAErC;;;;;;;;;;;;;;GAcG;AACH,IAAM,UAAU,GACd,6FAA6F,CAAA;AAE/F,IAAM,uBAAuB,GAAG,IAAI,GAAG,CAAC,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC,CAAA;AAEnE,MAAM,CAAC,IAAM,gBAAgB,GAAG,UAC9B,MAAe,EACf,oBAA4B,EAC5B,cAAsB;;IAEtB,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,oBAAoB,EAAE;QACvD,OAAO,SAAS,CAAA;KACjB;IACD,IAAI,CAAC,uBAAuB,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE;QAChD,OAAO,SAAS,CAAA;KACjB;IAED,IAAI,IAAI,GAAG,MAAM;SACd,IAAI,EAAE;SACN,OAAO,CAAC,oBAAoB,EAAE,EAAE,CAAC;SACjC,IAAI,EAAE,CAAA;IACT,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,oBAAoB,CAAC,EAAE;QAC1C,OAAO,SAAS,CAAA;KACjB;IACD,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAA;IAE9C,IAAM,IAAI,GAAa,EAAE,CAAA;IACzB,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE;QACtB,IAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;QACpC,IAAI,CAAC,KAAK,EAAE;YACV,OAAO,SAAS,CAAA;SACjB;QACD,IAAI,CAAC,IAAI,CAAC,MAAA,MAAA,MAAA,KAAK,CAAC,CAAC,CAAC,mCAAI,KAAK,CAAC,CAAC,CAAC,mCAAI,KAAK,CAAC,CAAC,CAAC,mCAAI,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;QACvD,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;KACnC;IAED,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAA;AAC3C,CAAC,CAAA;AAED;;;;;GAKG;AACH,MAAM,CAAC,IAAM,oBAAoB,GAAG,UAClC,WAAmC,EACnC,SAA+B,EAC/B,GAAmC;IAEnC,IAAI,CAAC,SAAS,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE;QACxC,OAAM;KACP;IACD,WAAW,CAAC,IAAI,CACd,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,CAC5F,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"localized-columns.d.ts","sourceRoot":"","sources":["../../src/localized-columns.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAA;AAEjE;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;AAQvE;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,gBACrB,MAAM,EAAE,gBACP,mBAAmB,GAAG,SAAS,KAC5C,kBAmBF,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,8BAA8B,aAAc,kBAAkB,KAAG,
|
|
1
|
+
{"version":3,"file":"localized-columns.d.ts","sourceRoot":"","sources":["../../src/localized-columns.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAA;AAEjE;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;AAQvE;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,gBACrB,MAAM,EAAE,gBACP,mBAAmB,GAAG,SAAS,KAC5C,kBAmBF,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,8BAA8B,aAAc,kBAAkB,KAAG,MA2D7E,CAAA"}
|
|
@@ -103,6 +103,19 @@ export var generateLocalizedColumnsHelper = function (families) {
|
|
|
103
103
|
" return '(' + clauses.join(' OR ') + ')'",
|
|
104
104
|
'}',
|
|
105
105
|
'',
|
|
106
|
+
'// A translatable list (tags) and its per-language copies, for a filter that',
|
|
107
|
+
'// matches a value in ANY language (a localized tag link resolves under every',
|
|
108
|
+
'// locale, like a localized slug). Null for a column with no copies.',
|
|
109
|
+
'function localizedColumnList(field) {',
|
|
110
|
+
' var copies = LOCALIZED_COLUMNS[field]',
|
|
111
|
+
' if (!copies) return null',
|
|
112
|
+
' var columns = [quoteLocalizedColumn(field)]',
|
|
113
|
+
' for (var locale in copies) {',
|
|
114
|
+
' if (Object.prototype.hasOwnProperty.call(copies, locale)) columns.push(quoteLocalizedColumn(copies[locale]))',
|
|
115
|
+
' }',
|
|
116
|
+
' return columns',
|
|
117
|
+
'}',
|
|
118
|
+
'',
|
|
106
119
|
"// ORDER BY a translatable column in the request's language, falling back to",
|
|
107
120
|
'// the base column for a row with no translation — the same resolution the',
|
|
108
121
|
'// row transform applies to what is displayed. Null when the field or the',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"localized-columns.js","sourceRoot":"","sources":["../../src/localized-columns.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAUH,oGAAoG;AACpG,IAAM,sBAAsB,GAAG,0BAA0B,CAAA;AAEzD,kFAAkF;AAClF,IAAM,mBAAmB,GAAG,UAAC,MAAc,EAAE,MAAc,IAAa,OAAA,UAAG,MAAM,cAAI,MAAM,CAAE,EAArB,CAAqB,CAAA;AAE7F;;;;GAIG;AACH,MAAM,CAAC,IAAM,uBAAuB,GAAG,UACrC,WAAqB,EACrB,YAA6C;IAE7C,IAAM,QAAQ,GAAuB,EAAE,CAAA;IACvC,IAAI,CAAC,YAAY,IAAI,YAAY,CAAC,gBAAgB,CAAC,MAAM,KAAK,CAAC,EAAE;QAC/D,OAAO,QAAQ,CAAA;KAChB;IACD,IAAM,OAAO,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,MAAM,CAAC,UAAC,IAAI,IAAK,OAAA,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAjC,CAAiC,CAAC,CAAC,CAAA;IACxF,OAAO,CAAC,OAAO,CAAC,UAAC,MAAM;QACrB,YAAY,CAAC,gBAAgB,CAAC,OAAO,CAAC,UAAC,MAAM;YAC3C,IAAM,IAAI,GAAG,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;YAChD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE;gBACtB,OAAM;aACP;YACD,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE;gBACrB,QAAQ,CAAC,MAAM,CAAC,GAAG,EAAE,CAAA;aACtB;YACD,QAAQ,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,GAAG,IAAI,CAAA;QACjC,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IACF,OAAO,QAAQ,CAAA;AACjB,CAAC,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,IAAM,8BAA8B,GAAG,UAAC,QAA4B;IACzE,IAAM,KAAK,GAAG;QACZ,oFAAoF;QACpF,8EAA8E;QAC9E,kCAA2B,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAE;QACrD,EAAE;QACF,yCAAyC;QACzC,+BAA+B;QAC/B,GAAG;QACH,EAAE;QACF,8EAA8E;QAC9E,4EAA4E;QAC5E,+EAA+E;QAC/E,sEAAsE;QACtE,yCAAyC;QACzC,wFAAwF;QACxF,+EAA+E;QAC/E,+DAA+D;QAC/D,yBAAyB;QACzB,gCAAgC;QAChC,4FAA4F;QAC5F,KAAK;QACL,sFAAsF;QACtF,oBAAoB;QACpB,8CAA8C;QAC9C,mBAAmB;QACnB,4BAA4B;QAC5B,oFAAoF;QACpF,4DAA4D;QAC5D,OAAO;QACP,KAAK;QACL,2CAA2C;QAC3C,GAAG;QACH,EAAE;QACF,8EAA8E;QAC9E,4EAA4E;QAC5E,2EAA2E;QAC3E,mDAAmD;QACnD,iDAAiD;QACjD,yCAAyC;QACzC,2EAA2E;QAC3E,0BAA0B;QAC1B,4GAA4G;QAC5G,GAAG;KACJ,CAAA;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACzB,CAAC,CAAA"}
|
|
1
|
+
{"version":3,"file":"localized-columns.js","sourceRoot":"","sources":["../../src/localized-columns.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAUH,oGAAoG;AACpG,IAAM,sBAAsB,GAAG,0BAA0B,CAAA;AAEzD,kFAAkF;AAClF,IAAM,mBAAmB,GAAG,UAAC,MAAc,EAAE,MAAc,IAAa,OAAA,UAAG,MAAM,cAAI,MAAM,CAAE,EAArB,CAAqB,CAAA;AAE7F;;;;GAIG;AACH,MAAM,CAAC,IAAM,uBAAuB,GAAG,UACrC,WAAqB,EACrB,YAA6C;IAE7C,IAAM,QAAQ,GAAuB,EAAE,CAAA;IACvC,IAAI,CAAC,YAAY,IAAI,YAAY,CAAC,gBAAgB,CAAC,MAAM,KAAK,CAAC,EAAE;QAC/D,OAAO,QAAQ,CAAA;KAChB;IACD,IAAM,OAAO,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,MAAM,CAAC,UAAC,IAAI,IAAK,OAAA,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAjC,CAAiC,CAAC,CAAC,CAAA;IACxF,OAAO,CAAC,OAAO,CAAC,UAAC,MAAM;QACrB,YAAY,CAAC,gBAAgB,CAAC,OAAO,CAAC,UAAC,MAAM;YAC3C,IAAM,IAAI,GAAG,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;YAChD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE;gBACtB,OAAM;aACP;YACD,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE;gBACrB,QAAQ,CAAC,MAAM,CAAC,GAAG,EAAE,CAAA;aACtB;YACD,QAAQ,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,GAAG,IAAI,CAAA;QACjC,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IACF,OAAO,QAAQ,CAAA;AACjB,CAAC,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,IAAM,8BAA8B,GAAG,UAAC,QAA4B;IACzE,IAAM,KAAK,GAAG;QACZ,oFAAoF;QACpF,8EAA8E;QAC9E,kCAA2B,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAE;QACrD,EAAE;QACF,yCAAyC;QACzC,+BAA+B;QAC/B,GAAG;QACH,EAAE;QACF,8EAA8E;QAC9E,4EAA4E;QAC5E,+EAA+E;QAC/E,sEAAsE;QACtE,yCAAyC;QACzC,wFAAwF;QACxF,+EAA+E;QAC/E,+DAA+D;QAC/D,yBAAyB;QACzB,gCAAgC;QAChC,4FAA4F;QAC5F,KAAK;QACL,sFAAsF;QACtF,oBAAoB;QACpB,8CAA8C;QAC9C,mBAAmB;QACnB,4BAA4B;QAC5B,oFAAoF;QACpF,4DAA4D;QAC5D,OAAO;QACP,KAAK;QACL,2CAA2C;QAC3C,GAAG;QACH,EAAE;QACF,8EAA8E;QAC9E,+EAA+E;QAC/E,sEAAsE;QACtE,uCAAuC;QACvC,yCAAyC;QACzC,4BAA4B;QAC5B,+CAA+C;QAC/C,gCAAgC;QAChC,kHAAkH;QAClH,KAAK;QACL,kBAAkB;QAClB,GAAG;QACH,EAAE;QACF,8EAA8E;QAC9E,4EAA4E;QAC5E,2EAA2E;QAC3E,mDAAmD;QACnD,iDAAiD;QACjD,yCAAyC;QACzC,2EAA2E;QAC3E,0BAA0B;QAC1B,4GAA4G;QAC5G,GAAG;KACJ,CAAA;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACzB,CAAC,CAAA"}
|