1
0
Fork 0

Notification support given no rating results

master
Mike Gerwitz 2018-05-01 09:45:01 -04:00
commit f38d0bf96c
16 changed files with 860 additions and 31 deletions

View File

@ -49,7 +49,8 @@
"remote": {
"host": "localhost",
"domain": ""
}
},
"noResultsUrl": ""
},
"c1export": {
"host": "localhost",

View File

@ -61,7 +61,7 @@ module.exports = Class( 'QuoteDataApi' )
if ( !( Class.isA( DataApi, dapi ) ) )
{
throw TypeError(
'Expected object of type DataApi; given: ' + data_api
'Expected object of type DataApi; given: ' + dapi
);
}

View File

@ -139,14 +139,33 @@ module.exports = Class( 'HttpDataApi' )
this._validateDataType( data );
this._impl.requestData(
this._url,
this._method,
this.requestData( this._url, this._method, data, callback );
return this;
},
/**
* Request data from underlying HttpImpl
*
* Subtypes may override this method to alter any aspect of the request
* before sending.
*
* @param {string} url destination URL
* @param {string} method RFC-2616-compliant HTTP method
* @param {Object|string} data request params
* @param {function(?Error, ?string)} callback server response callback
*
* @return {HttpDataApi} self
*/
'virtual protected requestData'( url, method, data, callback )
{
return this._impl.requestData(
url,
method,
this.encodeData( data ),
callback
);
return this;
},

View File

@ -0,0 +1,127 @@
/**
* Provide data as part of URL
*
* Copyright (C) 2018 R-T Specialty, LLC.
*
* This file is part of the Liza Data Collection Framework
*
* Liza is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
'use strict';
const { Trait } = require( 'easejs' );
const HttpDataApi = require( './HttpDataApi' );
/**
* Place fields from given data in the URL
*
* All remaining fields are passed to the underlying supertype.
*/
module.exports = Trait( 'HttpDataApiUrlData' )
.extend( HttpDataApi,
{
/**
* Fields to take from data and place in URL
* @type {string}
*/
'private _fields': [],
/**
* Initialize with URL field list
*
* @param {Array<string>} fields list of fields to include in URL
*/
__mixin( fields )
{
this._fields = fields;
},
/**
* Concatenate chosen fields with URL
*
* The previously specified fields will have their values delimited by '/'
* and will be concatenated with the URL. All used fields in DATA will be
* removed before being passed to the supertype. METHOD and CALLBACK are
* proxied as-is.
*
* @param {string} url destination URL
* @param {string} method RFC-2616-compliant HTTP method
* @param {Object|string} data request params
* @param {function(Error, Object)} callback server response callback
*
* @return {HttpImpl} self
*/
'override public requestData'( url, method, data, callback )
{
const [ values, filtered_data ] = this._getFieldValues( data );
const params = values.map( ( [ , value ] ) => value );
const missing = values.filter( ( [ , value ] ) => value === undefined );
if ( missing.length > 0 )
{
callback(
Error(
"Missing URL parameters: " +
missing.map( ( [ field ] ) => field ).join( ", " )
),
null
);
return this;
}
const built_url = ( params.length > 0 )
? url + '/' + params.join( '/' )
: url;
return this.__super( built_url, method, filtered_data, callback );
},
/**
* Associate fields with their respective values from DATA
*
* The returned values are of the form `[ [ field, value ], ... ]`.
* The returned data object is a copy of the original and is stripped
* of the respective fields.
*
* @param {Object} data source data
*
* @return {Array} values and copy of data stripped of those fields
*/
'private _getFieldValues'( data )
{
const fieldset = new Set( this._fields );
const values = this._fields.map( field => [ field, data[ field ] ] );
// copy of data with fields stripped
const new_data = Object.keys( data ).reduce( ( dest, key ) =>
{
if ( fieldset.has( key ) )
{
return dest;
}
dest[ key ] = data[ key ];
return dest;
}, {} );
return [ values, new_data ];
},
} );

View File

@ -87,7 +87,7 @@ module.exports = Class( 'NodeHttpImpl' )
*
* @return {HttpImpl} self
*/
'public requestData'( url, method, data, callback )
'virtual public requestData'( url, method, data, callback )
{
const options = this._parseUrl( url );
const protocol = options.protocol.replace( /:$/, '' );

View File

@ -37,7 +37,7 @@ module.exports = Trait( 'SpoofedNodeHttpImpl' )
{
/**
* Session to spoof
* @type {UserSession}
* @type {UserRequest}
*/
'private _request': null,
@ -45,11 +45,11 @@ module.exports = Trait( 'SpoofedNodeHttpImpl' )
/**
* Use session for spoofing requests
*
* @param {UserSession} session session to spoof
* @param {UserRequest} request session to spoof
*/
__mixin( session )
__mixin( request )
{
this._request = session;
this._request = request;
},

View File

@ -71,7 +71,7 @@ module.exports = Class( 'XhrHttpImpl' )
*
* @return {HttpImpl} self
*/
'public requestData': function( url, method, data, callback )
'virtual public requestData': function( url, method, data, callback )
{
if ( typeof data !== 'string' )
{

View File

@ -113,7 +113,8 @@ module.exports = AbstractClass( 'Daemon',
this._createDebugLog(),
this._createAccessLog(),
this._conf.get( 'skey' ),
] ).then( ([ debug_log, access_log, skey ]) =>
this._conf.get( 'services.rating.noResultsUrl' ),
] ).then( ([ debug_log, access_log, skey, no_results_url ]) =>
{
this._debugLog = debug_log;
this._accessLog = access_log;
@ -122,7 +123,7 @@ module.exports = AbstractClass( 'Daemon',
this._rater = liza.server.rater.ProcessManager();
this._encService = this.getEncryptionService();
this._memcache = this.getMemcacheClient();
this._routers = this.getRouters( skey );
this._routers = this.getRouters( skey, no_results_url );
} )
.then( () => this._startDaemon() );
},
@ -182,14 +183,16 @@ module.exports = AbstractClass( 'Daemon',
},
'protected getProgramController': function( skey )
'protected getProgramController': function( skey, no_results_url )
{
var controller = require( './controller' );
controller.rater = this._rater;
controller.rater = this._rater;
controller.no_results_url = no_results_url || "";
if ( skey )
{
controller.skey = skey;
controller.skey = skey;
}
return controller;
@ -276,10 +279,10 @@ module.exports = AbstractClass( 'Daemon',
'abstract protected getEncryptionService': [],
'protected getRouters': function( skey )
'protected getRouters': function( skey, no_results_url )
{
return [
this.getProgramController( skey ),
this.getProgramController( skey, no_results_url ),
this.getScriptsController(),
this.getClientErrorController(),
];

View File

@ -43,6 +43,12 @@ const {
},
dapi: {
http: {
HttpDataApi,
HttpDataApiUrlData,
NodeHttpImpl,
SpoofedNodeHttpImpl,
},
DataApiFactory,
DataApiManager,
},
@ -69,6 +75,7 @@ const {
},
RatingService,
RatingServiceSubmitNotify,
TokenedService,
TokenDao,
},
@ -93,8 +100,9 @@ var sflag = {};
// TODO: kluge to get liza somewhat decoupled from lovullo (rating module)
exports.rater = {};
exports.skey = "";
exports.rater = {};
exports.skey = "";
exports.no_results_url = "";
exports.init = function( logger, enc_service, conf )
@ -118,7 +126,31 @@ exports.init = function( logger, enc_service, conf )
server_cache = _createCache( server );
server.init( server_cache, exports.rater );
rating_service = RatingService( logger, dao, server, exports.rater );
// TODO: do none of this if no_results_url is provided
const createSubmitDapi = request => HttpDataApi
.use( HttpDataApiUrlData( [ 'quote_id' ] ) )
(
exports.no_results_url,
'PUT',
NodeHttpImpl
.use( SpoofedNodeHttpImpl( request ) )
(
{
http: require( 'http' ),
https: require( 'https' ),
},
require( 'url' ),
this._origin
),
''
);
rating_service = RatingService
.use( RatingServiceSubmitNotify( createSubmitDapi, dao ) )
(
logger, dao, server, exports.rater
);
// TODO: exports.init needs to support callbacks; this will work, but
// only because it's unlikely that we'll get a request within

View File

@ -783,5 +783,70 @@ module.exports = Class( 'MongoServerDao' )
}
);
},
} );
/**
* Set arbitrary data on a document
*
* @param {number} qid quote/document id
* @param {string} key field key
* @param {*} value field value
* @param {function(?Error)} callback completion callback
*
* @return {undefined}
*/
'public setDocumentField'( qid, key, value, callback )
{
this._collection.update(
{ id: qid },
{ '$set': { [key]: value } },
// create record if it does not yet exist
{ upsert: true },
// on complete
function( err )
{
callback && callback( err );
return;
}
);
},
/**
* Retrieve arbitrary data on a document
*
* @param {number} qid quote/document id
* @param {string} key field key
* @param {function(?Error)} callback completion callback
*
* @return {undefined}
*/
'public getDocumentField'( qid, key, callback )
{
this._collection.find(
{ id: qid },
{ limit: 1 },
function( err, cursor )
{
if ( err !== null )
{
callback( err, null );
return;
}
cursor.toArray( function( err, data )
{
if ( err !== null )
{
callback( err, null );
return;
}
callback( null, data[ key ] );
} );
}
);
},
} );

