diff --git a/History.md b/History.md index b14783b..3966157 100644 --- a/History.md +++ b/History.md @@ -1,38 +1,46 @@ +1.0.0 / 2015-09-18 +================== + + * Fixed tests + * Added support for a header row + * Better handling of data types + * Clean up + -0.6.0 / 2013-03-14 +0.6.0 / 2013-03-14 ================== * Added support for Node 0.10 * Added Node 0.10 to travis -0.5.0 / 2012-11-01 +0.5.0 / 2012-11-01 ================== * Support serializing an array of objects * Added travis.yml -0.4.1 / 2012-06-26 +0.4.1 / 2012-06-26 ================== * Added support for Node 0.8.0 -0.4.0 / 2012-06-17 +0.4.0 / 2012-06-17 ================== * Avoid extending `http.ServerResponse.prototype` with Express 3.x -0.3.1 / 2012-04-20 +0.3.1 / 2012-04-20 ================== * Fixed broken csv output when use `preventCast` option. -0.3.0 / 2012-03-28 +0.3.0 / 2012-03-28 ================== * Added option `ignoreNullOrUndefined`. * Added test. -0.2.1 / 2012-03-28 +0.2.1 / 2012-03-28 ================== * Adding clause setting undefined columns to an empty string. [mblackshaw] @@ -40,27 +48,27 @@ * Added test. * Added benchmark. -0.2.0 / 2012-03-21 +0.2.0 / 2012-03-21 ================== * Add option `preventCast`. -0.1.1 / 2012-03-18 +0.1.1 / 2012-03-18 ================= * Fixed `repository` in package.json -0.1.0 / 2012-03-18 +0.1.0 / 2012-03-18 ================== * Added test -0.0.2 / 2012-03-17 +0.0.2 / 2012-03-17 ================== * Add option `separator`. Default is `,`. -0.0.1 / 2012-03-17 +0.0.1 / 2012-03-17 ================== * Initial release. diff --git a/Readme.md b/Readme.md index 69fabf2..069b28b 100644 --- a/Readme.md +++ b/Readme.md @@ -1,31 +1,49 @@ -[![build status](https://secure.travis-ci.org/nulltask/express-csv.png)](http://travis-ci.org/nulltask/express-csv) -# Express CSV +# csv-express -``` - _____ ____ ______ __ -| ____|_ ___ __ _ __ ___ ___ ___ / ___/ ___\ \ / / -| _| \ \/ / '_ \| '__/ _ \/ __/ __| | | \___ \\ \ / / -| |___ > <| |_) | | | __/\__ \__ \ | |___ ___) |\ V / -|_____/_/\_\ .__/|_| \___||___/___/ \____|____/ \_/ - |_| -``` +A CSV response module for [Express](http://expressjs.com/). This is an up-to-date fork of [express-csv](https://github.com/nulltask/express-csv) that merges outstanding pull requests. + + +## Changes from [express-csv](https://github.com/nulltask/express-csv) ++ Adds support for adding a header row to the CSV ([#16](https://github.com/nulltask/express-csv/pull/16)) ++ Better escaping of numbers so that applications such as Excel properly recognize data types ++ Actively maintained for use with more current versions of Express ++ Up to date dependencies and tests - Express CSV provides response CSV easily to [Express](http://expressjs.com/). ## Installation -npm: +```` + npm install csv-express +```` + +## API +### Methods + +#### res.csv([data] [, csvHeaders] [, responseHeaders] [, statusCode]) - $ npm install express-csv ++ data (**required**) is an array of arrays or objects ++ csvHeaders (*optional*) is a Boolean for returning headers on the output CSV file. Default is false. ++ responseHeaders (*optional*) are custom response headers ++ statusCode (*optional*) is a custom response status code + +### Settings +#### #separator +The delimiter to use, default is `','`. + +#### #preventCast +Prevent Excels type casting, default is `false`. + +#### #ignoreNullOrUndefined +Treat `null` and `undefined` values as empty strings in the output, default is `true`. ## Usage Example: -```js -var express = require('express') - , csv = require('express-csv') - , app = module.exports = express.createServer(); +```` +var express = require('express'), + csv = require('csv-express'), + app = express(); app.get('/', function(req, res) { res.csv([ @@ -35,41 +53,26 @@ app.get('/', function(req, res) { }); app.listen(3000); -``` - -Response: - -``` -$ curl --verbose http://127.0.0.1:3000/ -* About to connect() to 127.0.0.1 port 3000 (#0) -* Trying 127.0.0.1... connected -* Connected to 127.0.0.1 (127.0.0.1) port 3000 (#0) -> GET / HTTP/1.1 -> User-Agent: curl/7.21.4 (universal-apple-darwin11.0) libcurl/7.21.4 OpenSSL/0.9.8r zlib/1.2.5 -> Host: 127.0.0.1:3000 -> Accept: */* -> -< HTTP/1.1 200 OK -< X-Powered-By: Express -< Content-Type: text/csv; charset=utf-8 -< Content-Length: 26 -< Connection: keep-alive -< -"a","b","c" -"d","e","f" -* Connection #0 to host 127.0.0.1 left intact -* Closing connection #0 -``` - -Alternatively, you can also pass an array of objects to be serialized, in which case the object's -properties will be iterated over. E.g.: - -```js -res.csv([ { name: "joe", id: 1 }] -//=> "joe", 1 -``` +```` + +You can also pass an array of objects and optionally return a header row. +Useful when working with the results from database queries using [node-mysql](https://github.com/felixge/node-mysql/) or [node-postgres](https://github.com/brianc/node-postgres). + +```` + res.csv([ + {"name": "Sam", "age": 1}, + {"name": "Mary": "age": 2} + ], true); + + => name, age + Sam, 1 + Mary, 2 +```` + + ## License +The original license is as follows: The MIT License @@ -92,4 +95,7 @@ res.csv([ { name: "joe", id: 1 }] IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +All modifications from the original are CC0. + diff --git a/index.js b/index.js index 64c9e46..a739c3e 100644 --- a/index.js +++ b/index.js @@ -1 +1 @@ -module.exports = require('./lib/express-csv'); +module.exports = require('./lib/csv-express'); diff --git a/lib/express-csv.js b/lib/csv-express.js similarity index 52% rename from lib/express-csv.js rename to lib/csv-express.js index fcc1d69..9acd35f 100644 --- a/lib/express-csv.js +++ b/lib/csv-express.js @@ -1,48 +1,23 @@ -/*! - * express-csv - * Copyright 2011 Seiya Konno - * MIT Licensed - */ - -/** - * Module dependencies. - */ +/* + csv-express + Forked and modified by John J Czaplewski -var http = require('http') - , express = require('express') - , res = express.response || http.ServerResponse.prototype; - -/** - * Import package information. + Copyright 2011 Seiya Konno + MIT Licensed */ -var package = require('../package'); - -/** - * Library version. - */ -exports.version = package.version; +'use strict'; -/** - * CSV separator - */ +var res = require('http').ServerResponse.prototype; +var iconv = require('iconv-lite'); +// Configurable settings exports.separator = ','; - -/** - * Prevent Excel's casting. - */ - exports.preventCast = false; - -/** - * Ignore `null` or `undefined` - */ - exports.ignoreNullOrUndefined = true; -/** +/* * Escape CSV field * * @param {Mixed} field @@ -57,6 +32,9 @@ function escape(field) { if (exports.preventCast) { return '="' + String(field).replace(/\"/g, '""') + '"'; } + if (!isNaN(parseFloat(field)) && isFinite(field)) { + return parseFloat(field); + } return '"' + String(field).replace(/\"/g, '""') + '"'; } @@ -82,27 +60,42 @@ function objToArray(obj) { return result; } -/** - * Send CSV response with `obj`, optional `headers`, and optional `status`. - * - * @param {Array} obj - * @param {Object|Number} headers or status - * @param {Number} status - * @return {ServerResponse} - * @api public + + /* + Send CSV response + + {data} - Array objects or arrays + {csvHeaders} - If true uses the keys of the objects in {obj} to set a header row + {headers} - Optional response headers + {stats} - Optional status code */ -res.csv = function(obj, headers, status) { +res.csv = function(data, csvHeaders, headers, status) { var body = ''; this.charset = this.charset || 'utf-8'; this.header('Content-Type', 'text/csv'); - obj.forEach(function(item) { - if (!(item instanceof Array)) item = objToArray(item); + if (csvHeaders) { + var header = []; + for (var prop in data[0]) { + if (data[0].hasOwnProperty(prop)) { + header.push(prop); + } + } + body += header + '\r\n'; + } + + data.forEach(function(item) { + if (!(item instanceof Array)) { + item = objToArray(item); + } body += item.map(escape).join(exports.separator) + '\r\n'; }); - return this.send(body, headers, status); -}; + if (this.charset !== 'utf-8') { + body = iconv.encode(body, this.charset); + } + return this.send(body, headers, status); +} diff --git a/package.json b/package.json index 8f395d2..02ef1da 100644 --- a/package.json +++ b/package.json @@ -1,26 +1,47 @@ { - "name": "express-csv" -, "description": "express-csv provides response csv easily to express." -, "version": "0.6.0" -, "author": "Seiya Konno " -, "contributors": [ - { "name": "Matthew Blackshaw", "web": "http://mattblackshaw.com" } - , { "name": "Craig McDonald", "web": "http://thrackle.com" } - ] -, "devDependencies": { - "mocha": "*" - , "should": "*" - , "superagent": "*" - , "express": "2" - } -, "keywords": ["express", "csv"] -, "repository": "git://github.com/nulltask/express-csv.git" -, "main": "index" -, "engines": { - "node": "0.6 || 0.8 || 0.10" - } -, "scripts": { - "test": "mocha" - , "prepublish": "make test" + "name": "csv-express", + "description": "Adds a CSV response method to expressjs applications.", + "version": "1.1.0", + "author": "John J Czaplewski (forked from Seiya Konno)", + "contributors": [ + { + "name": "Seiya Konno", + "web": "nulltask@gmail.com" + }, + { + "name": "Matthew Blackshaw", + "web": "http://mattblackshaw.com" + }, + { + "name": "Craig McDonald", + "web": "http://thrackle.com" + }, + { + "name": "Alexey Avramchik", + "web": "https://github.com/alexcrack" + } + ], + "devDependencies": { + "express": "4.x", + "mocha": "^2.3.2", + "should": "^7.1.0", + "superagent": "^1.4.0" + }, + "keywords": [ + "express", + "expressjs", + "csv" + ], + "repository": "git://github.com/jczaplew/csv-express.git", + "main": "index.js", + "engines": { + "node": "0.6 || 0.8 || 0.10 || 0.12 || 4.x" + }, + "scripts": { + "test": "mocha", + "prepublish": "make test" + }, + "dependencies": { + "iconv-lite": "^0.4.13" } } diff --git a/test/index.js b/test/index.js index 74cc9ce..0c1de2d 100644 --- a/test/index.js +++ b/test/index.js @@ -1,7 +1,7 @@ -var express = require('express') - , request = require('superagent') - , csv = require('../') - , app = express.createServer(); +var express = require('express'), + request = require('superagent'), + csv = require('../'), + app = express(); app.get('/test/1', function(req, res) { res.csv([ @@ -29,102 +29,46 @@ app.get('/test/objectArray', function(req, res) { ]); }); +app.get('/test/cyrillic/utf8', function(req, res) { + res.csv([ + ['Привет', 'мир'] + ]); +}); + +app.get('/test/cyrillic/cp1251', function(req, res) { + res.charset = "cp-1251"; + res.csv([ + ['Привет', 'мир'] + ]); +}); + app.listen(8383); -describe('express-csv', function() { - it('should expose .version', function() { - csv.version.should.be.match(/[0-9]+\.[0-9]+\.[0-9]+/); - }); +describe('csv-express', function() { it('should expose .separator', function() { - csv.separator.should.be.a('string'); + csv.separator.should.be.type('string'); }); it('should expose .preventCast', function() { - csv.preventCast.should.be.a('boolean'); + csv.preventCast.should.be.type('boolean'); }); - + it('should expose .ignoreNullOrUndefined', function() { - csv.ignoreNullOrUndefined.should.be.a('boolean'); + csv.ignoreNullOrUndefined.should.be.type('boolean'); }); it('should extend res.csv', function() { - if (express.version.match(/^2\.[0-9]+\.[0-9]+$/)) { - // express 2.x - require('http').ServerResponse.prototype.csv.should.be.a('function'); - } else { - // express 3.x - require('express').response.csv.should.be.a('function'); - } + require('express').response.csv.should.be.type('function'); }); }); describe('res.csv()', function() { - describe('when given an array of objects', function() { - describe('and ignoreNullOrUndefined is true', function() { - var rows; - - beforeEach(function(done) { - csv.ignoreNullOrUndefined = true; - request - .get('http://127.0.0.1:8383/test/objectArray') - .end(function(res) { - rows = res.text.split("\r\n"); - done(); - }); - }); - - it('should include values that exist', function() { - rows[0].split(",")[0].should.equal('"a"'); - rows[1].split(",")[0].should.equal('"b"'); - }); - - it('should exclude null', function() { - rows[0].split(",")[1].should.equal(''); - rows[0].split(",")[2].should.equal(''); - }); - - it('should exclude undefined', function() { - rows[0].split(",")[2].should.equal(''); - rows[1].split(",")[2].should.equal(''); - }); - }); - - describe('and ignoreNullOrUndefined is false', function() { - var rows; - - beforeEach(function(done) { - csv.ignoreNullOrUndefined = false; - request - .get('http://127.0.0.1:8383/test/objectArray') - .end(function(res) { - rows = res.text.split("\r\n"); - done(); - }); - }); - - it('should include values that exist', function() { - rows[0].split(",")[0].should.equal('"a"'); - rows[1].split(",")[0].should.equal('"b"'); - }); - - it('should include null', function() { - rows[0].split(",")[1].should.equal('"null"'); - rows[1].split(",")[1].should.equal('"null"'); - }); - - it('should include undefined', function() { - rows[0].split(",")[2].should.equal('"undefined"'); - rows[1].split(",")[2].should.equal('"undefined"'); - }); - }); - }); - it('should response csv', function(done) { - request + request .get('http://127.0.0.1:8383/test/1') - .end(function(res) { + .end(function(error, res) { res.text.should.equal('"a","b","c"\r\n"d","e","f"\r\n'); done(); }); @@ -133,7 +77,7 @@ describe('res.csv()', function() { it('should response valid content-type', function(done) { request .get('http://127.0.0.1:8383/test/1') - .end(function(res) { + .end(function(error, res) { res.headers['content-type'].should.match(/^text\/csv/); done(); }); @@ -142,7 +86,7 @@ describe('res.csv()', function() { it('should response csv includes ignored null', function(done) { request .get('http://127.0.0.1:8383/test/2') - .end(function(res) { + .end(function(error, res) { res.text.should.equal('"a","b",\r\n'); done(); }); @@ -151,7 +95,7 @@ describe('res.csv()', function() { it('should response csv includes ignored undefined', function(done) { request .get('http://127.0.0.1:8383/test/3') - .end(function(res) { + .end(function(error, res) { res.text.should.equal('"a","b",\r\n'); done(); }); @@ -162,7 +106,7 @@ describe('res.csv()', function() { csv.ignoreNullOrUndefined = false; request .get('http://127.0.0.1:8383/test/2') - .end(function(res) { + .end(function(error, res) { csv.ignoreNullOrUndefined = prevOption; res.text.should.equal('"a","b","null"\r\n'); done(); @@ -174,7 +118,7 @@ describe('res.csv()', function() { csv.ignoreNullOrUndefined = false; request .get('http://127.0.0.1:8383/test/3') - .end(function(res) { + .end(function(error, res) { csv.ignoreNullOrUndefined = prevOption; res.text.should.equal('"a","b","undefined"\r\n'); done(); @@ -186,7 +130,7 @@ describe('res.csv()', function() { csv.separator = '\t'; request .get('http://127.0.0.1:8383/test/1') - .end(function(res) { + .end(function(error, res) { csv.separator = prevSeparator; res.text.should.equal('"a"\t"b"\t"c"\r\n"d"\t"e"\t"f"\r\n'); done(); @@ -198,10 +142,93 @@ describe('res.csv()', function() { csv.preventCast = true; request .get('http://127.0.0.1:8383/test/1') - .end(function(res) { + .end(function(error, res) { csv.preventCast = prevSetting; res.text.should.equal('="a",="b",="c"\r\n="d",="e",="f"\r\n'); done(); }); }); }); + + +describe('when given an array of objects', function() { + describe('and ignoreNullOrUndefined is true', function() { + var rows; + + beforeEach(function(done) { + csv.ignoreNullOrUndefined = true; + request + .get('http://localhost:8383/test/objectArray') + .end(function(error, res) { + rows = res.text.split("\r\n"); + done(); + }); + }); + + it('should include values that exist', function() { + rows[0].split(",")[0].should.equal('"a"'); + rows[1].split(",")[0].should.equal('"b"'); + }); + + it('should exclude null', function() { + rows[0].split(",")[1].should.equal(''); + rows[0].split(",")[2].should.equal(''); + }); + + it('should exclude undefined', function() { + rows[0].split(",")[2].should.equal(''); + rows[1].split(",")[2].should.equal(''); + }); + }); + + describe('and ignoreNullOrUndefined is false', function() { + var rows; + + beforeEach(function(done) { + csv.ignoreNullOrUndefined = false; + request + .get('http://127.0.0.1:8383/test/objectArray') + .end(function(error, res) { + rows = res.text.split("\r\n"); + done(); + }); + }); + + it('should include values that exist', function() { + rows[0].split(",")[0].should.equal('"a"'); + rows[1].split(",")[0].should.equal('"b"'); + }); + + it('should include null', function() { + rows[0].split(",")[1].should.equal('"null"'); + rows[1].split(",")[1].should.equal('"null"'); + }); + + it('should include undefined', function() { + rows[0].split(",")[2].should.equal('"undefined"'); + rows[1].split(",")[2].should.equal('"undefined"'); + }); + }); +}); + + +describe('when given cyrillyc data', function() { + it('should response with utf-8 text', function(done) { + request + .get('http://127.0.0.1:8383/test/cyrillic/utf8') + .end(function(error, res) { + res.text.should.equal('"Привет","мир"\r\n'); + done(); + }); + }); + + it('should response with cp-1251 text', function(done) { + request + .get('http://127.0.0.1:8383/test/cyrillic/cp1251') + .end(function(error, res) { + var text = new Buffer([0x22, 0xcf, 0xf0, 0xe8, 0xe2, 0xe5, 0xf2, 0x22, 0x2c, 0x22, 0xcc, 0xe8, 0xf0, 0x22, 0x0d, 0x0a]); + res.text.should.equal(text.toString()); + done(); + }); + }); +});