Fetching contributors…
Cannot retrieve contributors at this time
233 lines (206 sloc) 8.33 KB

CSV Stringifier

Please look at the README, the samples and the tests for additional information.

stream = require 'stream'
util = require 'util'
get = require 'lodash.get'


This module export a function as its main entry point and return a transform stream.

Refers to the official prject documentation on how to call this function.

module.exports = ->
  if arguments.length is 3
    data = arguments[0]
    options = arguments[1]
    callback = arguments[2]
  else if arguments.length is 2
    if Array.isArray arguments[0]
    then data = arguments[0]
    else options = arguments[0]
    if typeof arguments[1] is 'function'
    then callback = arguments[1]
    else options = arguments[1]
  else if arguments.length is 1
    if typeof arguments[0] is 'function'
    then callback = arguments[0]
    else if Array.isArray arguments[0]
    then data = arguments[0]
    else options = arguments[0]
  options ?= {}
  stringifier = new Stringifier options
  if data
    process.nextTick ->
      stringifier.write d for d in data
  if callback
    chunks = []
    stringifier.on 'readable', ->
      while chunk =
        chunks.push chunk
    stringifier.on 'error', (err) ->
      callback err
    stringifier.on 'end', ->
      callback null, chunks.join ''

You can also use util.promisify native function (Node.js 8+) in order to wrap callbacks into promises for more convenient use when source is a readable stream and you are OK with storing entire result set in memory:

const { promisify } = require('util');
const csv = require('csv');
const stringifyAsync = promisify(csv.stringify);

//returns promise
function generateCsv(sourceData) {
    return stringifyAsync(sourceData);


Options are documented here.

Stringifier = (opts = {}) ->
  # Immutable options
  options = {}
  options[k] = v for k, v of opts @, options
  ## Default options
  @options = options
  @options.delimiter ?= ','
  @options.quote ?= '"'
  @options.quoted ?= false
  @options.quotedEmpty ?= undefined
  @options.quotedString ?= false
  @options.eof ?= true
  @options.escape ?= '"'
  @options.columns ?= null
  @options.header ?= false
  @options.formatters ?= {} ?= (value) ->
    # Cast date to timestamp string by default
    return '' + value.getTime()
  @options.formatters.bool ?= (value) ->
    # Cast boolean to string by default
    return if value then '1' else ''
  @options.formatters.object ?= (value) ->
    # Stringify object as JSON by default
    return JSON.stringify value
  @options.rowDelimiter ?= '\n'
  # Internal usage, state related
  @countWriten ?= 0
  switch @options.rowDelimiter
    when 'auto'
      @options.rowDelimiter = null
    when 'unix'
      @options.rowDelimiter = "\n"
    when 'mac'
      @options.rowDelimiter = "\r"
    when 'windows'
      @options.rowDelimiter = "\r\n"
    when 'ascii'
      @options.rowDelimiter = "\u001e"
    when 'unicode'
      @options.rowDelimiter = "\u2028"

util.inherits Stringifier, stream.Transform

module.exports.Stringifier = Stringifier


Print the header line if the option "header" is "true".

Stringifier.prototype.headers = ->
  return unless @options.header
  return unless @options.columns
  labels = @options.columns
  # If columns is an object, keys are fields and values are labels
  if typeof labels is 'object' then labels = for k, label of labels then label
  if @options.eof
    labels = @stringify(labels) + @options.rowDelimiter
    labels = @stringify(labels) @, labels

Stringifier.prototype.end = (chunk, encoding, callback)->
  @headers() if @countWriten is 0
  stream.Transform.prototype.end.apply @, arguments

Stringifier.prototype.write = (chunk, encoding, callback) ->
  return unless chunk?
  preserve = typeof chunk isnt 'object'
  # Emit and stringify the record
  unless preserve
    @options.columns ?= Object.keys chunk if @countWriten is 0 and not Array.isArray chunk
    try @emit 'record', chunk, @countWriten
    catch e then return @emit 'error', e
    # Convert the record into a string
    if @options.eof
      chunk = @stringify(chunk) + @options.rowDelimiter
      chunk = @stringify(chunk)
      chunk = @options.rowDelimiter + chunk if @options.header or @countWriten
  # Emit the csv
  chunk = "#{chunk}" if typeof chunk is 'number'
  @headers() if @countWriten is 0
  @countWriten++ unless preserve @, chunk, encoding, callback


Stringifier.prototype._transform = (chunk, encoding, callback) ->
  @push chunk


Convert a line to a string. Line may be an object, an array or a string.

Stringifier.prototype.stringify = (line) ->
  return line if typeof line isnt 'object'
  columns = @options.columns
  columns = Object.keys columns if typeof columns is 'object' and columns isnt null and not Array.isArray columns
  delimiter = @options.delimiter
  quote = @options.quote
  escape = @options.escape
  unless Array.isArray line
    _line = []
    if columns
      for i in [0...columns.length]
        column = columns[i]
        value = get line, column
        _line[i] = if (typeof value is 'undefined' or value is null) then '' else value
      for column of line
        _line.push line[column]
    line = _line
    _line = null
  else if columns # Note, we used to have @options.columns
    # We are getting an array but the user want specified output columns. In
    # this case, we respect the columns indexes
    line.splice columns.length
  if Array.isArray line
    newLine = ''
    for i in [0...line.length]
      field = line[i]
      if typeof field is 'string'
        # fine 99% of the cases, keep going
      else if typeof field is 'number'
        # Cast number to string
        field = '' + field
      else if typeof field is 'boolean'
        field = @options.formatters.bool(field)
      else if field instanceof Date
        field =
      else if typeof field is 'object' and field isnt null
        field = @options.formatters.object(field)
      if field
        return @emit 'error', Error 'Formatter must return a string, null or undefined' unless typeof field is 'string'
        containsdelimiter = field.indexOf(delimiter) >= 0
        containsQuote = (quote isnt '') and field.indexOf(quote) >= 0
        containsEscape = field.indexOf(escape) >= 0 and (escape isnt quote)
        containsRowDelimiter = field.indexOf(@options.rowDelimiter) >= 0
        shouldQuote = containsQuote or containsdelimiter or containsRowDelimiter or @options.quoted or (@options.quotedString and typeof line[i] is 'string')
        if shouldQuote and containsEscape
          regexp = if escape is '\\' then new RegExp(escape + escape, 'g') else new RegExp(escape, 'g');
          field = field.replace(regexp, escape + escape)
        if containsQuote
          regexp = new RegExp(quote,'g')
          field = field.replace(regexp, escape + quote)
        if shouldQuote
          field = quote + field + quote
        newLine += field
      else if @options.quotedEmpty or (not @options.quotedEmpty? and line[i] is '' and @options.quotedString)
        newLine += quote + quote
      if i isnt line.length - 1
        newLine += delimiter
    line = newLine