View File

@ -154,8 +154,8 @@ module.exports = Class( 'RatingService',
{
actions = actions || [];
_self._postProcessRaterData(
rate_data, actions, program, quote
_self.postProcessRaterData(
request, rate_data, actions, program, quote
);
const class_dest = {};
@ -244,14 +244,17 @@ module.exports = Class( 'RatingService',
/**
* Process rater data returned from a rater
*
* @param {Object} data rating data returned
* @param {Array} actions actions to send to client
* @param {Program} program program used to perform rating
* @param {Quote} quote quote used for rating
* @param {UserRequest} request user request to satisfy
* @param {Object} data rating data returned
* @param {Array} actions actions to send to client
* @param {Program} program program used to perform rating
* @param {Quote} quote quote used for rating
*
* @return {undefined}
*/
_postProcessRaterData: function( data, actions, program, quote )
'virtual protected postProcessRaterData': function(
request, data, actions, program, quote
)
{
var meta = data._cmpdata || {};

View File

@ -0,0 +1,140 @@
/**
* Notification on all submit
*
* Copyright (C) 2018 R-T Specialty, LLC.
*
* This file is part of liza.
*
* liza is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
'use strict';
const { Trait } = require( 'easejs' );
const DslRaterContext = require( '../rater/DslRaterContext' )
const RatingService = require( './RatingService' );
/**
* Triggers DataApi when no results are available
*
* This information is currently stored in `__prem_avail_count`. In the
* future, it may be worth accepting a parameter to configure this at
* runtime.
*
* Notification status will persist using the provided DAO. The next time
* such a notification is requested, it will only occur if the flag is not
* set.
*/
module.exports = Trait( 'RatingServiceSubmitNotify' )
.extend( RatingService,
{
/**
* Function returning DataApi to trigger
* @type {Function(UserSession):DataApi}
*/
'private _dapif': null,
/**
* Data store for notification flag
* @type {ServerDao}
*/
'private _notifyDao': null,
/**
* Initialize mixin with DataApi to trigger
*
* @param {Function(UserSession):DataApi} dapif Function producing DataApi
* @param {ServerDao} dao store for notification flag
*/
__mixin( dapif, dao )
{
this._dapif = dapif;
this._notifyDao = dao;
},
/**
* Trigger previously provided DataApi when no results are available
*
* Result count is determined by DATA.__prem_avail_count.
*
* @param {UserRequest} request user request
* @param {Object} data rating data returned
* @param {Array} actions actions to send to client
* @param {Program} program program used to perform rating
* @param {Quote} quote quote used for rating
*
* @return {undefined}
*/
'override protected postProcessRaterData'(
request, data, actions, program, quote
)
{
const quote_id = quote.getId();
const avail = ( data.__prem_avail_count || [ 0 ] )[ 0 ];
if ( avail === 0 )
{
this._getNotifyState( quote_id, notified =>
{
if ( notified === true )
{
return;
}
this._dapif( request )
.request( { quote_id: quote_id }, () => {} );
this._setNotified( quote_id );
} );
}
this.__super( request, data, actions, program, quote );
},
/**
* Get value of notification flag
*
* @param {number} quote_id id of quote
* @param {function(boolean)} callback callback to call when complete
*
* @return {undefined}
*/
'private _getNotifyState'( quote_id, callback )
{
this._notifyDao.getDocumentField(
quote_id,
'submitNotified',
( err, value ) => callback( value )
);
},
/**
* Set notification flag
*
* @param {number} quote_id id of quote
*
* @return {undefined}
*/
'private _setNotified'( quote_id )
{
this._notifyDao.setDocumentField(
quote_id, 'submitNotified', true
);
},
} );

View File

@ -0,0 +1,87 @@
/**
* Tests RatingService
*
* Copyright (C) 2018 R-T Specialty, LLC.
*
* This file is part of the Liza Data Collection Framework.
*
* liza is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
'use strict'
exports.getStubs = function()
{
const program_id = 'foo';
const program = {
getId: () => program_id,
};
// rate reply
const stub_rate_data = {};
const rater = {
rate: ( quote, session, indv, callback ) => callback( stub_rate_data ),
};
const raters = {
byId: () => rater,
};
const logger = {
log: () => {},
};
const server = {
sendResponse: () => {},
sendError: () => {},
};
const dao = {
mergeBucket: () => {},
saveQuoteClasses: () => {},
setWorksheets: () => {},
};
const session = {
isInternal: () => false,
};
const request = {
getSession: () => session,
getSessionIdName: () => {},
};
const response = {};
const quote = {
getProgramId: () => program_id,
getProgram: () => program,
getId: () => 0,
};
return {
program: program,
stub_rate_data: stub_rate_data,
rater: rater,
raters: raters,
logger: logger,
server: server,
dao: dao,
session: session,
request: request,
response: response,
quote: quote,
};
};

View File

@ -0,0 +1,123 @@
/**
* Tests HttpDataApiUrlData
*
* Copyright (C) 2018 R-T Specialty, LLC.
*
* This file is part of the Liza Data Collection Framework.
*
* liza is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
'use strict'
const { Class } = require( 'easejs' );
const { expect } = require( 'chai' );
const {
dapi: {
http: {
HttpDataApi,
HttpImpl,
HttpDataApiUrlData: Sut,
},
},
} = require( '../../../' );
describe( 'HttpDataApiUrlData', () =>
{
[
{
fields: [],
data: {
foo: "foodata",
bar: "bardata",
},
base_url: "base",
after_data: "foo=foodata&bar=bardata",
expected: "base",
},
{
fields: [ 'foo', 'bar' ],
data: {
foo: "foodata",
bar: "bardata",
baz: "shouldnotuse",
},
base_url: "base2",
after_data: "baz=shouldnotuse",
expected: "base2/foodata/bardata",
},
].forEach( ( { fields, data, base_url, after_data, expected }, i ) =>
it( `can include portion of data in url (#${i})`, done =>
{
const expected_method = 'PUT';
const impl = Class.implement( HttpImpl ).extend(
{
'virtual requestData'( url, method, given_data, callback )
{
expect( url ).to.equal( expected );
expect( method ).to.equal( expected_method );
expect( given_data ).to.deep.equal( after_data );
// should not have mutated the original object
// (they shouldn't be the same object)
expect( data ).to.not.equal( given_data );
// should be done
callback();
},
setOptions: () => {},
} )();
const sut = HttpDataApi.use( Sut( fields ) )(
base_url, expected_method, impl
);
const result = sut.request( data, done );
} )
);
it( "throws error if param is missing from data", done =>
{
const impl = Class.implement( HttpImpl ).extend(
{
'virtual requestData': ( url, method, data, callback ) => {},
setOptions: () => {},
} )();
const sut = HttpDataApi.use( Sut( [ 'foo' ] ) )(
'', 'PUT', impl
);
// missing `foo` in data
sut.request( { unused: "missing foo" }, ( err, data ) =>
{
expect( err ).to.be.instanceof( Error );
expect( err.message ).to.match( /\bfoo\b/ );
expect( data ).to.equal( null );
done();
} );
} );
} );

View File

@ -0,0 +1,164 @@
/**
* Tests RatingServiceSubmitNotify
*
* Copyright (C) 2018 R-T Specialty, LLC.
*
* This file is part of the Liza Data Collection Framework.
*
* liza is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
'use strict'
const { Class } = require( 'easejs' );
const { expect } = require( 'chai' );
const {
dapi: {
DataApi,
},
server: {
service: {
RatingServiceSubmitNotify: Sut,
RatingService,
},
},
test: {
server: {
service: {
RatingServiceStub,
},
},
},
} = require( '../../../' );
describe( 'RatingServiceSubmitNotify', () =>
{
[
{
prem_avail_count: [ 0 ],
prev_called: false,
expected_request: true,
},
{
prem_avail_count: [ 2 ],
prev_called: false,
expected_request: false,
},
{
// this shouldn't happen; ignore all but first index
prem_avail_count: [ 2, 2 ],
prev_called: false,
expected_request: false,
},
// save as above, but already saved
{
prem_avail_count: [ 0 ],
prev_called: true,
expected_request: false,
},
{
prem_avail_count: [ 2 ],
prev_called: true,
expected_request: false,
},
{
// this shouldn't happen; ignore all but first index
prem_avail_count: [ 2, 2 ],
prev_called: true,
expected_request: false,
},
].forEach( ( { prem_avail_count, expected_request, prev_called }, i ) =>
it( `sends request on post process if no premiums (#${i})`, done =>
{
const {
dao,
logger,
quote,
raters,
request,
response,
server,
stub_rate_data,
} = RatingServiceStub.getStubs();
const quote_id = 1234;
let requested = false;
const dapif = given_request =>
Class.implement( DataApi ).extend(
{
// warning: if an expectation fails, because of how
// RatingService handles errors, it will cause the test to
// _hang_ rather than throw the assertion error
request( data, callback )
{
expect( given_request ).to.equal( request );
expect( data ).to.deep.equal( { quote_id: quote_id } );
requested = true;
},
} )();
const sut = RatingService.use( Sut( dapif, dao ) )(
logger, dao, server, raters
);
quote.getId = () => quote_id;
// one of the methods that is called by the supertype
let save_called = false;
dao.setWorksheets = () => save_called = true;
// whether the notify flag is actually set
let notify_saved = false;
// request for notification status
dao.getDocumentField = ( qid, key, callback ) =>
{
expect( qid ).to.equal( quote_id );
expect( key ).to.equal( 'submitNotified' );
callback( null, prev_called );
};
dao.setDocumentField = ( qid, key, value, callback ) =>
{
expect( qid ).to.equal( quote_id );
expect( key ).to.equal( 'submitNotified' );
expect( value ).to.equal( true );
notify_saved = true;
};
stub_rate_data.__prem_avail_count = prem_avail_count;
sut.request( request, response, quote, 'something', () =>
{
expect( requested ).to.equal( expected_request );
expect( save_called ).to.be.true;
// only save notification status if we're notifying
expect( notify_saved ).to.equal(
!prev_called && expected_request
);
done();
} );
} )
);
} );

View File

@ -0,0 +1,65 @@
/**
* Tests RatingService
*
* Copyright (C) 2018 R-T Specialty, LLC.
*
* This file is part of the Liza Data Collection Framework.
*
* liza is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
'use strict'
const { expect } = require( 'chai' );
const Sut = require( '../../../' ).server.service.RatingService;
const RatingServiceStub = require( '../../../' ).test.server.service.RatingServiceStub;
describe( 'RatingService', () =>
{
describe( "protected API", () =>
{
it( "calls #postProcessRaterData after rating before save", done =>
{
let processed = false;
const {
logger,
server,
raters,
dao,
request,
response,
quote,
} = RatingServiceStub.getStubs();
dao.mergeBucket = () =>
{
expect( processed ).to.equal( true );
done();
};
const sut = Sut.extend(
{
'override postProcessRaterData'(
request, data, actions, program, quote
)
{
processed = true;
}
} )( logger, dao, server, raters );
sut.request( request, response, quote, 'something', () => {} );
} );
} );
} );