/** * Module dependencies. */ var os = require('os'); var lodashGet = require('lodash.get'); /** * Main function that converts json to csv * * @param {Object} params Function parameters containing data, fields, * delimiter (default is ','), hasCSVColumnTitle (default is true) * and default value (default is '') * @param {Function} callback(err, csv) - Callback function * if error, returning error in call back. * if csv is created successfully, returning csv output to callback. */ module.exports = function (params, callback) { if (!callback || typeof callback !== 'function') { throw new Error('Callback is required'); } checkParams(params, function (err) { if (err) { return callback(err); } var titles = createColumnTitles(params); var csv = createColumnContent(params, titles); callback(null, csv); }); }; /** * Check passing params * * @param {Object} params Function parameters containing data, fields, * delimiter, default value, mark quotes and hasCSVColumnTitle * @param {Function} callback Callback function returning error when invalid field is found */ function checkParams(params, callback) { params.data = params.data || []; // if data is an Object, not in array [{}], then just create 1 item array. // So from now all data in array of object format. if (!Array.isArray(params.data)) { params.data = [params.data]; } // Set params.fields default to first data element's keys if (!params.fields && (params.data.length === 0 || typeof params.data[0] !== 'object')) { return callback(new Error('params should include "fields" and/or non-empty "data" array of objects')); } params.fields = params.fields || Object.keys(params.data[0]); //#check fieldNames if (params.fieldNames && params.fieldNames.length !== params.fields.length) { return callback(new Error('fieldNames and fields should be of the same length, if fieldNames is provided.')); } // Get fieldNames from fields params.fieldNames = params.fields.map(function (field, i) { if (params.fieldNames && typeof field === 'string') { return params.fieldNames[i]; } return (typeof field === 'string') ? field : (field.label || field.value); }); //#check delimiter params.del = params.del || ','; //#check end of line character params.eol = params.eol || ''; //#check quotation mark params.quotes = typeof params.quotes === 'string' ? params.quotes : '"'; //#check default value params.defaultValue = params.defaultValue; //#check hasCSVColumnTitle, if it is not explicitly set to false then true. params.hasCSVColumnTitle = params.hasCSVColumnTitle !== false; callback(null); } /** * Create the title row with all the provided fields as column headings * * @param {Object} params Function parameters containing data, fields and delimiter * @returns {String} titles as a string */ function createColumnTitles(params) { var str = ''; //if CSV has column title, then create it if (params.hasCSVColumnTitle) { params.fieldNames.forEach(function (element) { if (str !== '') { str += params.del; } str += JSON.stringify(element).replace(/\"/g, params.quotes); }); } return str; } /** * Replace the quotation marks of the field element if needed (can be a not string-like item) * * @param {string} stringifiedElement The field element after JSON.stringify() * @param {string} quotes The params.quotes value. At this point we know that is not equal to double (") */ function replaceQuotationMarks(stringifiedElement, quotes) { var lastCharIndex = stringifiedElement.length - 1; //check if it's an string-like element if (stringifiedElement[0] === '"' && stringifiedElement[lastCharIndex] === '"') { //split the stringified field element because Strings are immutable var splitElement = stringifiedElement.split(''); //replace the quotation marks splitElement[0] = quotes; splitElement[lastCharIndex] = quotes; //join again stringifiedElement = splitElement.join(''); } return stringifiedElement; } /** * Create the content column by column and row by row below the title * * @param {Object} params Function parameters containing data, fields and delimiter * @param {String} str Title row as a string * @returns {String} csv string */ function createColumnContent(params, str) { params.data.forEach(function (dataElement) { //if null or empty object do nothing if (dataElement && Object.getOwnPropertyNames(dataElement).length > 0) { var line = ''; var eol = params.newLine || os.EOL || '\n'; params.fields.forEach(function (fieldElement) { var val; if (fieldElement && (typeof fieldElement === 'string' || typeof fieldElement.value === 'string')) { var path = (typeof fieldElement === 'string') ? fieldElement : fieldElement.value; var defaultValue = fieldElement.default || params.defaultValue; val = lodashGet(dataElement, path, defaultValue); if (val === null && defaultValue !== undefined){ val = defaultValue; } } else if (fieldElement && typeof fieldElement.value === 'function') { val = fieldElement.value(dataElement) || fieldElement.default; } if (val !== undefined) { var stringifiedElement = JSON.stringify(val); if (typeof val === 'object') stringifiedElement = JSON.stringify(stringifiedElement); if (params.quotes !== '"') { stringifiedElement = replaceQuotationMarks(stringifiedElement, params.quotes); } //JSON.stringify('\\') results in a string with two backslash //characters in it. I.e. '\\\\'. stringifiedElement = stringifiedElement.replace(/\\\\/g, '\\'); line += stringifiedElement; } line += params.del; }); //remove last delimeter line = line.substring(0, line.length - 1); //Replace single quotes with double quotes. Single quotes are preceeded by //a backslash. Be careful not to remove backslash content from the string. line = line.split('\\\\').map(function (portion) { return portion.replace(/\\"/g, Array(3).join(params.quotes)); }).join('\\\\'); //Remove the final excess backslashes from the stringified value. line = line.replace(/\\\\/g, '\\'); //If header exists, add it, otherwise, print only content if (str !== '') { str += eol + line + params.eol; } else { str = line + params.eol; } } }); return str; }