All files processors.js

100% Statements 31/31
100% Branches 20/20
100% Functions 10/10
100% Lines 29/29

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 1021x 1x   1x               1x         98x   78x 78x 78x   78x 1x           77x         29x                     1x     5x   4x   3x 3x       2x       2x 2x       4x           1x           1x             3x       2x 6x 12x             12x 12x    
const google = require('./adapters/google');
const utils = require('./utils');
 
const processors = exports;
 
// Simplifies manipulating deep object paths
/**
 * @template T
 * @param {T} object
 * @returns {Promise<T>}
 */
processors.processPath = async (
  /** @type {T} */ object,
  /** @type {string} */ path,
  /** @type {(value: any) => Promise<any>} */ processor,
) => {
  if (object == null) return object;
 
  const [firstKey, ...remainingKeys] = path.split('.');
  const isMapOperation = firstKey.endsWith('[]');
  const normalizedKey = firstKey.replace('[]', '');
 
  if (remainingKeys.length === 0) {
    return {
      ...object[normalizedKey],
      [normalizedKey]: await processor(object[normalizedKey]),
    };
  }
 
  return {
    ...object,
    [normalizedKey]: await (object[normalizedKey] && isMapOperation
      ? Promise.all(
          object[normalizedKey].map((value) =>
            processors.processPath(value, remainingKeys.join('.'), processor),
          ),
        )
      : processors.processPath(
          object[normalizedKey],
          remainingKeys.join('.'),
          processor,
        )),
  };
};
 
processors.processData = async (
  /** @type {Record<string, import('./types').Datasource>} */ datasources,
) => {
  if (!datasources) return {};
 
  const data = await Promise.all(
    Object.keys(datasources).map(async (key) => {
      const datasource = datasources[key];
      if (utils.isGoogleSheet(datasource.url)) {
        // NOTE: If we ever need to support multiple ranges we can use `spreadsheets.values.batchGet`
        // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/batchGet
        // We can build an object with sparse fields based on the longest of the resulting arrays
        const result = await google.sheets.spreadsheets.values.get({
          spreadsheetId: utils.getSheetIdByUrl(datasource.url),
          range: datasource.options.range,
        });
        const [columns, ...rowsOfCells] = result.data.values;
        return {
          [key]: rowsOfCells.map(
            toRowObject(
              columns.map(
                (column) => (datasource.options.columns || {})[column] || column,
              ),
            ),
          ),
        };
      } else {
        throw {
          response: {
            status: 400,
            body: `Unsupported datasource URL: ${datasource.url}`,
          },
          toString() {
            return this.response.body;
          },
        };
      }
    }),
  );
 
  return Object.assign({}, ...data); // Merges the array of objects into each other - `{ ...data }` is not the same!
};
 
function toRowObject(/** @type {string[]} */ columns) {
  return (/** @type {string[]} */ cells) => {
    return columns.reduce(
      (row, column, index) => ({ ...row, [column]: parseValue(cells[index]) }),
      {},
    );
  };
}
 
function parseValue(value) {
  const valueAsNumber = Number(value);
  return isNaN(valueAsNumber) || value === '' ? value : valueAsNumber;
}