All files / builtins export-csv-raw.ts

100% Statements 58/58
100% Branches 12/12
100% Functions 14/14
100% Lines 57/57

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 1572x 2x   2x       2x 2x   2x         1x             180x 90x 90x             1x           36x   36x 18x 348x     18x 306x               1x             216x   216x 36x     180x           1x       38x 38x   38x 216x 216x       38x             1x 90x 90x   90x           1x         2x 2x   2x 6x   6x 126x 126x   6x     2x           1x 2x 2x   1x             1x 3x 1x     2x     2x 2x 90x   2x   2x     1x    
import * as fs from 'fs/promises';
import {ERRORS} from '@grnsft/if-core/utils';
 
import {STRINGS} from '../config';
 
import {Context} from '../types/manifest';
 
const {ExhaustOutputArgError, WriteFileError} = ERRORS;
const {OUTPUT_REQUIRED, WRITE_CSV_ERROR, EXPORTING_RAW_CSV_FILE} = STRINGS;
 
export const ExportCSVRaw = () => {
  /**
   * handle a tree leaf, where there are no child nodes, by adding it as key->value pair to the flat map
   * and capturing key as a header
   */
  const handleLeafValue = (
    value: any,
    fullPath: string,
    key: any,
    flatMap: {[key: string]: any},
    headers: Set<string>
  ) => {
    if (fullPath.includes('outputs')) {
      headers.add(key);
      flatMap[fullPath] = value;
    }
  };
 
  /**
   * handle a tree node, recursively traverse the children and append their results to the flat map and captured headers
   */
  const handleNodeValue = (
    value: any,
    fullPath: string,
    flatMap: Record<string, any>,
    headers: Set<string>
  ) => {
    const [subFlatMap, subHeaders] = extractFlatMapAndHeaders(value, fullPath);
 
    if (Object.keys(subFlatMap).length > 0) {
      Object.entries(subFlatMap).forEach(([subKey, value]) => {
        flatMap[subKey] = value;
      });
 
      subHeaders.forEach(subHeader => {
        headers.add(subHeader);
      });
    }
  };
 
  /**
   * Handles a key at the top level of the tree
   */
  const handleKey = (
    value: any,
    key: any,
    prefix: string,
    flatMap: Record<string, any>,
    headers: Set<string>
  ) => {
    const fullPath = prefix ? `${prefix}.${key}` : key;
 
    if (value !== null && typeof value === 'object') {
      return handleNodeValue(value, fullPath, flatMap, headers);
    }
 
    return handleLeafValue(value, fullPath, key, flatMap, headers);
  };
 
  /**
   * Recursively extract a flat map and headers from the hierarcial tree.
   */
  const extractFlatMapAndHeaders = (
    tree: any,
    prefix = ''
  ): [Record<string, any>, Set<string>] => {
    const headers: Set<string> = new Set();
    const flatMap: Record<string, any> = [];
 
    for (const key in tree) {
      if (key in tree) {
        handleKey(tree[key], key, prefix, flatMap, headers);
      }
    }
 
    return [flatMap, headers];
  };
 
  /**
   * extract the id of the key, that is removing the last token (which is the index).
   * in this manner, multiple keys that identical besides their index share the same id.
   */
  const extractIdHelper = (key: string): string => {
    const parts = key.split('.');
    parts.pop();
 
    return parts.join('.');
  };
 
  /**
   * generate a CSV formatted string based on a flat key->value map, headers and ids
   */
  const getCsvString = (
    map: {[key: string]: any},
    headers: Set<string>,
    ids: Set<string>
  ): string => {
    const csvRows: string[] = [];
    csvRows.push(['id', ...headers].join(','));
 
    ids.forEach(id => {
      const rowData = [id];
 
      headers.forEach(header => {
        const value = map[`${id}.${header}`] ?? '';
        rowData.push(value.toString());
      });
      csvRows.push(rowData.join(','));
    });
 
    return csvRows.join('\n');
  };
 
  /**
   * write the given string content to a file at the provided path
   */
  const writeOutputFile = async (content: string, outputPath: string) => {
    try {
      await fs.writeFile(`${outputPath}.csv`, content);
    } catch (error) {
      throw new WriteFileError(WRITE_CSV_ERROR(outputPath, error));
    }
  };
 
  /**
   * export the provided tree content to a CSV file, represented in a flat structure
   */
  const execute = async (tree: any, _context: Context, outputPath: string) => {
    if (!outputPath) {
      throw new ExhaustOutputArgError(OUTPUT_REQUIRED);
    }
 
    console.debug(EXPORTING_RAW_CSV_FILE(outputPath));
 
    const [extractredFlatMap, extractedHeaders] =
      extractFlatMapAndHeaders(tree);
    const ids = new Set(
      Object.keys(extractredFlatMap).map(key => extractIdHelper(key))
    );
    const csvString = getCsvString(extractredFlatMap, extractedHeaders, ids);
 
    await writeOutputFile(csvString, outputPath);
  };
 
  return {execute};
};