github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/ui/dashboard/src/utils/template.ts (about) 1 import { 2 KeyValuePairs, 3 RowRenderResult, 4 TemplatesMap, 5 } from "../components/dashboards/common/types"; 6 7 const replaceSingleQuotesWithDoubleQuotes = (str) => { 8 if (!str) { 9 return str; 10 } 11 return str.replaceAll("'", '"'); 12 }; 13 14 export const buildJQFilter = (template) => { 15 if (!template) { 16 return template; 17 } 18 19 const templateParts = template.split(/({{.*?}})/gs).filter((p) => p); 20 const newTemplateParts: string[] = []; 21 // Iterate over each template part - we want to distinguish between regular strings and 22 // interpolated strings - we'll treat them differently. 23 for (const templatePart of templateParts) { 24 const interpolatedMatch = /{{(.*?)}}/gs.exec(templatePart); 25 // If it's a plain string, quote it 26 if (!interpolatedMatch) { 27 newTemplateParts.push(JSON.stringify(templatePart)); 28 } else { 29 // If it's an interpolated string, replace the double curly braces with single parentheses 30 // to frame this particular jq sub-expression 31 let newInterpolatedTemplate = templatePart; 32 newInterpolatedTemplate = 33 "(" + newInterpolatedTemplate.substring(interpolatedMatch.index + 2); 34 newInterpolatedTemplate = 35 newInterpolatedTemplate.substring(0, interpolatedMatch[0].length - 3) + 36 ")"; 37 38 // Replace any single quotes with jq-compatible double quotes 39 const doubleQuotedString = replaceSingleQuotesWithDoubleQuotes( 40 newInterpolatedTemplate 41 ); 42 43 newTemplateParts.push(doubleQuotedString); 44 } 45 } 46 47 // Join all field parts into an array and then use jq 48 // to join then into the final filter. 49 // This ensures that types are coerced 50 // e.g. (5 + " hello") would result in an error, 51 // whereas ([5, " hello"] | join("")) would give "5 hello" 52 return `([${newTemplateParts.join(", ")}] | join(""))`; 53 }; 54 55 const buildCombinedJQFilter = (templates: TemplatesMap) => { 56 const filters: TemplatesMap = {}; 57 // Iterate over all the template fields 58 for (const [field, template] of Object.entries(templates)) { 59 filters[field] = buildJQFilter(template); 60 } 61 62 // Now we want to include all fields with their filter - we're going to map the output to 63 // an object with a key per field and the value is the rendered template 64 const allFieldsFilter = Object.entries(filters) 65 .map(([field, filter]) => `"${field}": ${filter}`) 66 .join(", "); 67 68 // Finally, build the overall filter - which will iterate over all passed in rows of data, then turn 69 // the result into an array of the object returned by the combined field filters 70 return `[ .[] | { ${allFieldsFilter} }]`; 71 }; 72 73 const renderInterpolatedTemplates = async ( 74 templates: TemplatesMap, 75 data: KeyValuePairs[], 76 jq: any 77 ): Promise<RowRenderResult[]> => { 78 try { 79 const finalFilter = buildCombinedJQFilter(templates); 80 const results = await jq.json(data, finalFilter); 81 return results.map((result) => { 82 const mapped = {}; 83 Object.entries(result).forEach(([field, rendered]) => { 84 mapped[field] = { 85 result: rendered, 86 }; 87 }); 88 return mapped; 89 }); 90 } catch (err) { 91 const interpolatedMatcher = /{{(.*?)}}/gs; 92 const testRow = data[0]; 93 const fieldResults: RowRenderResult = {}; 94 for (const [field, template] of Object.entries(templates)) { 95 let updatedTemplate = template; 96 try { 97 let match; 98 while ((match = interpolatedMatcher.exec(template)) !== null) { 99 // This is necessary to avoid infinite loops with zero-width matches 100 if (match.index === interpolatedMatcher.lastIndex) { 101 interpolatedMatcher.lastIndex++; 102 } 103 104 const templatePart = match[1]; 105 106 // Replace any single quotes with jq-compatible double quotes 107 const doubleQuotedString = 108 replaceSingleQuotesWithDoubleQuotes(templatePart); 109 110 const rendered = await jq.json(testRow, doubleQuotedString); 111 112 updatedTemplate = updatedTemplate.replace(match[0], rendered); 113 } 114 } catch (err) { 115 // @ts-ignore 116 fieldResults[field] = { error: err.stack }; 117 } 118 } 119 const errorResult: RowRenderResult[] = []; 120 for (let i = 0; i < data.length; i++) { 121 errorResult.push(fieldResults); 122 } 123 return errorResult; 124 } 125 }; 126 127 export { renderInterpolatedTemplates };