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 };