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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
/**
 * 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;
}