diff --git a/README.md b/README.md index 297db7a..42c9c3d 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ Get network traffic statistics from [vnStat](https://github.com/vergoh/vnstat). ## Example +### Callback style + ```js const vnstat = require( 'vnstat-dumpdb' )(); @@ -36,6 +38,28 @@ vnstat.getConfig( ( err, config ) => { } ); ``` +### Promise style + +```js +const vnstat = require( 'vnstat-dumpdb' )(); + +// Get traffic per day with async/await +async function getTraffic () { + try { + const data = await vnstat.getStats( 'eth0' ); + console.log( data.traffic.days ); + + const config = await vnstat.getConfig(); + console.log( `Interfaces updating every ${config.UpdateInterval} minutes` ); + } + catch ( err ) { + console.log( err ); + } +} + +getTraffic(); +``` + ## Installation @@ -56,7 +80,11 @@ setting | type | default | description ## Callback & errors -Each method below takes a callback _function_ which gets two arguments: +Each method supports both **callback** and **Promise** patterns: + +### Callback style + +Each method takes an optional callback _function_ which gets two arguments: * `err` - Instance of `Error` or `null` * `data` - Result `object` or not set when error @@ -73,6 +101,26 @@ const myCallback = ( err, data ) => { }; ``` +### Promise style + +If no callback is provided, the method returns a `Promise`: + +```js +// Using async/await +try { + const data = await vnstat.getConfig(); + console.log( data ); +} +catch ( err ) { + console.log( err ); +} + +// Using .then() / .catch() +vnstat.getConfig() + .then( ( data ) => console.log( data ) ) + .catch( ( err ) => console.log( err ) ); +``` + #### Errors @@ -84,18 +132,21 @@ invalid interface | `iface` is invalid or not set up | -## getStats ( [iface], callback ) +## getStats ( [iface], [callback] ) Get statistics for one, multiple or all interfaces. -* One: `vnstat.getStats( 'eth0', callback )` -* All: `vnstat.getStats( false, callback )` +* One: `vnstat.getStats( 'eth0', callback )` or `await vnstat.getStats( 'eth0' )` +* All: `vnstat.getStats( false, callback )` or `await vnstat.getStats()` ```js -// Get traffic for interface en1 +// Get traffic for interface en1 (callback) vnstat.getStats( 'en1', console.log ); +// Get traffic for interface en1 (promise) +const data = await vnstat.getStats( 'en1' ); + // Output { id: 'en1', nick: 'en1', @@ -146,11 +197,12 @@ vnstat.getStats( 'en1', console.log ); ``` -## getConfig ( callback ) +## getConfig ( [callback] ) Get vnStat configuration. ```js +// Callback style vnstat.getConfig( ( err, config ) => { if ( err ) { console.log( err ); @@ -159,6 +211,10 @@ vnstat.getConfig( ( err, config ) => { console.log( `Interfaces updating every ${config.UpdateInterval} seconds` ); } ); + +// Promise style +const config = await vnstat.getConfig(); +console.log( `Interfaces updating every ${config.UpdateInterval} seconds` ); ``` diff --git a/package.json b/package.json index e4b371e..48b9dca 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "url": "https://frankl.in" }, "name": "vnstat-dumpdb", - "version": "2.0.3", + "version": "2.1.0", "description": "Get vnStat network traffic and configuration", "repository": { "type": "git", diff --git a/test.js b/test.js index d7b6bed..1d6bd16 100644 --- a/test.js +++ b/test.js @@ -102,5 +102,117 @@ dotest.add( 'Error: command failed', ( test ) => { } ); +// Promise-based tests +dotest.add( 'Promise: .getConfig', async ( test ) => { + config.bin = './testing/mock-vnstat'; + vnstat = app( config ); + + try { + const data = await vnstat.getConfig(); + test() + .isObject( 'fail', 'data', data ) + .isNotEmpty( 'fail', 'data.DatabaseDir', data?.DatabaseDir ) + .isString( 'fail', 'data.UpdateInterval', data?.UpdateInterval ) + .done(); + } + catch ( err ) { + test( err ).done(); + } +} ); + + +dotest.add( 'Promise: .getStats - iface', async ( test ) => { + try { + const data = await vnstat.getStats( 'eth0' ); + const days = data?.traffic?.days || data?.traffic?.day; + const rx = days?.[0]?.rx; + const hasData = days && days.length > 0; + + const t = test() + .isObject( 'fail', 'data', data ) + .isString( 'fail', 'data.id', data?.id || data?.name ) + .isObject( 'fail', 'data.traffic', data?.traffic ) + .isArray( 'fail', 'data.traffic.days', days ); + + if ( hasData ) { + t.isObject( 'fail', 'data.traffic.days[0]', days?.[0] ) + .isNumber( 'fail', 'data.traffic.days[0].rx', rx ); + } + + t.done(); + } + catch ( err ) { + test( err ).done(); + } +} ); + + +dotest.add( 'Promise: .getStats - all', async ( test ) => { + try { + const data = await vnstat.getStats(); + test() + .isArray( 'fail', 'data', data ) + .done(); + } + catch ( err ) { + test( err ).done(); + } +} ); + + +dotest.add( 'Promise: Error - invalid interface', async ( test ) => { + try { + await vnstat.getStats( 'unreal-iface' ); + test() + .fail( 'Should have thrown an error' ) + .done(); + } + catch ( err ) { + test() + .isError( 'fail', 'err', err ) + .isExactly( 'fail', 'err.message', err?.message, 'invalid interface' ) + .done(); + } +} ); + + +dotest.add( 'Promise: Error - no config', async ( test ) => { + config.bin = '-'; + vnstat = app( config ); + + try { + await vnstat.getConfig(); + test() + .fail( 'Should have thrown an error' ) + .done(); + } + catch ( err ) { + test() + .isError( 'fail', 'err', err ) + .isExactly( 'fail', 'err.message', err?.message, 'no config' ) + .done(); + } +} ); + + +dotest.add( 'Promise: Error - command failed', async ( test ) => { + config.bin = '-'; + vnstat = app( config ); + + try { + await vnstat.getStats(); + test() + .fail( 'Should have thrown an error' ) + .done(); + } + catch ( err ) { + test() + .isError( 'fail', 'err', err ) + .isExactly( 'fail', 'err.message', err?.message, 'command failed' ) + .done(); + } +} ); + + // Start the tests dotest.run(); diff --git a/vnstat-dumpdb.js b/vnstat-dumpdb.js index bded456..e40004b 100644 --- a/vnstat-dumpdb.js +++ b/vnstat-dumpdb.js @@ -36,11 +36,44 @@ function doError ( message, err, details, callback ) { } +/** + * Helper to promisify a function that uses callbacks + * + * @param {function} fn The function to promisify + * @returns {function} Wrapped function that returns void if callback provided, or Promise if not + */ + +function promisify ( fn ) { + return function ( ...args ) { + const callback = args[args.length - 1]; + + // If last argument is a function, use callback style + if ( typeof callback === 'function' ) { + return fn.apply( this, args ); + } + + // Otherwise, return a Promise + return new Promise( ( resolve, reject ) => { + args.push( ( err, data ) => { + if ( err ) { + reject( err ); + } + else { + resolve( data ); + } + } ); + + fn.apply( this, args ); + } ); + }; +} + + /** * Get vnStat config * - * @param {function} callback `(err, data)` - * @returns {void} + * @param {function} [callback] `(err, data)` + * @returns {void|Promise} * @callback callback */ @@ -72,9 +105,9 @@ function getConfig ( callback ) { /** * Get stats database * - * @param {string} [iface] Limit data to one interface - * @param {function} callback `(err, data)` - * @returns {void} + * @param {string} [iface] Limit data to one interface + * @param {function} [callback] `(err, data)` + * @returns {void|Promise} * @callback callback */ @@ -138,8 +171,8 @@ module.exports = ( { set.iface = iface; return { - getStats, - getConfig, + getStats: promisify( getStats ), + getConfig: promisify( getConfig ), set, };