@teleporthq/teleport-plugin-next-data-source 0.44.12 → 0.44.14
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__/browser-row-policy.test.ts +6 -6
- package/__tests__/fetchers.test.ts +1 -1
- package/__tests__/items-path.test.ts +3 -2
- package/__tests__/related-items-transform.test.ts +17 -0
- package/__tests__/search-relevance.test.ts +199 -52
- package/dist/cjs/fetchers/csv-file.d.ts.map +1 -1
- package/dist/cjs/fetchers/csv-file.js +1 -1
- package/dist/cjs/fetchers/csv-file.js.map +1 -1
- package/dist/cjs/fetchers/google-sheets.d.ts.map +1 -1
- package/dist/cjs/fetchers/google-sheets.js +1 -1
- package/dist/cjs/fetchers/google-sheets.js.map +1 -1
- package/dist/cjs/fetchers/javascript.d.ts.map +1 -1
- package/dist/cjs/fetchers/javascript.js +1 -1
- package/dist/cjs/fetchers/javascript.js.map +1 -1
- package/dist/cjs/fetchers/mariadb.js +1 -1
- package/dist/cjs/fetchers/mariadb.js.map +1 -1
- package/dist/cjs/fetchers/mysql.js +1 -1
- package/dist/cjs/fetchers/mysql.js.map +1 -1
- package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
- package/dist/cjs/fetchers/postgresql.js +3 -3
- package/dist/cjs/fetchers/postgresql.js.map +1 -1
- package/dist/cjs/fetchers/redshift.js +1 -1
- package/dist/cjs/fetchers/rest-api.d.ts.map +1 -1
- package/dist/cjs/fetchers/rest-api.js +1 -1
- package/dist/cjs/fetchers/rest-api.js.map +1 -1
- package/dist/cjs/fetchers/supabase.js +2 -2
- package/dist/cjs/fetchers/supabase.js.map +1 -1
- package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
- package/dist/cjs/fetchers/teleport.js +4 -4
- package/dist/cjs/fetchers/teleport.js.map +1 -1
- package/dist/cjs/fetchers/turso.js +1 -1
- package/dist/cjs/fetchers/turso.js.map +1 -1
- package/dist/cjs/search-relevance.d.ts +67 -26
- package/dist/cjs/search-relevance.d.ts.map +1 -1
- package/dist/cjs/search-relevance.js +102 -29
- package/dist/cjs/search-relevance.js.map +1 -1
- package/dist/cjs/transformations/blog-post.d.ts.map +1 -1
- package/dist/cjs/transformations/blog-post.js +1 -1
- package/dist/cjs/transformations/blog-post.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/fetchers/csv-file.d.ts.map +1 -1
- package/dist/esm/fetchers/csv-file.js +1 -1
- package/dist/esm/fetchers/csv-file.js.map +1 -1
- package/dist/esm/fetchers/google-sheets.d.ts.map +1 -1
- package/dist/esm/fetchers/google-sheets.js +1 -1
- package/dist/esm/fetchers/google-sheets.js.map +1 -1
- package/dist/esm/fetchers/javascript.d.ts.map +1 -1
- package/dist/esm/fetchers/javascript.js +1 -1
- package/dist/esm/fetchers/javascript.js.map +1 -1
- package/dist/esm/fetchers/mariadb.js +2 -2
- package/dist/esm/fetchers/mariadb.js.map +1 -1
- package/dist/esm/fetchers/mysql.js +2 -2
- package/dist/esm/fetchers/mysql.js.map +1 -1
- package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
- package/dist/esm/fetchers/postgresql.js +4 -4
- package/dist/esm/fetchers/postgresql.js.map +1 -1
- package/dist/esm/fetchers/redshift.js +2 -2
- package/dist/esm/fetchers/rest-api.d.ts.map +1 -1
- package/dist/esm/fetchers/rest-api.js +1 -1
- package/dist/esm/fetchers/rest-api.js.map +1 -1
- package/dist/esm/fetchers/supabase.js +3 -3
- package/dist/esm/fetchers/supabase.js.map +1 -1
- package/dist/esm/fetchers/teleport.d.ts.map +1 -1
- package/dist/esm/fetchers/teleport.js +5 -5
- package/dist/esm/fetchers/teleport.js.map +1 -1
- package/dist/esm/fetchers/turso.js +2 -2
- package/dist/esm/fetchers/turso.js.map +1 -1
- package/dist/esm/search-relevance.d.ts +67 -26
- package/dist/esm/search-relevance.d.ts.map +1 -1
- package/dist/esm/search-relevance.js +100 -27
- package/dist/esm/search-relevance.js.map +1 -1
- package/dist/esm/transformations/blog-post.d.ts.map +1 -1
- package/dist/esm/transformations/blog-post.js +1 -1
- package/dist/esm/transformations/blog-post.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/fetchers/csv-file.ts +6 -11
- package/src/fetchers/google-sheets.ts +6 -12
- package/src/fetchers/javascript.ts +5 -12
- package/src/fetchers/mariadb.ts +2 -2
- package/src/fetchers/mysql.ts +2 -2
- package/src/fetchers/postgresql.ts +9 -4
- package/src/fetchers/redshift.ts +2 -2
- package/src/fetchers/rest-api.ts +4 -11
- package/src/fetchers/supabase.ts +4 -4
- package/src/fetchers/teleport.ts +9 -4
- package/src/fetchers/turso.ts +2 -2
- package/src/search-relevance.ts +195 -61
- package/src/transformations/blog-post.ts +5 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"teleport.js","sourceRoot":"","sources":["../../../src/fetchers/teleport.ts"],"names":[],"mappings":";;;AAAA,kCAMiB;AACjB,sDAK2B;AAC3B,4DAAmG;AACnG,sDAAwD;AACxD,0CAAgF;AAChF,kEAA4E;AAC5E,0DAA8F;AAC9F,oDAAwD;AACxD,iEAA+F;AAC/F,iEAAyE;AACzE,0DAAkE;AAClE,
|
|
1
|
+
{"version":3,"file":"teleport.js","sourceRoot":"","sources":["../../../src/fetchers/teleport.ts"],"names":[],"mappings":"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"}
|
|
@@ -20,7 +20,7 @@ var generateTursoFetcher = function (config, tableName) {
|
|
|
20
20
|
var tursoConfig = config;
|
|
21
21
|
var databaseUrl = tursoConfig.databaseUrl;
|
|
22
22
|
var token = tursoConfig.token;
|
|
23
|
-
return "import { createClient } from '@libsql/client'\n\n".concat((0, utils_1.generateSafeJSONParseCode)(), "\n\n").concat((0, utils_1.generateFilterTreeHelpersCode)(), "\n\n").concat((0, utils_1.generateSearchEscapeHelpersCode)(), "\n").concat((0, search_relevance_1.generateSearchSqlHelpersCode)(search_relevance_1.
|
|
23
|
+
return "import { createClient } from '@libsql/client'\n\n".concat((0, utils_1.generateSafeJSONParseCode)(), "\n\n").concat((0, utils_1.generateFilterTreeHelpersCode)(), "\n\n").concat((0, utils_1.generateSearchEscapeHelpersCode)(), "\n").concat((0, search_relevance_1.generateSearchSqlHelpersCode)(search_relevance_1.SQLITE_SEARCH_SQL), "\n\n").concat((0, utils_1.generateDateFormatterCode)(), "\n\nexport default async function handler(req, res) {\n let client = null\n try {\n client = createClient({\n url: ").concat(JSON.stringify(databaseUrl), ",\n authToken: ").concat((0, utils_1.replaceSecretReference)(token), "\n })\n \n const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset } = req.query\n \n let sql = `SELECT * FROM ").concat(tableName, "`\n const whereClauses = []\n const queryParams = []\n let searchQueryColumns = null\n let search = null\n \n if (query) {\n if (queryColumns) {\n const parsed = safeJSONParse(queryColumns)\n const columns = Array.isArray(parsed) ? parsed : [parsed]\n // Cast columns to TEXT and LOWER both sides so the match is\n // case-insensitive regardless of SQLite collation.\n search = buildSearchSql(query, columns)\n if (search) {\n whereClauses.push(search.where)\n queryParams.push(...search.params)\n }\n } else {\n // Store query for post-filtering if columns not specified\n searchQueryColumns = query\n }\n }\n \n // Helper to sanitize identifier (prevent SQL injection in column names)\n const sanitizeIdentifier = (name) => {\n // Only allow alphanumeric and underscore\n if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {\n throw new Error(`Invalid identifier: ${name}`)\n }\n return `\"${name}\"`\n }\n \n if (filters) {\n const filterTree = normalizeFilterTree(safeJSONParse(filters))\n \n if (filterTree) {\n const buildCondition = (condition) => {\n const field = sanitizeIdentifier(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 if (Array.isArray(value)) {\n if (value.length === 0) return null\n const placeholders = value.map(() => '?').join(', ')\n queryParams.push(...value)\n return operand === '!='\n ? `${field} NOT IN (${placeholders})`\n : `${field} IN (${placeholders})`\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 queryParams.push(value)\n return `${field} ${sqlOperator} ?`\n }\n \n const clause = buildFilterTreeClause(filterTree, buildCondition)\n if (clause) whereClauses.push(clause)\n }\n }\n \n if (whereClauses.length > 0) {\n sql += ` WHERE ${whereClauses.join(' AND ')}`\n }\n \n // Handle sorts - new array format. A search orders its matches by where\n // the words are first; the sort orders the rows within each of those groups.\n const orderClauses = search && search.rank ? [search.rank] : []\n if (sorts) {\n const parsedSorts = safeJSONParse(sorts)\n if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {\n orderClauses.push(...parsedSorts.map((sort) => {\n if (!sort.field) return null\n const order = (sort.order || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'\n return `${sanitizeIdentifier(sort.field)} ${order}`\n }).filter(Boolean))\n }\n } else if (sortBy) {\n const sortOrderValue = (sortOrder || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'\n orderClauses.push(`${sanitizeIdentifier(sortBy)} ${sortOrderValue}`)\n }\n if (orderClauses.length > 0) {\n sql += ` ORDER BY ${orderClauses.join(', ')}`\n if (search && search.rank) queryParams.push(...search.rankParams)\n }\n \n const limitValue = limit || perPage\n const offsetValue = offset !== undefined ? parseInt(offset) : (page && perPage ? (parseInt(page) - 1) * parseInt(perPage) : undefined)\n \n // Only apply SQL pagination if we're not doing post-filtering\n if (!searchQueryColumns) {\n if (limitValue) {\n sql += ` LIMIT ?`\n queryParams.push(parseInt(limitValue))\n }\n \n if (offsetValue !== undefined) {\n sql += ` OFFSET ?`\n queryParams.push(offsetValue)\n }\n }\n \n const result = await client.execute({\n sql,\n args: queryParams\n })\n \n let data = result.rows.map((row) => {\n const obj = {}\n result.columns.forEach((col, idx) => {\n obj[col] = row[col]\n })\n return obj\n })\n \n // Apply post-filtering for search without queryColumns\n if (searchQueryColumns) {\n const searchTerms = searchQueryColumns.toLowerCase().split(' ').filter(Boolean)\n data = data.filter((item) => {\n try {\n const stringified = JSON.stringify(item).toLowerCase()\n return searchTerms.every((term) => stringified.includes(term))\n } catch {\n return false\n }\n })\n \n // Apply pagination after filtering\n if (limitValue) {\n const start = offsetValue || 0\n data = data.slice(start, start + parseInt(limitValue))\n } else if (offsetValue) {\n data = data.slice(offsetValue)\n }\n }\n \n const safeData = JSON.parse(JSON.stringify(data, dateReplacer))\n \n return res.status(200).json({\n success: true,\n data: safeData,\n timestamp: Date.now()\n })\n } catch (error) {\n console.error('Turso fetch error:', error)\n return res.status(500).json({\n success: false,\n error: error.message || 'Failed to fetch data',\n timestamp: Date.now()\n })\n } finally {\n if (client) {\n try {\n await client.close()\n } catch (error) {\n console.error('Error closing Turso client:', error)\n }\n }\n }\n}\n");
|
|
24
24
|
};
|
|
25
25
|
exports.generateTursoFetcher = generateTursoFetcher;
|
|
26
26
|
//# sourceMappingURL=turso.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"turso.js","sourceRoot":"","sources":["../../../src/fetchers/turso.ts"],"names":[],"mappings":";;;AAAA,kCAMiB;AACjB,
|
|
1
|
+
{"version":3,"file":"turso.js","sourceRoot":"","sources":["../../../src/fetchers/turso.ts"],"names":[],"mappings":";;;AAAA,kCAMiB;AACjB,wDAAqF;AAE9E,IAAM,mBAAmB,GAAG,UACjC,MAA+B;IAE/B,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE;QACzC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,+BAA+B,EAAE,CAAA;KAClE;IAED,IAAI,CAAC,MAAM,CAAC,WAAW,IAAI,OAAO,MAAM,CAAC,WAAW,KAAK,QAAQ,EAAE;QACjE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,gCAAgC,EAAE,CAAA;KACnE;IAED,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,EAAE;QACrD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,wCAAwC,EAAE,CAAA;KAC3E;IAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;AAC1B,CAAC,CAAA;AAhBY,QAAA,mBAAmB,uBAgB/B;AASM,IAAM,oBAAoB,GAAG,UAClC,MAA+B,EAC/B,SAAiB;IAEjB,IAAM,WAAW,GAAG,MAAqB,CAAA;IACzC,IAAM,WAAW,GAAG,WAAW,CAAC,WAAW,CAAA;IAC3C,IAAM,KAAK,GAAG,WAAW,CAAC,KAAK,CAAA;IAE/B,OAAO,2DAEP,IAAA,iCAAyB,GAAE,iBAE3B,IAAA,qCAA6B,GAAE,iBAE/B,IAAA,uCAA+B,GAAE,eACjC,IAAA,+CAA4B,EAAC,oCAAiB,CAAC,iBAE/C,IAAA,iCAAyB,GAAE,0IAMhB,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,iCACrB,IAAA,8BAAsB,EAAC,KAAK,CAAC,gLAKhB,SAAS,spLAwKxC,CAAA;AACD,CAAC,CAAA;AAtMY,QAAA,oBAAoB,wBAsMhC"}
|
|
@@ -1,40 +1,81 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* How a list's search box narrows and orders the rows of a generated route.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* The typed text is split into terms at white space, letter case aside, and
|
|
5
|
+
* the punctuation around a term is dropped ("form?" looks for "form",
|
|
6
|
+
* "\"create" for "create"). Every term must appear in one of the searched
|
|
7
|
+
* columns, so "create form" finds "How to create a form".
|
|
8
|
+
*
|
|
9
|
+
* Where the stored text is read the same way as the typed text (the
|
|
10
|
+
* TeleportHQ database and other Postgres sources, and the routes that search
|
|
11
|
+
* rows in memory), punctuation inside a term counts as a space on both sides
|
|
12
|
+
* ("e-commerce" finds "E-Commerce", "what's" finds "what’s"), and HTML tags
|
|
13
|
+
* and entities count as spaces too, so a word that is only in a rich-text
|
|
14
|
+
* body's markup ("strong", "href", "amp") never matches.
|
|
15
|
+
*
|
|
16
|
+
* The matches are ordered column by column, in the order the list searches
|
|
17
|
+
* them: by how much of the typed text the first column (a title) holds, then
|
|
18
|
+
* the second (a summary), and so on. A term counts 3 as a whole word, 2 at
|
|
19
|
+
* the start of a word ("form" in "forms") and 1 inside one ("form" in
|
|
20
|
+
* "platform"). The list's own sort orders the rows that score alike.
|
|
11
21
|
*/
|
|
12
|
-
/**
|
|
13
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Characters a search reads as a space: punctuation, and line breaks and
|
|
24
|
+
* tabs, so a word at the end of a line is still a whole word.
|
|
25
|
+
*/
|
|
26
|
+
export declare const SEARCH_PUNCTUATION: string;
|
|
27
|
+
/**
|
|
28
|
+
* `searchTerms(query)`: the typed terms, lowercased, each once. `normalised`
|
|
29
|
+
* also reads the punctuation inside a term as a space, for the routes whose
|
|
30
|
+
* stored text is read the same way; otherwise only the punctuation around a
|
|
31
|
+
* term is dropped, so "e-commerce" still finds the raw text "e-commerce".
|
|
32
|
+
*/
|
|
33
|
+
export declare const generateSearchTermsCode: (normalised: boolean) => string;
|
|
34
|
+
/**
|
|
35
|
+
* How one SQL dialect reads a column for a search. `textSql` is the source
|
|
36
|
+
* of `(column) => sql`, the column's text as the search reads it: lower case,
|
|
37
|
+
* with a space at each end so a whole word and a word start can be told
|
|
38
|
+
* apart. `likeSql` is the source of `(text, placeholder, before, after) =>
|
|
39
|
+
* sql`, testing `text LIKE before + term + after` with the term bound to the
|
|
40
|
+
* placeholder. `normalised` dialects also read tags, entities and punctuation
|
|
41
|
+
* in the stored text as spaces; `lateral` ones work each column's text out
|
|
42
|
+
* once per row, in a LATERAL subquery the route joins after its table.
|
|
43
|
+
*/
|
|
44
|
+
export interface SearchSqlDialect {
|
|
45
|
+
textSql: string;
|
|
46
|
+
likeSql: string;
|
|
47
|
+
normalised: boolean;
|
|
48
|
+
lateral: boolean;
|
|
49
|
+
}
|
|
50
|
+
export declare const PG_SEARCH_SQL: SearchSqlDialect;
|
|
51
|
+
export declare const REDSHIFT_SEARCH_SQL: SearchSqlDialect;
|
|
52
|
+
export declare const MYSQL_SEARCH_SQL: SearchSqlDialect;
|
|
53
|
+
export declare const MARIADB_SEARCH_SQL: SearchSqlDialect;
|
|
54
|
+
export declare const SQLITE_SEARCH_SQL: SearchSqlDialect;
|
|
14
55
|
/**
|
|
15
56
|
* `buildSearchSql(query, columns, firstIndex)` for the SQL routes:
|
|
16
|
-
* `{ where, params, rank, rankParams, nextIndex }`, or null when there
|
|
17
|
-
* nothing to search. `
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
57
|
+
* `{ where, params, from, rank, rankParams, nextIndex }`, or null when there
|
|
58
|
+
* is nothing to search. `from` is the LATERAL join a `lateral` dialect's
|
|
59
|
+
* route appends after its table (selecting `table.*`, so the worked-out
|
|
60
|
+
* texts never reach a row), '' otherwise. `rank` is the ORDER BY keys, one
|
|
61
|
+
* per searched column, best first.
|
|
62
|
+
*
|
|
63
|
+
* With `firstIndex` the placeholders are numbered (`$n`) and each term is
|
|
64
|
+
* bound once, the rank reusing it; without it they are positional (`?`),
|
|
65
|
+
* every occurrence binds its own copy, and `rankParams` go after the
|
|
66
|
+
* parameters of the whole WHERE, as the ORDER BY comes after it.
|
|
23
67
|
*
|
|
24
68
|
* Uses `escapeLikePattern` and `sanitizeSearchIdentifier`
|
|
25
69
|
* (`generateSearchEscapeHelpersCode`).
|
|
26
70
|
*/
|
|
27
|
-
export declare const generateSearchSqlHelpersCode: (
|
|
71
|
+
export declare const generateSearchSqlHelpersCode: (dialect: SearchSqlDialect) => string;
|
|
28
72
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
73
|
+
* For the routes that search rows in memory: `matchesSearch(row, query,
|
|
74
|
+
* columns, valueOf)`, true when every term is in one of the columns, and
|
|
75
|
+
* `rankBySearch(rows, query, columns, valueOf)`, the same order as the SQL
|
|
76
|
+
* routes' rank, applied after the list's sort (a stable sort keeps it among
|
|
77
|
+
* rows that score alike) and before the page is cut. Stored text is read
|
|
78
|
+
* the normalised way: tags, entities and punctuation as spaces.
|
|
33
79
|
*/
|
|
34
80
|
export declare const generateSearchRankHelperCode: () => string;
|
|
35
|
-
/** Each dialect's case-insensitive substring match, for `generateSearchSqlHelpersCode`. */
|
|
36
|
-
export declare const PG_SEARCH_MATCH_SQL = "(column, placeholder) => '\"' + column + '\"::text ILIKE ' + placeholder + \" ESCAPE '|'\"";
|
|
37
|
-
export declare const MYSQL_SEARCH_MATCH_SQL = "(column, placeholder) => 'LOWER(CAST(' + mysql.escapeId(column) + ' AS CHAR)) LIKE LOWER(' + placeholder + \") ESCAPE '|'\"";
|
|
38
|
-
export declare const MARIADB_SEARCH_MATCH_SQL = "(column, placeholder) => 'LOWER(CAST(`' + column + '` AS CHAR)) LIKE LOWER(' + placeholder + \") ESCAPE '|'\"";
|
|
39
|
-
export declare const SQLITE_SEARCH_MATCH_SQL = "(column, placeholder) => 'LOWER(CAST(\"' + column + '\" AS TEXT)) LIKE LOWER(' + placeholder + \") ESCAPE '|'\"";
|
|
40
81
|
//# sourceMappingURL=search-relevance.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search-relevance.d.ts","sourceRoot":"","sources":["../../src/search-relevance.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"search-relevance.d.ts","sourceRoot":"","sources":["../../src/search-relevance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;GAGG;AACH,eAAO,MAAM,kBAAkB,QAE2E,CAAA;AAwB1G;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,eACtB,OAAO,KAClB,MAmBD,CAAA;AAEF;;;;;;;;;GASG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,OAAO,CAAA;IACnB,OAAO,EAAE,OAAO,CAAA;CACjB;AAID,eAAO,MAAM,aAAa,EAAE,gBAK3B,CAAA;AAED,eAAO,MAAM,mBAAmB,EAAE,gBAKjC,CAAA;AAID,eAAO,MAAM,gBAAgB,EAAE,gBAK9B,CAAA;AAED,eAAO,MAAM,kBAAkB,EAAE,gBAKhC,CAAA;AAED,eAAO,MAAM,iBAAiB,EAAE,gBAK/B,CAAA;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,4BAA4B,YAC9B,gBAAgB,KACxB,MAuCD,CAAA;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B,QAAO,MAyB9C,CAAA"}
|
|
@@ -2,45 +2,118 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* How a list's search box narrows and orders the rows of a generated route.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
5
|
+
* The typed text is split into terms at white space, letter case aside, and
|
|
6
|
+
* the punctuation around a term is dropped ("form?" looks for "form",
|
|
7
|
+
* "\"create" for "create"). Every term must appear in one of the searched
|
|
8
|
+
* columns, so "create form" finds "How to create a form".
|
|
9
|
+
*
|
|
10
|
+
* Where the stored text is read the same way as the typed text (the
|
|
11
|
+
* TeleportHQ database and other Postgres sources, and the routes that search
|
|
12
|
+
* rows in memory), punctuation inside a term counts as a space on both sides
|
|
13
|
+
* ("e-commerce" finds "E-Commerce", "what's" finds "what’s"), and HTML tags
|
|
14
|
+
* and entities count as spaces too, so a word that is only in a rich-text
|
|
15
|
+
* body's markup ("strong", "href", "amp") never matches.
|
|
16
|
+
*
|
|
17
|
+
* The matches are ordered column by column, in the order the list searches
|
|
18
|
+
* them: by how much of the typed text the first column (a title) holds, then
|
|
19
|
+
* the second (a summary), and so on. A term counts 3 as a whole word, 2 at
|
|
20
|
+
* the start of a word ("form" in "forms") and 1 inside one ("form" in
|
|
21
|
+
* "platform"). The list's own sort orders the rows that score alike.
|
|
12
22
|
*/
|
|
13
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
-
exports.
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
|
|
24
|
+
exports.generateSearchRankHelperCode = exports.generateSearchSqlHelpersCode = exports.SQLITE_SEARCH_SQL = exports.MARIADB_SEARCH_SQL = exports.MYSQL_SEARCH_SQL = exports.REDSHIFT_SEARCH_SQL = exports.PG_SEARCH_SQL = exports.generateSearchTermsCode = exports.SEARCH_PUNCTUATION = void 0;
|
|
25
|
+
/**
|
|
26
|
+
* Characters a search reads as a space: punctuation, and line breaks and
|
|
27
|
+
* tabs, so a word at the end of a line is still a whole word.
|
|
28
|
+
*/
|
|
29
|
+
exports.SEARCH_PUNCTUATION = '\t\n\v\f\r!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~\u00a0\u00a1\u00ab\u00b7\u00bb\u00bf' +
|
|
30
|
+
'\u2010\u2011\u2012\u2013\u2014\u2015\u2018\u2019\u201a\u201b\u201c\u201d\u201e\u2022\u2026\u2039\u203a';
|
|
31
|
+
/** A JavaScript string literal that survives being pasted into any generated file. */
|
|
32
|
+
var jsString = function (value) {
|
|
33
|
+
return JSON.stringify(value).replace(/[\u007f-\uffff`$]/g, function (char) { return '\\u' + ('0000' + char.charCodeAt(0).toString(16)).slice(-4); });
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* One Postgres regular expression, as a string literal, for everything a
|
|
37
|
+
* normalised search reads as a space: a tag, an entity, or one of
|
|
38
|
+
* SEARCH_PUNCTUATION. The bracket expression puts `]` first and `-` last,
|
|
39
|
+
* escapes the backslash, and puts `[` before `-` so it never opens a class.
|
|
40
|
+
* One pass: `translate()` is several times slower over long multibyte text.
|
|
41
|
+
*/
|
|
42
|
+
var PG_SPACES_REGEX_SQL = "'".concat(('<[^>]*>|&[a-z0-9#]+;|[]' +
|
|
43
|
+
exports.SEARCH_PUNCTUATION.split('')
|
|
44
|
+
.filter(function (char) { return !']-\\['.includes(char); })
|
|
45
|
+
.join('') +
|
|
46
|
+
'\\\\[-]').replace(/'/g, "''"), "'");
|
|
47
|
+
/**
|
|
48
|
+
* `searchTerms(query)`: the typed terms, lowercased, each once. `normalised`
|
|
49
|
+
* also reads the punctuation inside a term as a space, for the routes whose
|
|
50
|
+
* stored text is read the same way; otherwise only the punctuation around a
|
|
51
|
+
* term is dropped, so "e-commerce" still finds the raw text "e-commerce".
|
|
52
|
+
*/
|
|
53
|
+
var generateSearchTermsCode = function (normalised) { return "const SEARCH_PUNCTUATION = ".concat(jsString(exports.SEARCH_PUNCTUATION), "\nconst searchPunctuationToSpaces = (text) => {\n let out = ''\n for (const char of text) out += SEARCH_PUNCTUATION.indexOf(char) === -1 ? char : ' '\n return out\n}\nconst searchTerms = (query) => {\n const terms = []\n String(query == null ? '' : query).toLowerCase().split(/\\s+/).forEach((token) => {\n const spaced = searchPunctuationToSpaces(token)\n").concat(normalised
|
|
54
|
+
? " const term = spaced.trim().replace(/\\s+/g, ' ')"
|
|
55
|
+
: " const start = spaced.length - spaced.trimStart().length\n const term = token.slice(start, start + spaced.trim().length)", "\n if (term && terms.indexOf(term) === -1) terms.push(term)\n })\n return terms\n}"); };
|
|
56
|
+
exports.generateSearchTermsCode = generateSearchTermsCode;
|
|
57
|
+
var PG_LIKE_SQL = "(text, placeholder, before, after) => text + \" LIKE ('\" + before + \"' || \" + placeholder + \"::text || '\" + after + \"') ESCAPE '|'\"";
|
|
58
|
+
exports.PG_SEARCH_SQL = {
|
|
59
|
+
textSql: "(column) => \"(' ' || regexp_replace(lower(\\\"\" + column + \"\\\"::text), \" + SEARCH_SPACES_REGEX_SQL + \", ' ', 'g') || ' ')\"",
|
|
60
|
+
likeSql: PG_LIKE_SQL,
|
|
61
|
+
normalised: true,
|
|
62
|
+
lateral: true,
|
|
63
|
+
};
|
|
64
|
+
exports.REDSHIFT_SEARCH_SQL = {
|
|
65
|
+
textSql: "(column) => \"(' ' || lower(\\\"\" + column + \"\\\"::text) || ' ')\"",
|
|
66
|
+
likeSql: PG_LIKE_SQL,
|
|
67
|
+
normalised: false,
|
|
68
|
+
lateral: false,
|
|
69
|
+
};
|
|
70
|
+
var CONCAT_LIKE_SQL = "(text, placeholder, before, after) => text + \" LIKE CONCAT('\" + before + \"', \" + placeholder + \", '\" + after + \"') ESCAPE '|'\"";
|
|
71
|
+
exports.MYSQL_SEARCH_SQL = {
|
|
72
|
+
textSql: "(column) => \"CONCAT(' ', LOWER(CAST(\" + mysql.escapeId(column) + \" AS CHAR)), ' ')\"",
|
|
73
|
+
likeSql: CONCAT_LIKE_SQL,
|
|
74
|
+
normalised: false,
|
|
75
|
+
lateral: false,
|
|
76
|
+
};
|
|
77
|
+
exports.MARIADB_SEARCH_SQL = {
|
|
78
|
+
textSql: "(column) => \"CONCAT(' ', LOWER(CAST(\\`\" + column + \"\\` AS CHAR)), ' ')\"",
|
|
79
|
+
likeSql: CONCAT_LIKE_SQL,
|
|
80
|
+
normalised: false,
|
|
81
|
+
lateral: false,
|
|
82
|
+
};
|
|
83
|
+
exports.SQLITE_SEARCH_SQL = {
|
|
84
|
+
textSql: "(column) => \"(' ' || LOWER(CAST(\\\"\" + column + \"\\\" AS TEXT)) || ' ')\"",
|
|
85
|
+
likeSql: "(text, placeholder, before, after) => text + \" LIKE ('\" + before + \"' || \" + placeholder + \" || '\" + after + \"') ESCAPE '|'\"",
|
|
86
|
+
normalised: false,
|
|
87
|
+
lateral: false,
|
|
88
|
+
};
|
|
18
89
|
/**
|
|
19
90
|
* `buildSearchSql(query, columns, firstIndex)` for the SQL routes:
|
|
20
|
-
* `{ where, params, rank, rankParams, nextIndex }`, or null when there
|
|
21
|
-
* nothing to search. `
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
91
|
+
* `{ where, params, from, rank, rankParams, nextIndex }`, or null when there
|
|
92
|
+
* is nothing to search. `from` is the LATERAL join a `lateral` dialect's
|
|
93
|
+
* route appends after its table (selecting `table.*`, so the worked-out
|
|
94
|
+
* texts never reach a row), '' otherwise. `rank` is the ORDER BY keys, one
|
|
95
|
+
* per searched column, best first.
|
|
96
|
+
*
|
|
97
|
+
* With `firstIndex` the placeholders are numbered (`$n`) and each term is
|
|
98
|
+
* bound once, the rank reusing it; without it they are positional (`?`),
|
|
99
|
+
* every occurrence binds its own copy, and `rankParams` go after the
|
|
100
|
+
* parameters of the whole WHERE, as the ORDER BY comes after it.
|
|
27
101
|
*
|
|
28
102
|
* Uses `escapeLikePattern` and `sanitizeSearchIdentifier`
|
|
29
103
|
* (`generateSearchEscapeHelpersCode`).
|
|
30
104
|
*/
|
|
31
|
-
var generateSearchSqlHelpersCode = function (
|
|
105
|
+
var generateSearchSqlHelpersCode = function (dialect) { return "".concat((0, exports.generateSearchTermsCode)(dialect.normalised), "\n").concat(dialect.normalised ? "const SEARCH_SPACES_REGEX_SQL = ".concat(jsString(PG_SPACES_REGEX_SQL), "\n") : '', "const searchTextSql = ").concat(dialect.textSql, "\nconst searchLikeSql = ").concat(dialect.likeSql, "\nconst buildSearchSql = (query, columns, firstIndex) => {\n const terms = searchTerms(query)\n const searched = []\n ;(Array.isArray(columns) ? columns : []).forEach((column) => {\n const name = sanitizeSearchIdentifier(column)\n if (searched.indexOf(name) === -1) searched.push(name)\n })\n if (terms.length === 0 || searched.length === 0) return null\n const patterns = terms.map((term) => escapeLikePattern(term))\n const numbered = typeof firstIndex === 'number'\n const params = numbered ? patterns.slice() : []\n const rankParams = []\n const placeholder = (termIndex, bound) => {\n if (numbered) return '$' + (firstIndex + termIndex)\n bound.push(patterns[termIndex])\n return '?'\n }\n const texts = searched.map((column, position) => ").concat(dialect.lateral ? "'\"__tq_search\".\"__tq_search_' + position + '\"'" : 'searchTextSql(column)', ")\n const from = ").concat(dialect.lateral
|
|
106
|
+
? "' CROSS JOIN LATERAL (SELECT ' + searched.map((column, position) => searchTextSql(column) + ' AS \"__tq_search_' + position + '\"').join(', ') + ' OFFSET 0) AS \"__tq_search\"'"
|
|
107
|
+
: "''", "\n const where = '(' + patterns.map((pattern, termIndex) =>\n '(' + texts.map((text) => searchLikeSql(text, placeholder(termIndex, params), '%', '%')).join(' OR ') + ')'\n ).join(' AND ') + ')'\n const rank = texts.map((text) => '(' + patterns.map((pattern, termIndex) =>\n 'CASE WHEN ' + searchLikeSql(text, placeholder(termIndex, rankParams), '% ', ' %') + ' THEN 3' +\n ' WHEN ' + searchLikeSql(text, placeholder(termIndex, rankParams), '% ', '%') + ' THEN 2' +\n ' WHEN ' + searchLikeSql(text, placeholder(termIndex, rankParams), '%', '%') + ' THEN 1 ELSE 0 END'\n ).join(' + ') + ') DESC').join(', ')\n return { where, params, from, rank, rankParams, nextIndex: numbered ? firstIndex + patterns.length : firstIndex }\n}"); };
|
|
32
108
|
exports.generateSearchSqlHelpersCode = generateSearchSqlHelpersCode;
|
|
33
109
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
110
|
+
* For the routes that search rows in memory: `matchesSearch(row, query,
|
|
111
|
+
* columns, valueOf)`, true when every term is in one of the columns, and
|
|
112
|
+
* `rankBySearch(rows, query, columns, valueOf)`, the same order as the SQL
|
|
113
|
+
* routes' rank, applied after the list's sort (a stable sort keeps it among
|
|
114
|
+
* rows that score alike) and before the page is cut. Stored text is read
|
|
115
|
+
* the normalised way: tags, entities and punctuation as spaces.
|
|
38
116
|
*/
|
|
39
|
-
var generateSearchRankHelperCode = function () { return "".concat((0, exports.
|
|
117
|
+
var generateSearchRankHelperCode = function () { return "".concat((0, exports.generateSearchTermsCode)(true), "\nconst searchText = (value) => {\n const text = value === null || value === undefined ? '' : typeof value === 'object' ? JSON.stringify(value) : String(value)\n return ' ' + searchPunctuationToSpaces(text.toLowerCase().replace(/<[^>]*>|&[a-z0-9#]+;/g, ' ')) + ' '\n}\nconst searchScore = (text, terms) => terms.reduce((score, term) =>\n score + (text.indexOf(' ' + term + ' ') !== -1 ? 3 : text.indexOf(' ' + term) !== -1 ? 2 : text.indexOf(term) !== -1 ? 1 : 0), 0)\nconst matchesSearch = (row, query, columns, valueOf) => {\n const terms = searchTerms(query)\n if (terms.length === 0) return true\n const texts = (Array.isArray(columns) ? columns : []).map((column) => searchText(valueOf(row, column)))\n return terms.every((term) => texts.some((text) => text.indexOf(term) !== -1))\n}\nconst rankBySearch = (rows, query, columns, valueOf) => {\n const terms = searchTerms(query)\n if (!Array.isArray(columns) || columns.length === 0 || terms.length === 0) return rows\n return rows\n .map((row, index) => ({ row, index, scores: columns.map((column) => searchScore(searchText(valueOf(row, column)), terms)) }))\n .sort((a, b) => {\n for (let position = 0; position < a.scores.length; position++) {\n if (a.scores[position] !== b.scores[position]) return b.scores[position] - a.scores[position]\n }\n return a.index - b.index\n })\n .map((entry) => entry.row)\n}"); };
|
|
40
118
|
exports.generateSearchRankHelperCode = generateSearchRankHelperCode;
|
|
41
|
-
/** Each dialect's case-insensitive substring match, for `generateSearchSqlHelpersCode`. */
|
|
42
|
-
exports.PG_SEARCH_MATCH_SQL = "(column, placeholder) => '\"' + column + '\"::text ILIKE ' + placeholder + \" ESCAPE '|'\"";
|
|
43
|
-
exports.MYSQL_SEARCH_MATCH_SQL = "(column, placeholder) => 'LOWER(CAST(' + mysql.escapeId(column) + ' AS CHAR)) LIKE LOWER(' + placeholder + \") ESCAPE '|'\"";
|
|
44
|
-
exports.MARIADB_SEARCH_MATCH_SQL = "(column, placeholder) => 'LOWER(CAST(`' + column + '` AS CHAR)) LIKE LOWER(' + placeholder + \") ESCAPE '|'\"";
|
|
45
|
-
exports.SQLITE_SEARCH_MATCH_SQL = "(column, placeholder) => 'LOWER(CAST(\"' + column + '\" AS TEXT)) LIKE LOWER(' + placeholder + \") ESCAPE '|'\"";
|
|
46
119
|
//# sourceMappingURL=search-relevance.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search-relevance.js","sourceRoot":"","sources":["../../src/search-relevance.ts"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"search-relevance.js","sourceRoot":"","sources":["../../src/search-relevance.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blog-post.d.ts","sourceRoot":"","sources":["../../../src/transformations/blog-post.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AACvE,OAAO,EAAE,aAAa,EAA0B,MAAM,6BAA6B,CAAA;AAMnF,kFAAkF;AAClF,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,MAAM,CAAC,EAAE,aAAa,CAAC,aAAa,CAAA;IACpC,+EAA+E;IAC/E,UAAU,CAAC,EAAE,qBAAqB,EAAE,CAAA;IACpC,yEAAyE;IACzE,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,yGAAyG;IACzG,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED;;;;GAIG;AACH,eAAO,MAAM,kCAAkC,aACpC,wBAAwB,KAChC,
|
|
1
|
+
{"version":3,"file":"blog-post.d.ts","sourceRoot":"","sources":["../../../src/transformations/blog-post.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AACvE,OAAO,EAAE,aAAa,EAA0B,MAAM,6BAA6B,CAAA;AAMnF,kFAAkF;AAClF,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,MAAM,CAAC,EAAE,aAAa,CAAC,aAAa,CAAA;IACpC,+EAA+E;IAC/E,UAAU,CAAC,EAAE,qBAAqB,EAAE,CAAA;IACpC,yEAAyE;IACzE,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,yGAAyG;IACzG,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED;;;;GAIG;AACH,eAAO,MAAM,kCAAkC,aACpC,wBAAwB,KAChC,MA8SF,CAAA"}
|
|
@@ -15,7 +15,7 @@ var generateBlogPostTransformationCode = function (options) {
|
|
|
15
15
|
var _a, _b;
|
|
16
16
|
if (options === void 0) { options = {}; }
|
|
17
17
|
var tables = (_a = options.tables) !== null && _a !== void 0 ? _a : teleport_shared_1.ContentTables.contentTablesByKey('blog');
|
|
18
|
-
return "\n// ============================================================\n// Blog Post Transformation\n// ============================================================\n".concat((0, category_taxonomy_1.generateCategoryTaxonomyCode)('BLOG_CATEGORIES_BY_ID', options.categories), "\n").concat((0, blog_comments_1.generateBlogCommentsCode)((_b = tables.comments) !== null && _b !== void 0 ? _b : "".concat(tables.posts, "_comments")), "\n").concat((0, blog_adjacent_posts_1.generateAdjacentPostsCode)(tables.posts, options.arranged === true), "\n").concat((0, heading_anchors_1.generateHeadingAnchorsCode)(), "\n").concat(teleport_shared_1.RichTextContentCodegen.generateLegacyRichTextNormalizerCode(), "\nvar BLOG_HEADING_ANCHORS = ").concat(options.headingAnchors === true, "\nvar BLOG_CONTENTS = ").concat(options.contents === true, "\nvar BLOG_PICTURE_STAND_IN = ").concat(tables.pictureStandIn !== false, "\n\n// A post's reading time from its body, for one that stores none: its words at\n// about 200 a minute, at least one \u2014 what the editor's post form computes. A\n// card or post page otherwise printed \"min read\" with no number before it.\n// MUST mirror readingTimeOf in teleport-gui packages/renderer/src/utils/blog-posts.ts.\nfunction blogReadingTimeOf(html) {\n var text = String(html || '').replace(/<[^>]*>/g, ' ').replace(/\\s+/g, ' ').trim()\n return text ? Math.max(1, Math.ceil(text.split(' ').length / 200)) : null\n}\n\nfunction buildBlogPost(record, options) {\n if (!record || typeof record !== 'object') return record\n options = options || {}\n var currentLang = options.currentLanguage || null\n var mainLang = options.mainLanguage || null\n var assetMap = options.assetMap || {}\n\n var id = record.id !== undefined && record.id !== null ? record.id : null\n\n // i18n-resolved text fields\n var title = resolveI18nField(record, 'title', 'title', currentLang, mainLang) || ''\n var slug = resolveI18nField(record, 'slug', 'slug', currentLang, mainLang) || ''\n // A body stored before the rich-text contract (the studio's flat list lines,\n // the admin panel's no-break spaces) reads as stored HTML from here on.\n var content = normalizeLegacyRichTextHtml(\n resolveI18nField(record, 'content', 'content', currentLang, mainLang) || ''\n )\n // Counted before the heading anchors add their marks.\n var bodyReadingTime = blogReadingTimeOf(content)\n // The Contents list links to the headings, so they get their ids for it too\n // \u2014 the # links only with the heading anchors on. Its sections ride on a\n // post page's own fetch only (options.details): a listing draws no list, and\n // every card of it would otherwise carry one.\n var contentsHeadings = BLOG_CONTENTS && options.details === true ? [] : null\n if (BLOG_HEADING_ANCHORS || BLOG_CONTENTS) {\n content = addHeadingAnchors(content, { links: BLOG_HEADING_ANCHORS, sections: contentsHeadings })\n }\n var excerpt = resolveI18nField(record, 'excerpt', 'excerpt', currentLang, mainLang) || ''\n var category = resolveI18nField(record, 'category', 'category', currentLang, mainLang) || null\n\n // Assigned category ids -> resolved {id,name,slug} objects for the post-details\n // BREADCRUMBS (and anything else mapping over them). The taxonomy itself is not\n // in the DB (categories are authored in the GUI and live only in\n // blogSettings.categories); resolved against BLOG_CATEGORIES_BY_ID, baked in at\n // export time. Unknown/stale ids are silently dropped. `category` above stays\n // the denormalized PRIMARY name.\n var categories = resolveAssignedCategories(\n record.category_ids,\n BLOG_CATEGORIES_BY_ID,\n currentLang,\n mainLang,\n category\n )\n var metaTitle = resolveI18nField(record, 'meta_title', 'metaTitle', currentLang, mainLang) || null\n var metaDescription = resolveI18nField(record, 'meta_description', 'metaDescription', currentLang, mainLang) || null\n var featuredImageAlt = resolveI18nField(record, 'featured_image_alt', 'featuredImageAlt', currentLang, mainLang) || null\n var authorBio = resolveI18nField(record, 'author_bio', 'authorBio', currentLang, mainLang) || null\n\n // Status\n var status = record.status || 'draft'\n\n // Asset fields - resolve IDs to URLs\n var rawFeaturedImage = pickFirst(record.featured_image_url, record.featuredImageUrl)\n var TELEPORT_DEFAULT_FEATURED_IMG = 'https://play.teleporthq.io/static/svg/default-img.svg'\n var featuredImageUrl = resolveAssetUrl(rawFeaturedImage, assetMap)\n if (\n !featuredImageUrl ||\n rawFeaturedImage === TELEPORT_DEFAULT_FEATURED_IMG ||\n featuredImageUrl === TELEPORT_DEFAULT_FEATURED_IMG\n ) {\n // No picture of its own: the grey \"No image\" stand-in, or none at all for a\n // preset whose posts are text (ContentTables.pictureStandIn) \u2014 its card and\n // post page show the picture only when there is one.\n featuredImageUrl = BLOG_PICTURE_STAND_IN\n ? 'data:image/svg+xml;charset=utf-8,' +\n encodeURIComponent(\n '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"400\" height=\"240\" viewBox=\"0 0 400 240\"><rect fill=\"#e5e7eb\" width=\"400\" height=\"240\"/><text x=\"50%\" y=\"50%\" dominant-baseline=\"middle\" text-anchor=\"middle\" fill=\"#9ca3af\" font-family=\"system-ui,sans-serif\" font-size=\"13\">No image</text></svg>'\n )\n : null\n }\n\n var rawAuthorAvatar = pickFirst(record.author_avatar_url, record.authorAvatarUrl)\n var authorAvatarUrl = resolveAssetUrl(rawAuthorAvatar, assetMap)\n\n // Simple pass-through fields\n var authorName = pickFirst(record.author_name, record.authorName)\n var authorInitials = blogAuthorInitials(authorName)\n // The author's email never leaves the server: a visitor's API read hides it\n // (browser-row-policy.ts), and the page data a post or listing page ships\n // is built here, so it is not part of the post a page sees either.\n var readingTimeMinutes = safeNumber(pickFirst(record.reading_time_minutes, record.readingTimeMinutes), null)\n if (!(readingTimeMinutes > 0)) readingTimeMinutes = bodyReadingTime\n\n // Tags - i18n-resolved then parsed as JSON array\n var rawTags = resolveI18nField(record, 'tags', 'tags', currentLang, mainLang)\n var tags = parseJsonArray(rawTags)\n\n // Gallery images - parse JSON array then resolve each asset URL\n var rawGalleryImages = pickFirst(record.gallery_images, record.galleryImages)\n var parsedGalleryImages = parseJsonArray(rawGalleryImages)\n var galleryImages = resolveAssetUrls(parsedGalleryImages, assetMap)\n\n // Computed: allImages = [featuredImageUrl, ...galleryImages] (skip null featured)\n var allImages = []\n if (featuredImageUrl) allImages.push(featuredImageUrl)\n allImages = allImages.concat(galleryImages)\n\n // Boolean fields\n var isFeatured = coerceBoolean(pickFirst(record.is_featured, record.isFeatured), false)\n var allowComments = record.allow_comments !== undefined\n ? coerceBoolean(record.allow_comments, true)\n : record.allowComments !== undefined\n ? coerceBoolean(record.allowComments, true)\n : true\n\n // Per-post SEO overrides. The transform is the single normalization point \u2014\n // rows can also be written by the raw admin CRUD form, so whitespace and\n // unknown redirect types are neutralized here. Absent columns (a table\n // provisioned before the feature) read as undefined and normalize to null.\n // robotsContent is null \u2014 never '' or false \u2014 for unset rows, so the\n // page-level fallback (the `??` in the generated Head) can take over.\n var noIndex = coerceBoolean(pickFirst(record.no_index, record.noIndex), false)\n var canonicalUrl = normalizeSeoUrlField(pickFirst(record.canonical_url, record.canonicalUrl))\n var redirectUrl = normalizeSeoUrlField(pickFirst(record.redirect_url, record.redirectUrl))\n var rawRedirectType = pickFirst(record.redirect_type, record.redirectType)\n var redirectType =\n redirectUrl && (rawRedirectType === '301' || rawRedirectType === '302')\n ? rawRedirectType\n : null\n var robotsContent = noIndex ? 'noindex' : null\n\n // The author's related-post picks. `relatedPosts` carries the TRANSFORMED\n // rows, not ids, so the details page's related-posts rail can map over it and\n // draw an article card per entry; `relatedPostIds` keeps the raw selection.\n // The rows arrive from ONE batched query (getRelatedPostsMap) that\n // transformRecords runs, keyed by id in options.relatedPostsById.\n //\n // MUST mirror buildRelatedBlogPosts in packages/renderer/src/utils/blog-posts.ts\n // (the canvas-preview SSOT): the author's order, UNPUBLISHED posts dropped,\n // self dropped, and the nested build deliberately WITHOUT the map \u2014 two posts\n // pointing at each other is ordinary and would otherwise recurse forever.\n var relatedPostIds = parseRelatedIds(record.related_post_ids)\n var relatedPostsById = options.relatedPostsById\n var relatedPosts = []\n // The nested builds of the related and adjacent posts get NO maps: each is a\n // card, not a page, and two posts that point at each other would recurse.\n var relatedNestedOptions = {\n assetMap: assetMap,\n currentLanguage: currentLang,\n mainLanguage: mainLang,\n }\n if (relatedPostsById && relatedPostIds.length > 0) {\n for (var rp = 0; rp < relatedPostIds.length; rp++) {\n var relatedId = relatedPostIds[rp]\n if (id != null && relatedId === String(id)) continue\n var relatedRecord = relatedPostsById[relatedId]\n if (!relatedRecord) continue\n // A draft must not reach the storefront through another post's rail.\n // Missing status reads as 'draft', the same default as above.\n if ((relatedRecord.status || 'draft') !== 'published') continue\n relatedPosts.push(buildBlogPost(relatedRecord, relatedNestedOptions))\n }\n }\n\n // The previous (older) and next (newer) published post, as 0-or-1-item\n // lists: the post navigation maps over them like the related rail does, so an\n // end of the blog simply renders no card on that side.\n var adjacent = options.adjacentPostsById && id != null ? options.adjacentPostsById[String(id)] : null\n var previousPosts = adjacent && adjacent.previous ? [buildBlogPost(adjacent.previous, relatedNestedOptions)] : []\n var nextPosts = adjacent && adjacent.next ? [buildBlogPost(adjacent.next, relatedNestedOptions)] : []\n\n // The approved comments \u2014 the newest top-level ones, each with its replies \u2014\n // present only on a details fetch; see blog-comments.ts.\n var commentsEntry = options.commentsByPostId && id != null ? options.commentsByPostId[String(id)] : null\n var comments = commentsEntry ? buildBlogComments(commentsEntry.rows) : []\n var commentsCount = commentsEntry ? commentsEntry.count : 0\n\n // Timestamps\n var rawPublishedAt = pickFirst(record.published_at, record.publishedAt)\n var publishedAt = rawPublishedAt != null ? normalizeTimestamp(rawPublishedAt) : null\n\n var rawCreatedAt = pickFirst(record.created_at, record.createdAt, record.created)\n var createdAt = normalizeTimestamp(rawCreatedAt)\n\n var rawUpdatedAt = pickFirst(record.updated_at, record.updatedAt, record.updated)\n var updatedAt = rawUpdatedAt != null ? normalizeTimestamp(rawUpdatedAt) : createdAt\n\n // Computed: created = Unix seconds from createdAt milliseconds\n var created = Math.floor(createdAt / 1000)\n\n return {\n id: id,\n title: title,\n slug: slug,\n content: content,\n excerpt: excerpt,\n status: status,\n category: category,\n categories: categories,\n tags: tags,\n relatedPostIds: relatedPostIds,\n relatedPosts: relatedPosts,\n featuredImageUrl: featuredImageUrl,\n featuredImageAlt: featuredImageAlt,\n galleryImages: galleryImages,\n allImages: allImages,\n authorName: authorName,\n author_name: authorName,\n authorAvatarUrl: authorAvatarUrl,\n authorBio: authorBio,\n authorInitials: authorInitials,\n metaTitle: metaTitle,\n metaDescription: metaDescription,\n readingTimeMinutes: readingTimeMinutes,\n // The post's sections, what its page's Contents list links to \u2014 see\n // blogContentsSectionsOf; empty off a post page or under three sections.\n sections: contentsHeadings ? blogContentsSectionsOf(contentsHeadings) : [],\n isFeatured: isFeatured,\n allowComments: allowComments,\n comments: comments,\n commentsCount: commentsCount,\n previousPosts: previousPosts,\n nextPosts: nextPosts,\n noIndex: noIndex,\n canonicalUrl: canonicalUrl,\n redirectUrl: redirectUrl,\n redirectType: redirectType,\n robotsContent: robotsContent,\n publishedAt: publishedAt,\n createdAt: createdAt,\n updatedAt: updatedAt,\n created: created,\n // The post's place among its primary category's posts (Manual Order), or\n // null: what the category pages order by.\n sortOrder: safeNumber(pickFirst(record.sort_order, record.sortOrder), null),\n }\n}\n\nfunction transformBlogPosts(records, options) {\n if (!Array.isArray(records)) return []\n return records.map(function(record) { return buildBlogPost(record, options) })\n}\n\n// Batched fetch of the post rows a set of posts reference as \"related\", keyed by\n// id. Best-effort in the same way as the product side: a table provisioned\n// before the column yields no ids (the read is of an absent property, not of a\n// column \u2014 the SQL never names it), the map stays empty, and the rail stays\n// hidden behind its is_not_empty gate.\nasync function getRelatedPostsMap(getClientFn, records) {\n var map = {}\n if (!Array.isArray(records) || records.length === 0) return map\n\n var wantedIds = []\n for (var i = 0; i < records.length; i++) {\n var ids = parseRelatedIds(records[i] && records[i].related_post_ids)\n for (var j = 0; j < ids.length; j++) {\n if (wantedIds.indexOf(ids[j]) === -1) wantedIds.push(ids[j])\n }\n }\n if (wantedIds.length === 0) return map\n\n var client\n try {\n client = getClientFn()\n await client.connect()\n var result = await client.query('SELECT * FROM ").concat(tables.posts, " WHERE id = ANY($1)', [\n wantedIds,\n ])\n if (result && result.rows) {\n for (var r = 0; r < result.rows.length; r++) {\n var row = result.rows[r]\n if (row && row.id != null) map[String(row.id)] = row\n }\n }\n } catch (e) {\n // Leaves relatedPosts empty rather than failing the page.\n } finally {\n if (client) {\n try { await client.end() } catch (e) { /* ignore */ }\n }\n }\n return map\n}\n");
|
|
18
|
+
return "\n// ============================================================\n// Blog Post Transformation\n// ============================================================\n".concat((0, category_taxonomy_1.generateCategoryTaxonomyCode)('BLOG_CATEGORIES_BY_ID', options.categories), "\n").concat((0, blog_comments_1.generateBlogCommentsCode)((_b = tables.comments) !== null && _b !== void 0 ? _b : "".concat(tables.posts, "_comments")), "\n").concat((0, blog_adjacent_posts_1.generateAdjacentPostsCode)(tables.posts, options.arranged === true), "\n").concat((0, heading_anchors_1.generateHeadingAnchorsCode)(), "\n").concat(teleport_shared_1.RichTextContentCodegen.generateLegacyRichTextNormalizerCode(), "\nvar BLOG_HEADING_ANCHORS = ").concat(options.headingAnchors === true, "\nvar BLOG_CONTENTS = ").concat(options.contents === true, "\nvar BLOG_PICTURE_STAND_IN = ").concat(tables.pictureStandIn !== false, "\n\n// A post's reading time from its body, for one that stores none: its words at\n// about 200 a minute, at least one \u2014 what the editor's post form computes. A\n// card or post page otherwise printed \"min read\" with no number before it.\n// MUST mirror readingTimeOf in teleport-gui packages/renderer/src/utils/blog-posts.ts.\nfunction blogReadingTimeOf(html) {\n var text = String(html || '').replace(/<[^>]*>/g, ' ').replace(/\\s+/g, ' ').trim()\n return text ? Math.max(1, Math.ceil(text.split(' ').length / 200)) : null\n}\n\nfunction buildBlogPost(record, options) {\n if (!record || typeof record !== 'object') return record\n options = options || {}\n var currentLang = options.currentLanguage || null\n var mainLang = options.mainLanguage || null\n var assetMap = options.assetMap || {}\n\n var id = record.id !== undefined && record.id !== null ? record.id : null\n\n // i18n-resolved text fields\n var title = resolveI18nField(record, 'title', 'title', currentLang, mainLang) || ''\n var slug = resolveI18nField(record, 'slug', 'slug', currentLang, mainLang) || ''\n // A body stored before the rich-text contract (the studio's flat list lines,\n // the admin panel's no-break spaces) reads as stored HTML from here on.\n var content = normalizeLegacyRichTextHtml(\n resolveI18nField(record, 'content', 'content', currentLang, mainLang) || ''\n )\n // Counted before the heading anchors add their marks.\n var bodyReadingTime = blogReadingTimeOf(content)\n // The Contents list links to the headings, so they get their ids for it too\n // \u2014 the # links only with the heading anchors on. Its sections ride on a\n // post page's own fetch only (options.details): a listing draws no list, and\n // every card of it would otherwise carry one.\n var contentsHeadings = BLOG_CONTENTS && options.details === true ? [] : null\n if (BLOG_HEADING_ANCHORS || BLOG_CONTENTS) {\n content = addHeadingAnchors(content, { links: BLOG_HEADING_ANCHORS, sections: contentsHeadings })\n }\n var excerpt = resolveI18nField(record, 'excerpt', 'excerpt', currentLang, mainLang) || ''\n var category = resolveI18nField(record, 'category', 'category', currentLang, mainLang) || null\n\n // Assigned category ids -> resolved {id,name,slug} objects for the post-details\n // BREADCRUMBS (and anything else mapping over them). The taxonomy itself is not\n // in the DB (categories are authored in the GUI and live only in\n // blogSettings.categories); resolved against BLOG_CATEGORIES_BY_ID, baked in at\n // export time. Unknown/stale ids are silently dropped. `category` above stays\n // the denormalized PRIMARY name.\n var categories = resolveAssignedCategories(\n record.category_ids,\n BLOG_CATEGORIES_BY_ID,\n currentLang,\n mainLang,\n category\n )\n var metaTitle = resolveI18nField(record, 'meta_title', 'metaTitle', currentLang, mainLang) || null\n var metaDescription = resolveI18nField(record, 'meta_description', 'metaDescription', currentLang, mainLang) || null\n var featuredImageAlt = resolveI18nField(record, 'featured_image_alt', 'featuredImageAlt', currentLang, mainLang) || null\n var authorBio = resolveI18nField(record, 'author_bio', 'authorBio', currentLang, mainLang) || null\n\n // Status\n var status = record.status || 'draft'\n\n // Asset fields - resolve IDs to URLs\n var rawFeaturedImage = pickFirst(record.featured_image_url, record.featuredImageUrl)\n var TELEPORT_DEFAULT_FEATURED_IMG = 'https://play.teleporthq.io/static/svg/default-img.svg'\n var featuredImageUrl = resolveAssetUrl(rawFeaturedImage, assetMap)\n if (\n !featuredImageUrl ||\n rawFeaturedImage === TELEPORT_DEFAULT_FEATURED_IMG ||\n featuredImageUrl === TELEPORT_DEFAULT_FEATURED_IMG\n ) {\n // No picture of its own: the grey \"No image\" stand-in, or none at all for a\n // preset whose posts are text (ContentTables.pictureStandIn) \u2014 its card and\n // post page show the picture only when there is one.\n featuredImageUrl = BLOG_PICTURE_STAND_IN\n ? 'data:image/svg+xml;charset=utf-8,' +\n encodeURIComponent(\n '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"400\" height=\"240\" viewBox=\"0 0 400 240\"><rect fill=\"#e5e7eb\" width=\"400\" height=\"240\"/><text x=\"50%\" y=\"50%\" dominant-baseline=\"middle\" text-anchor=\"middle\" fill=\"#9ca3af\" font-family=\"system-ui,sans-serif\" font-size=\"13\">No image</text></svg>'\n )\n : null\n }\n\n var rawAuthorAvatar = pickFirst(record.author_avatar_url, record.authorAvatarUrl)\n var authorAvatarUrl = resolveAssetUrl(rawAuthorAvatar, assetMap)\n\n // Simple pass-through fields\n var authorName = pickFirst(record.author_name, record.authorName)\n var authorInitials = blogAuthorInitials(authorName)\n // The author's email never leaves the server: a visitor's API read hides it\n // (browser-row-policy.ts), and the page data a post or listing page ships\n // is built here, so it is not part of the post a page sees either.\n var readingTimeMinutes = safeNumber(pickFirst(record.reading_time_minutes, record.readingTimeMinutes), null)\n if (!(readingTimeMinutes > 0)) readingTimeMinutes = bodyReadingTime\n\n // Tags - i18n-resolved then parsed as JSON array\n var rawTags = resolveI18nField(record, 'tags', 'tags', currentLang, mainLang)\n var tags = parseJsonArray(rawTags)\n\n // Gallery images - parse JSON array then resolve each asset URL\n var rawGalleryImages = pickFirst(record.gallery_images, record.galleryImages)\n var parsedGalleryImages = parseJsonArray(rawGalleryImages)\n var galleryImages = resolveAssetUrls(parsedGalleryImages, assetMap)\n\n // Computed: allImages = [featuredImageUrl, ...galleryImages] (skip null featured)\n var allImages = []\n if (featuredImageUrl) allImages.push(featuredImageUrl)\n allImages = allImages.concat(galleryImages)\n\n // Boolean fields\n var isFeatured = coerceBoolean(pickFirst(record.is_featured, record.isFeatured), false)\n var allowComments = record.allow_comments !== undefined\n ? coerceBoolean(record.allow_comments, true)\n : record.allowComments !== undefined\n ? coerceBoolean(record.allowComments, true)\n : true\n\n // Per-post SEO overrides. The transform is the single normalization point \u2014\n // rows can also be written by the raw admin CRUD form, so whitespace and\n // unknown redirect types are neutralized here. Absent columns (a table\n // provisioned before the feature) read as undefined and normalize to null.\n // robotsContent is null \u2014 never '' or false \u2014 for unset rows, so the\n // page-level fallback (the `??` in the generated Head) can take over.\n var noIndex = coerceBoolean(pickFirst(record.no_index, record.noIndex), false)\n var canonicalUrl = normalizeSeoUrlField(pickFirst(record.canonical_url, record.canonicalUrl))\n var redirectUrl = normalizeSeoUrlField(pickFirst(record.redirect_url, record.redirectUrl))\n var rawRedirectType = pickFirst(record.redirect_type, record.redirectType)\n var redirectType =\n redirectUrl && (rawRedirectType === '301' || rawRedirectType === '302')\n ? rawRedirectType\n : null\n var robotsContent = noIndex ? 'noindex' : null\n\n // The author's related-post picks. `relatedPosts` carries the TRANSFORMED\n // rows, not ids, so the details page's related-posts rail can map over it and\n // draw an article card per entry; `relatedPostIds` keeps the raw selection.\n // The rows arrive from ONE batched query (getRelatedPostsMap) that\n // transformRecords runs, keyed by id in options.relatedPostsById.\n //\n // MUST mirror buildRelatedBlogPosts in packages/renderer/src/utils/blog-posts.ts\n // (the canvas-preview SSOT): the author's order, UNPUBLISHED posts dropped,\n // self dropped, and the nested build deliberately WITHOUT the map \u2014 two posts\n // pointing at each other is ordinary and would otherwise recurse forever.\n var relatedPostIds = parseRelatedIds(record.related_post_ids)\n var relatedPostsById = options.relatedPostsById\n var relatedPosts = []\n // The nested builds of the related and adjacent posts get NO maps: each is a\n // card, not a page, and two posts that point at each other would recurse.\n var relatedNestedOptions = {\n assetMap: assetMap,\n currentLanguage: currentLang,\n mainLanguage: mainLang,\n card: true,\n }\n if (relatedPostsById && relatedPostIds.length > 0) {\n for (var rp = 0; rp < relatedPostIds.length; rp++) {\n var relatedId = relatedPostIds[rp]\n if (id != null && relatedId === String(id)) continue\n var relatedRecord = relatedPostsById[relatedId]\n if (!relatedRecord) continue\n // A draft must not reach the storefront through another post's rail.\n // Missing status reads as 'draft', the same default as above.\n if ((relatedRecord.status || 'draft') !== 'published') continue\n relatedPosts.push(buildBlogPost(relatedRecord, relatedNestedOptions))\n }\n }\n\n // The previous (older) and next (newer) published post, as 0-or-1-item\n // lists: the post navigation maps over them like the related rail does, so an\n // end of the blog simply renders no card on that side.\n var adjacent = options.adjacentPostsById && id != null ? options.adjacentPostsById[String(id)] : null\n var previousPosts = adjacent && adjacent.previous ? [buildBlogPost(adjacent.previous, relatedNestedOptions)] : []\n var nextPosts = adjacent && adjacent.next ? [buildBlogPost(adjacent.next, relatedNestedOptions)] : []\n\n // The approved comments \u2014 the newest top-level ones, each with its replies \u2014\n // present only on a details fetch; see blog-comments.ts.\n var commentsEntry = options.commentsByPostId && id != null ? options.commentsByPostId[String(id)] : null\n var comments = commentsEntry ? buildBlogComments(commentsEntry.rows) : []\n var commentsCount = commentsEntry ? commentsEntry.count : 0\n\n // Timestamps\n var rawPublishedAt = pickFirst(record.published_at, record.publishedAt)\n var publishedAt = rawPublishedAt != null ? normalizeTimestamp(rawPublishedAt) : null\n\n var rawCreatedAt = pickFirst(record.created_at, record.createdAt, record.created)\n var createdAt = normalizeTimestamp(rawCreatedAt)\n\n var rawUpdatedAt = pickFirst(record.updated_at, record.updatedAt, record.updated)\n var updatedAt = rawUpdatedAt != null ? normalizeTimestamp(rawUpdatedAt) : createdAt\n\n // Computed: created = Unix seconds from createdAt milliseconds\n var created = Math.floor(createdAt / 1000)\n\n return {\n id: id,\n title: title,\n slug: slug,\n // A card of another post carries no body: no card shows one, and six\n // related cards' bodies were most of an article page's data. MUST mirror\n // `card` in packages/renderer/src/utils/blog-posts.ts (teleport-gui).\n content: options.card === true ? '' : content,\n excerpt: excerpt,\n status: status,\n category: category,\n categories: categories,\n tags: tags,\n relatedPostIds: relatedPostIds,\n relatedPosts: relatedPosts,\n featuredImageUrl: featuredImageUrl,\n featuredImageAlt: featuredImageAlt,\n galleryImages: galleryImages,\n allImages: allImages,\n authorName: authorName,\n author_name: authorName,\n authorAvatarUrl: authorAvatarUrl,\n authorBio: authorBio,\n authorInitials: authorInitials,\n metaTitle: metaTitle,\n metaDescription: metaDescription,\n readingTimeMinutes: readingTimeMinutes,\n // The post's sections, what its page's Contents list links to \u2014 see\n // blogContentsSectionsOf; empty off a post page or under three sections.\n sections: contentsHeadings ? blogContentsSectionsOf(contentsHeadings) : [],\n isFeatured: isFeatured,\n allowComments: allowComments,\n comments: comments,\n commentsCount: commentsCount,\n previousPosts: previousPosts,\n nextPosts: nextPosts,\n noIndex: noIndex,\n canonicalUrl: canonicalUrl,\n redirectUrl: redirectUrl,\n redirectType: redirectType,\n robotsContent: robotsContent,\n publishedAt: publishedAt,\n createdAt: createdAt,\n updatedAt: updatedAt,\n created: created,\n // The post's place among its primary category's posts (Manual Order), or\n // null: what the category pages order by.\n sortOrder: safeNumber(pickFirst(record.sort_order, record.sortOrder), null),\n }\n}\n\nfunction transformBlogPosts(records, options) {\n if (!Array.isArray(records)) return []\n return records.map(function(record) { return buildBlogPost(record, options) })\n}\n\n// Batched fetch of the post rows a set of posts reference as \"related\", keyed by\n// id. Best-effort in the same way as the product side: a table provisioned\n// before the column yields no ids (the read is of an absent property, not of a\n// column \u2014 the SQL never names it), the map stays empty, and the rail stays\n// hidden behind its is_not_empty gate.\nasync function getRelatedPostsMap(getClientFn, records) {\n var map = {}\n if (!Array.isArray(records) || records.length === 0) return map\n\n var wantedIds = []\n for (var i = 0; i < records.length; i++) {\n var ids = parseRelatedIds(records[i] && records[i].related_post_ids)\n for (var j = 0; j < ids.length; j++) {\n if (wantedIds.indexOf(ids[j]) === -1) wantedIds.push(ids[j])\n }\n }\n if (wantedIds.length === 0) return map\n\n var client\n try {\n client = getClientFn()\n await client.connect()\n var result = await client.query('SELECT * FROM ").concat(tables.posts, " WHERE id = ANY($1)', [\n wantedIds,\n ])\n if (result && result.rows) {\n for (var r = 0; r < result.rows.length; r++) {\n var row = result.rows[r]\n if (row && row.id != null) map[String(row.id)] = row\n }\n }\n } catch (e) {\n // Leaves relatedPosts empty rather than failing the page.\n } finally {\n if (client) {\n try { await client.end() } catch (e) { /* ignore */ }\n }\n }\n return map\n}\n");
|
|
19
19
|
};
|
|
20
20
|
exports.generateBlogPostTransformationCode = generateBlogPostTransformationCode;
|
|
21
21
|
//# sourceMappingURL=blog-post.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blog-post.js","sourceRoot":"","sources":["../../../src/transformations/blog-post.ts"],"names":[],"mappings":";;;AACA,+DAAmF;AACnF,yDAAkE;AAClE,6DAAiE;AACjE,iDAA0D;AAC1D,qDAA8D;AAuB9D;;;;GAIG;AACI,IAAM,kCAAkC,GAAG,UAChD,OAAsC;;IAAtC,wBAAA,EAAA,YAAsC;IAEtC,IAAM,MAAM,GAAG,MAAA,OAAO,CAAC,MAAM,mCAAI,+BAAa,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAA;IACzE,OAAO,2KAIP,IAAA,gDAA4B,EAAC,uBAAuB,EAAE,OAAO,CAAC,UAAU,CAAC,eACzE,IAAA,wCAAwB,EAAC,MAAA,MAAM,CAAC,QAAQ,mCAAI,UAAG,MAAM,CAAC,KAAK,cAAW,CAAC,eACvE,IAAA,+CAAyB,EAAC,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,QAAQ,KAAK,IAAI,CAAC,eAClE,IAAA,4CAA0B,GAAE,eAC5B,wCAAsB,CAAC,oCAAoC,EAAE,0CAClC,OAAO,CAAC,cAAc,KAAK,IAAI,mCACtC,OAAO,CAAC,QAAQ,KAAK,IAAI,2CACjB,MAAM,CAAC,cAAc,KAAK,KAAK,
|
|
1
|
+
{"version":3,"file":"blog-post.js","sourceRoot":"","sources":["../../../src/transformations/blog-post.ts"],"names":[],"mappings":";;;AACA,+DAAmF;AACnF,yDAAkE;AAClE,6DAAiE;AACjE,iDAA0D;AAC1D,qDAA8D;AAuB9D;;;;GAIG;AACI,IAAM,kCAAkC,GAAG,UAChD,OAAsC;;IAAtC,wBAAA,EAAA,YAAsC;IAEtC,IAAM,MAAM,GAAG,MAAA,OAAO,CAAC,MAAM,mCAAI,+BAAa,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAA;IACzE,OAAO,2KAIP,IAAA,gDAA4B,EAAC,uBAAuB,EAAE,OAAO,CAAC,UAAU,CAAC,eACzE,IAAA,wCAAwB,EAAC,MAAA,MAAM,CAAC,QAAQ,mCAAI,UAAG,MAAM,CAAC,KAAK,cAAW,CAAC,eACvE,IAAA,+CAAyB,EAAC,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,QAAQ,KAAK,IAAI,CAAC,eAClE,IAAA,4CAA0B,GAAE,eAC5B,wCAAsB,CAAC,oCAAoC,EAAE,0CAClC,OAAO,CAAC,cAAc,KAAK,IAAI,mCACtC,OAAO,CAAC,QAAQ,KAAK,IAAI,2CACjB,MAAM,CAAC,cAAc,KAAK,KAAK,gvaA8QR,MAAM,CAAC,KAAK,6cAkBhE,CAAA;AACD,CAAC,CAAA;AAhTY,QAAA,kCAAkC,sCAgT9C"}
|