json-csv
Version:
Easily convert JSON array to CSV in Node.JS via buffered or streaming.
146 lines (129 loc) • 4.19 kB
JavaScript
const { Transform, Readable } = require('stream')
const StringWriter = require('./string-writer')
class CsvExporter {
constructor(options = {}) {
this.options = { ...options } // shallow
if (options.fieldSeparator == null) this.options.fieldSeparator = ','
if (options.ignoreHeader == null) this.options.ignoreHeader = false
if (options.buffered == null) this.options.buffered = true
if (options.fields == null) this.options.fields = []
if (options.encoding == null) this.options.encoding = 'utf8'
}
// alias
buffered(data) {
return new Promise((resolve, reject) => {
let writer = new StringWriter({ defaultEncoding: 'utf8' })
let readable = Readable.from(data.map(d => d === null ? {} : d))
.pipe(this.stream())
readable.on('error', reject)
readable.pipe(writer)
.on('finish', () => {
resolve(writer.data)
})
})
}
stream() {
let writtenHeader = false
let { ignoreHeader, fields, encoding } = this.options
// eslint-disable-next-line @typescript-eslint/no-this-alias
let self = this
let transformer = new Transform({
writableObjectMode: true,
transform(chunk, _encoding, callback) {
// debug(`incoming chunk: ${require('util').inspect(chunk)}`)
// debug(encoding)
if (!writtenHeader && !ignoreHeader) {
writtenHeader = true
let header = self.getHeaderRow(fields)
// debug(`writing header ${header}`)
this.push(header)
}
let row = self.getBodyRow(chunk, fields)
// debug(`writing row ${require("util").inspect(row)}`)
this.push(row)
callback()
}
})
transformer.setEncoding(encoding)
return transformer
}
prepValue(arg, forceQuoted) {
var quoted = forceQuoted || arg.indexOf('"') >= 0 || arg.indexOf(this.options.fieldSeparator) >= 0 || arg.indexOf('\n') >= 0
var result = arg.replace(/"/g, '""')
if (quoted) { result = '"' + result + '"' }
return result
}
getHeaderRow(fields) {
let fieldKeys = Object.keys(fields)
var header = fieldKeys.reduce((line, fieldKey) => {
let field = fields[fieldKey]
var label = field.label || field.name
if (line === 'START') {
line = ''
} else {
line += this.options.fieldSeparator
}
line += this.prepValue(label)
return line
}, 'START')
header += '\r\n'
return header
}
getBodyRow(data, fields) {
let reducer = (line, field) => {
if (line === 'START') {
line = ''
} else {
line += this.options.fieldSeparator
}
var val = this.getValue(data, field.name)
// vinicioslc support to OR || operator allowing multiples names to the same column
// the code will use the last non null and non empty value
if (field.name.includes('||')) {
// by default column is empty
val = ''
let fields = field.name.split('||')
// for each alternative
fields.forEach((field) => {
// get value and associate
let fieldVal = this.getValue(data, field)
// remove whitespaces and check if non null before assign
if (val != null && fieldVal.trim().length > 0 && fieldVal.trim() !== '') {
val = fieldVal
}
// do this for every field
})
}
if (typeof field.transform === 'function') {
val = field.transform(val)
} else if (typeof field.filter === 'function') {
val = field.filter(val)
}
if (typeof val !== 'undefined' && val !== null) {
var quoted = typeof field.quoted !== 'undefined' && field.quoted
line += this.prepValue(val.toString(), quoted)
}
return line
}
var row = fields.reduce(reducer, 'START')
row += '\r\n'
return row
}
getValue(data, arg) {
var args = arg.split('.')
if (args.length > 0) return this.getValueIx(data, args, 0)
return ''
}
getValueIx(data, args, ix) {
if (!data) { return '' }
// for filtered fields using the whole row as a source.
// `this` is a keyword here; hoping not to conflict with existing fields.
if (args[0] === 'this') { return data }
var val = data[args[ix]]
if (typeof val === 'undefined') { return '' }
// walk the dot-notation recursively to get the remaining values.
if ((args.length - 1) > ix) { return this.getValueIx(val, args, ix + 1) }
return val
}
}
module.exports = CsvExporter