Comparing version 3.0.2 to 3.1.0
@@ -5,4 +5,4 @@ 'use strict'; | ||
const Hoek = require('hoek'); | ||
const Async = require('async'); | ||
const internals = { | ||
@@ -14,3 +14,3 @@ routeMap: new Map() | ||
options.separator = options.separator || ','; | ||
internals.separator = options.separator || ','; | ||
internals.maximumElementsInArray = options.maximumElementsInArray || 5; | ||
@@ -40,3 +40,2 @@ | ||
if (path.endsWith('.csv')) { | ||
request.setUrl(`${path.substring(0, path.length - 4)}${request.url.search}`); | ||
@@ -66,5 +65,30 @@ request.headers.accept = 'text/csv'; | ||
if (preferedType && internals.routeMap.has(internals.createRouteMethodString(request.route.path, request.route.method))) { | ||
if (!(preferedType && internals.routeMap.has(internals.createRouteMethodString(request.route.path, request.route.method)))) { | ||
return reply.continue(); | ||
} | ||
const dynamicHandlerObject = request.route.settings.plugins['hapi-csv'] || {}; | ||
const resolvedSchemasObject = {}; | ||
return Async.forEachOf(dynamicHandlerObject, (handler, path, callback) => { | ||
return handler(request, (err, dynamicSchema) => { | ||
if (err) { | ||
return callback(err); | ||
} | ||
resolvedSchemasObject[path] = dynamicSchema; | ||
return callback(); | ||
}); | ||
}, (err) => { | ||
if (err) { | ||
return reply(err); | ||
} | ||
const schema = internals.routeMap.get(internals.createRouteMethodString(request.route.path, request.route.method)); | ||
const csv = internals.schemaToCsv(schema, request.response.source, options.separator, options.resultKey); | ||
const csv = internals.schemaToCsv(schema, resolvedSchemasObject, request.response.source, options.resultKey); | ||
@@ -76,5 +100,3 @@ // FUTURE add header=present but wait for response on https://github.com/hapijs/hapi/issues/3243 | ||
return reply(csv).type(preferedType).header('content-disposition', 'attachment;'); | ||
} | ||
return reply.continue(); | ||
}); | ||
}); | ||
@@ -90,3 +112,3 @@ | ||
internals.schemaToCsv = (schema, dataset, separator, resultKey) => { | ||
internals.schemaToCsv = (schema, dynamicSchemas, dataset, resultKey) => { | ||
@@ -105,6 +127,6 @@ // We return the dataset if the dataset is not an array or an object, just a primitive type | ||
const headerQueryArray = internals.parseSchema(schemaDescription); | ||
const headerQueryArray = internals.parseSchema(schemaDescription, dynamicSchemas); | ||
const headerQueryMap = internals.arrayToMap(headerQueryArray); | ||
return internals.headerQueryMapToCsv(headerQueryMap, dataset, separator); | ||
return internals.headerQueryMapToCsv(headerQueryMap, dataset); | ||
}; | ||
@@ -114,6 +136,10 @@ | ||
* Recursive function which parses a Joi schema to an array of header, query entries | ||
* `parrentIsArray` is true when the previous call to the function handled a joi schema of type array | ||
* `parentIsArray` is true when the previous call to the function handled a joi schema of type array | ||
*/ | ||
internals.parseSchema = (joiSchema, keyName, parrentIsArray) => { | ||
internals.parseSchema = (joiSchema, dynamicSchemas, keyName, path, parentIsArray) => { | ||
if (dynamicSchemas[path]) { | ||
joiSchema = Joi.compile(dynamicSchemas[path]).describe(); | ||
} | ||
if (joiSchema.type === 'object') { | ||
@@ -123,3 +149,3 @@ | ||
const childrenKeys = Object.keys(joiSchema.children); | ||
let children = childrenKeys.map((key) => internals.parseSchema(joiSchema.children[key], key)); | ||
let children = childrenKeys.map((key) => internals.parseSchema(joiSchema.children[key], dynamicSchemas, key, path !== undefined ? `${path}.${key}` : key)); | ||
@@ -138,3 +164,3 @@ if (!keyName) { | ||
// If the previous call was not for a joi schema of type array, we alter the keys to prefix them with the keyName | ||
if (!parrentIsArray) { | ||
if (!parentIsArray) { | ||
const name = `${keyName}.${key}`; | ||
@@ -157,3 +183,3 @@ child[name] = child[key]; | ||
const item = joiSchema.items[0]; | ||
const parsedItem = internals.parseSchema(item, keyName, true); | ||
const parsedItem = internals.parseSchema(item, dynamicSchemas, keyName, path, true); | ||
@@ -165,3 +191,3 @@ if (keyName && parsedItem) { | ||
prefixedItemArray.push(parsedItem.map((headerQuery, index) => { | ||
prefixedItemArray.push(parsedItem.map((headerQuery) => { | ||
@@ -206,3 +232,3 @@ const key = Object.keys(headerQuery)[0]; | ||
internals.headerQueryMapToCsv = (headerQueryMap, dataset, separator) => { | ||
internals.headerQueryMapToCsv = (headerQueryMap, dataset) => { | ||
@@ -212,3 +238,3 @@ let headerRow = ''; | ||
for (const header of headerQueryMap.keys()) { | ||
headerRow += `${header}${separator}`; | ||
headerRow += `${header}${internals.separator}`; | ||
} | ||
@@ -249,3 +275,3 @@ | ||
dataRow += separator; | ||
dataRow += internals.separator; | ||
} | ||
@@ -268,2 +294,5 @@ | ||
internals.createRouteMethodString = (route, method) => route + '_' + method; | ||
internals.createRouteMethodString = (route, method) => { | ||
return `${route}_${method}`; | ||
}; |
@@ -0,0 +0,0 @@ The MIT License (MIT) |
{ | ||
"name": "hapi-csv", | ||
"version": "3.0.2", | ||
"version": "3.1.0", | ||
"description": "Hapi plugin for converting a Joi response schema and dataset to csv", | ||
@@ -24,12 +24,13 @@ "main": "lib/index.js", | ||
"dependencies": { | ||
"hoek": "4.x.x" | ||
"hoek": "4.x.x", | ||
"async": "^2.5.0" | ||
}, | ||
"devDependencies": { | ||
"joi": "10.x.x", | ||
"joi": "^11.0.0", | ||
"code": "4.x.x", | ||
"hapi": "16.x.x", | ||
"lab": "12.x.x" | ||
"lab": "14.x.x" | ||
}, | ||
"peerDependencies": { | ||
"hapi": ">=13.x.x", | ||
"hapi": ">=16.1.1", | ||
"joi": ">=10.x.x" | ||
@@ -36,0 +37,0 @@ }, |
102
README.md
# Hapi-csv [![Build Status](https://travis-ci.org/Salesflare/hapi-csv.svg?branch=master)](https://travis-ci.org/Salesflare/hapi-csv) | ||
## What | ||
Converts the response to csv based on the Joi response schema when the Accept header includes `text/csv` or `application/csv` or the requested route ends with `.csv` | ||
@@ -8,3 +9,3 @@ | ||
`npm install hapi-csv` | ||
`npm install --save hapi-csv` | ||
@@ -15,11 +16,12 @@ Register the hapi-csv plugin on the server | ||
server.register({ | ||
register: require('hapi-csv'), | ||
options: { | ||
maximumElementsInArray: 5, | ||
separator: ',' | ||
} | ||
register: require('hapi-csv'), | ||
options: { | ||
maximumElementsInArray: 5, | ||
separator: ',', | ||
resultKey: 'items' | ||
} | ||
}, function (err) { | ||
if (err) throw err; | ||
... | ||
if (err) throw err; | ||
... | ||
}); | ||
@@ -50,9 +52,13 @@ ``` | ||
Or do `GET /users.csv`. | ||
The header approach is prefered. | ||
The header approach is preferred. | ||
When the request path ends in `.csv` the `.csv` part will be stripped and the accept header will be set to `text/csv`. | ||
Currently the `content-disposition` header is set to `attachment;` by default since this plugin is intended for exporting purposes, if this hinders you just let us know. | ||
Currently the `Content-Disposition` header is set to `attachment;` by default since this plugin is intended for exporting purposes, if this hinders you just let us know. | ||
To handle typical pagination responses like | ||
### Paginated responses | ||
To handle typical pagination responses pass the `resultKey` option. The value is the top level key you want to convert to csv. | ||
```json | ||
// paginated response | ||
{ | ||
@@ -67,4 +73,2 @@ "page": 1, | ||
pass in the `resultKey` option: | ||
```javascript | ||
@@ -74,3 +78,3 @@ server.register({ | ||
options: { | ||
resultKey: 'items' | ||
resultKey: 'items' // We only want the `items` in csv | ||
} | ||
@@ -83,1 +87,71 @@ }, function (err) { | ||
``` | ||
### Dynamic schemas | ||
Hapi-csv supports dynamic response schemas as well. | ||
Imagine one of your property's schema is dynamic but you still want to export the value of it to csv. | ||
You can tell hapi-csv to translate a given key on the fly when it is converting the response to csv (`onPreResponse`). | ||
On the route config set the plugin config to an object like | ||
```javascript | ||
{ | ||
'keyPath': (request, callback) => { | ||
return callback(/* Error, Joi schema */) | ||
} | ||
} | ||
``` | ||
The key is the path of the property you want to resolve dynamically. | ||
E.g. | ||
```javascript | ||
Joi.object().keys({ | ||
a: Joi.object(), | ||
b: Joi.object().keys({ | ||
c: Joi.object() | ||
}) | ||
}) | ||
``` | ||
If you want to convert `a` the key would be `a`. | ||
For `c` it would be `b.c`. | ||
Full example: | ||
```javascript | ||
server.route([{ | ||
..., | ||
config: { | ||
..., | ||
response: { | ||
schema: Joi.object().keys({ | ||
first_name: Joi.string(), | ||
last_name: Joi.string(), | ||
age: Joi.number(), | ||
custom: Joi.object(), | ||
deepCustom: Joi.object().keys({ | ||
deepestCustom: Joi.object() | ||
}) | ||
}) | ||
}, | ||
plugins: { | ||
'hapi-csv': { | ||
'custom': (request, callback) => { | ||
const schema = Joi.object().keys({ | ||
id: Joi.number(), | ||
name: Joi.string() | ||
}); | ||
return callback(null, schema); | ||
}, | ||
'deepCustom.deepestCustom': (request, callback) => { | ||
return callback(new Error('nope')); | ||
} | ||
} | ||
} | ||
} | ||
]) | ||
``` |
@@ -76,15 +76,15 @@ 'use strict'; | ||
}, | ||
{ | ||
method: 'POST', | ||
path: '/user', | ||
config: { | ||
handler: function (request, reply) { | ||
{ | ||
method: 'POST', | ||
path: '/user', | ||
config: { | ||
handler: function (request, reply) { | ||
return reply(postUser); | ||
}, | ||
response: { | ||
schema: testPostResponseSchema | ||
} | ||
return reply(postUser); | ||
}, | ||
response: { | ||
schema: testPostResponseSchema | ||
} | ||
}, { | ||
} | ||
}, { | ||
method: 'GET', | ||
@@ -554,2 +554,144 @@ path: '/userWithoutSchema', | ||
describe('Dynamic schemas', () => { | ||
it('Uses dynamic schemas', (done) => { | ||
const user = { | ||
first_name: 'firstName', | ||
last_name: 'lastName', | ||
age: 25, | ||
tag: { id: 1, name: 'guitar' } | ||
}; | ||
const userCSV = 'first_name,last_name,age,tag.id,tag.name,\n"firstName","lastName","25","1","guitar",'; | ||
const server = new Hapi.Server(); | ||
server.connection(); | ||
return server.register({ | ||
register: HapiCsv | ||
}, (err) => { | ||
expect(err, 'error').to.not.exist(); | ||
server.route([{ | ||
method: 'GET', | ||
path: '/test', | ||
config: { | ||
handler: function (request, reply) { | ||
return reply(user); | ||
}, | ||
response: { | ||
schema: Joi.object().keys({ | ||
first_name: Joi.string(), | ||
last_name: Joi.string(), | ||
age: Joi.number(), | ||
tag: Joi.object() | ||
}) | ||
}, | ||
plugins: { | ||
'hapi-csv': { | ||
'tag': (request, callback) => { | ||
const schema = Joi.object().keys({ | ||
id: Joi.number(), | ||
name: Joi.string() | ||
}); | ||
return callback(null, schema); | ||
} | ||
} | ||
} | ||
} | ||
}]); | ||
return server.initialize((err) => { | ||
expect(err, 'error').to.not.exist(); | ||
return server.inject({ | ||
method: 'GET', | ||
url: '/test', | ||
headers: { | ||
'Accept': 'text/csv' | ||
} | ||
}, (res) => { | ||
expect(res.result, 'result').to.equal(userCSV); | ||
expect(res.headers['content-type']).to.equal('text/csv; charset=utf-8'); | ||
expect(res.headers['content-disposition']).to.equal('attachment;'); | ||
return server.stop(done); | ||
}); | ||
}); | ||
}); | ||
}); | ||
it('Uses dynamic schemas: resolver function throws an error', (done) => { | ||
const user = { | ||
first_name: 'firstName', | ||
last_name: 'lastName', | ||
age: 25, | ||
tag: { id: 1, name: 'guitar' } | ||
}; | ||
const server = new Hapi.Server(); | ||
server.connection(); | ||
return server.register({ | ||
register: HapiCsv | ||
}, (err) => { | ||
expect(err, 'error').to.not.exist(); | ||
server.route([{ | ||
method: 'GET', | ||
path: '/test', | ||
config: { | ||
handler: function (request, reply) { | ||
return reply(user); | ||
}, | ||
response: { | ||
schema: Joi.object().keys({ | ||
first_name: Joi.string(), | ||
last_name: Joi.string(), | ||
age: Joi.number(), | ||
tag: Joi.object() | ||
}) | ||
}, | ||
plugins: { | ||
'hapi-csv': { | ||
'tag': (request, callback) => { | ||
return callback(new Error('ERROR')); | ||
} | ||
} | ||
} | ||
} | ||
}]); | ||
return server.initialize((err) => { | ||
expect(err, 'error').to.not.exist(); | ||
return server.inject({ | ||
method: 'GET', | ||
url: '/test', | ||
headers: { | ||
'Accept': 'text/csv' | ||
} | ||
}, (res) => { | ||
expect(res.statusCode, 'statusCode').to.equal(500); | ||
return server.stop(done); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
describe('Result key (e.g. for pagination)', () => { | ||
@@ -556,0 +698,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
172020
8
866
151
0
4
+ Addedasync@^2.5.0
+ Addedasync@2.6.4(transitive)
+ Addedlodash@4.17.21(transitive)