From 7583cc1a7115bb6476513abea7b2cfc4a39b5194 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Mon, 21 Oct 2019 11:22:20 -0400 Subject: [PATCH 01/14] RatingServiceSubmitNotify: Remove unused trait --- src/server/daemon/controller.js | 1 - .../service/RatingServiceSubmitNotify.js | 163 --------------- .../service/RatingServiceSubmitNotifyTest.js | 186 ------------------ 3 files changed, 350 deletions(-) delete mode 100644 src/server/service/RatingServiceSubmitNotify.js delete mode 100644 test/server/service/RatingServiceSubmitNotifyTest.js diff --git a/src/server/daemon/controller.js b/src/server/daemon/controller.js index 7e8ca54..5b1b6b0 100644 --- a/src/server/daemon/controller.js +++ b/src/server/daemon/controller.js @@ -85,7 +85,6 @@ const { RatingService, RatingServicePublish, - RatingServiceSubmitNotify, TokenedService, }, diff --git a/src/server/service/RatingServiceSubmitNotify.js b/src/server/service/RatingServiceSubmitNotify.js deleted file mode 100644 index 05712c6..0000000 --- a/src/server/service/RatingServiceSubmitNotify.js +++ /dev/null @@ -1,163 +0,0 @@ -/** - * Notification on all submit - * - * Copyright (C) 2010-2019 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 . - */ - -'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. The flag is not set in the event of an error (determined by the - * DataApi; usually an HTTP error). - */ -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. If the - * notification is successful (determined by the DataApi), then a - * flag will be set preventing the request from being trigerred for - * subsequent rating data. - * - * @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._maybeNotify( quote_id, request ); - } - - this.__super( request, data, actions, program, quote ); - }, - - - /** - * Perform notification if flag has not been set - * - * See #postProcessRaterData for more information. - * - * @param {number} quote_id effective quote/document id - * @param {UserRequest} request user request - * - * @return {undefined} - */ - 'private _maybeNotify'( quote_id, request ) - { - this._getNotifyState( quote_id, notified => - { - if ( notified === true ) - { - return; - } - - // make the request, only setting the notification flag if - // it is successful - this._dapif( request ) - .request( { quote_id: quote_id }, err => - { - err || this._setNotified( quote_id ); - } ); - } ); - }, - - - /** - * 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 - ); - }, -} ); diff --git a/test/server/service/RatingServiceSubmitNotifyTest.js b/test/server/service/RatingServiceSubmitNotifyTest.js deleted file mode 100644 index 1333eaf..0000000 --- a/test/server/service/RatingServiceSubmitNotifyTest.js +++ /dev/null @@ -1,186 +0,0 @@ -/** - * Tests RatingServiceSubmitNotify - * - * Copyright (C) 2010-2019 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 . - */ - -'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', () => -{ - [ - // not available; make successful request and save flag - { - prem_avail_count: [ 0 ], - prev_called: false, - expected_request: true, - request_err: null, - save: true, - }, - // not available; make failing request, don't save flag - { - prem_avail_count: [ 0 ], - prev_called: false, - expected_request: true, - request_err: Error(), - save: false, - }, - // available - { - prem_avail_count: [ 2 ], - prev_called: false, - expected_request: false, - request_err: null, - save: false, - }, - // this shouldn't happen; ignore all but first index - { - prem_avail_count: [ 2, 2 ], - prev_called: false, - expected_request: false, - request_err: null, - save: false, - }, - // save as above, but already saved - { - prem_avail_count: [ 0 ], - prev_called: true, - expected_request: false, - request_err: null, - save: false, - }, - // available; don't make request - { - prem_avail_count: [ 2 ], - prev_called: true, - expected_request: false, - request_err: null, - save: false, - }, - // this shouldn't happen; ignore all but first index - { - prem_avail_count: [ 2, 2 ], - prev_called: true, - expected_request: false, - request_err: null, - save: false, - }, - ].forEach( ( expected, 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, id ) - { - expect( given_request ).to.equal( request ); - expect( data ).to.deep.equal( { quote_id: quote_id } ); - - requested = true; - - callback( expected.request_err, null ); - }, - } )(); - - 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( expected.flag_error, expected.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 = expected.prem_avail_count; - - sut.request( request, response, quote, 'something', () => - { - expect( requested ).to.equal( expected.expected_request ); - expect( save_called ).to.be.true; - - // only save notification status if we're notifying - expect( notify_saved ).to.equal( expected.save ); - - done(); - } ); - } ) - ); -} ); From e2edbfc7b22fe8d04806b64ed049317f1a9014c4 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Mon, 21 Oct 2019 14:07:27 -0400 Subject: [PATCH 02/14] src/numeric: New module This beings to introduce compile-time safety for numeric values under the assumption that they are enforced by the runtime. See docblock for more information. --- src/numeric.ts | 43 ++++++++++++++++++++++++++++++++ test/numeric-test.ts | 59 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 src/numeric.ts create mode 100644 test/numeric-test.ts diff --git a/src/numeric.ts b/src/numeric.ts new file mode 100644 index 0000000..a86edef --- /dev/null +++ b/src/numeric.ts @@ -0,0 +1,43 @@ +/** + * Numeric types + * + * Copyright (C) 2010-2019 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 . + * + * TypeScript's type system does not support algebraic numeric domains. A + * compromise is to provide nominal types that allow developers to assume + * that some constraint has been met, and then ensure that the type is only + * ever asserted when that constraint is explicitly validated at + * runtime. This allows us to have compile-time checks on numeric values + * under the assumption that the runtime will enforce them. + * + * For this to work, _it is important to always use type predicates_; + * if you explicit cast to one of these numeric types, it circumvents the + * safety provided by the system and may introduce nasty bugs, since users + * of these types assume the provided data has already been validated. + */ + +/** + * Any number ≥ 0 + * + * This is useful for array indexing. + */ +export type PositiveInteger = NominalType; + + +/** Whether the given number is suitable as a PositiveInteger */ +export const isPositiveInteger = ( n: number ): n is PositiveInteger => n >= 0; diff --git a/test/numeric-test.ts b/test/numeric-test.ts new file mode 100644 index 0000000..8f07bb6 --- /dev/null +++ b/test/numeric-test.ts @@ -0,0 +1,59 @@ +/** + * Test numeric types + * + * Copyright (C) 2010-2019 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 . + */ + +import { expect } from 'chai'; +import { PositiveInteger, isPositiveInteger } from "../src/numeric"; + + +describe( 'isPositiveInteger', () => +{ + [ + 0, + 5, + ].forEach( value => it( `accepts positive integers (${value})`, () => + { + expect( isPositiveInteger( value ) ).to.be.true; + } ) ); + + + [ + -1, + -5, + ].forEach( value => it( `rejects negative integers (${value})`, () => + { + expect( isPositiveInteger( value ) ).to.be.false; + } ) ); + + + it( "asserts type PositiveInteger", () => + { + const n = 5; + + if ( isPositiveInteger( n ) ) + { + // TS should recognize as PositiveInteger within this block + checkPositiveInteger( n ); + } + } ); +} ); + + +const checkPositiveInteger = ( _n: PositiveInteger ): void => {}; From 5552de93d539d56ab38d41f1916fc37a4745cd30 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Mon, 21 Oct 2019 16:36:44 -0400 Subject: [PATCH 03/14] Rater: Remove interface It's used only by HttpRater and the interface is no longer adhered to by DslRater (which no longer implements it). --- src/server/rater/DslRater.js | 1 - src/server/rater/DslRaterContext.js | 1 - src/server/rater/HttpRater.js | 8 ++---- src/server/rater/Rater.js | 40 ----------------------------- 4 files changed, 2 insertions(+), 48 deletions(-) delete mode 100644 src/server/rater/Rater.js diff --git a/src/server/rater/DslRater.js b/src/server/rater/DslRater.js index 0437dbb..75bc1f1 100644 --- a/src/server/rater/DslRater.js +++ b/src/server/rater/DslRater.js @@ -20,7 +20,6 @@ */ var Class = require( 'easejs' ).Class, - Rater = require( './Rater' ), EventEmitter = require( 'events' ).EventEmitter, DslRaterContext = require( './DslRaterContext' ); diff --git a/src/server/rater/DslRaterContext.js b/src/server/rater/DslRaterContext.js index a9a837b..622eab1 100644 --- a/src/server/rater/DslRaterContext.js +++ b/src/server/rater/DslRaterContext.js @@ -20,7 +20,6 @@ */ var Class = require( 'easejs' ).Class, - Rater = require( './Rater' ), EventEmitter = require( 'events' ).EventEmitter, Quote = require( '../../quote/Quote' ); diff --git a/src/server/rater/HttpRater.js b/src/server/rater/HttpRater.js index 28f0095..ab7e600 100644 --- a/src/server/rater/HttpRater.js +++ b/src/server/rater/HttpRater.js @@ -22,18 +22,14 @@ * "HttpRater" */ -var Class = require( 'easejs' ).Class, - Rater = require( './Rater' ), - - querystring = require( 'querystring' ) -; +var Class = require( 'easejs' ).Class; +var querystring = require( 'querystring' ); /** * Rates using one of the PHP raters */ module.exports = Class( 'HttpRater' ) - .implement( Rater ) .extend( { /** diff --git a/src/server/rater/Rater.js b/src/server/rater/Rater.js deleted file mode 100644 index 3647960..0000000 --- a/src/server/rater/Rater.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Contains Rater interface - * - * Copyright (C) 2010-2019 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 . - */ - -var Interface = require( 'easejs' ).Interface; - - -/** - * Represents a rater that will generate a quote from a given set of values - */ -module.exports = Interface( 'Rater', -{ - /** - * Asynchronously performs rating using the data from the given bucket - * - * @param {Quote} quote to rate - * @param {function()} callback function to call when complete - * - * @return {Rater} self - */ - 'public rate': [ 'quote', 'args', 'callback' ], -} ); - From 2771cf2a71b3c599125b0ce7bc8d5450ea8998e8 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Tue, 22 Oct 2019 16:51:40 -0400 Subject: [PATCH 04/14] src/types/misc: Add UnixTimestampMillis, Seconds, and Milliseconds These should be moved into their own module at some point and provide functions to convert between and to yield these types. --- src/types/misc.d.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/types/misc.d.ts b/src/types/misc.d.ts index 83b11b5..5572739 100644 --- a/src/types/misc.d.ts +++ b/src/types/misc.d.ts @@ -38,12 +38,24 @@ type NominalType = K & { __nominal_type__: T }; +/** Unit of time in seconds */ +type Seconds = NominalType; + + /** * Unix timestamp * * Number of seconds since the Unix epoch (1970-01-01 UTC). */ -type UnixTimestamp = NominalType; +type UnixTimestamp = NominalType; + + +/** Unit of time in milliseconds */ +type Milliseconds = NominalType; + + +/** Unix timestamp represented in milliseconds */ +type UnixTimestampMillis = NominalType; /** From fa80c79650a31377f518a4a785693c8683e4dc0f Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Wed, 23 Oct 2019 10:33:50 -0400 Subject: [PATCH 05/14] doc/hacking.texi (Nominal Typing)[isPositiveInteger]: Fix arg name --- doc/hacking.texi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/hacking.texi b/doc/hacking.texi index 226899d..b445e10 100644 --- a/doc/hacking.texi +++ b/doc/hacking.texi @@ -446,7 +446,7 @@ This can be done using @verbatim type PositiveInteger = NominalType; -const isPositiveInteger = ( x: number ): n is PositiveInteger => n > 0; +const isPositiveInteger = ( n: number ): n is PositiveInteger => n > 0; const lookupIndex( arr: T[], i: PositiveInteger ): T => arr[ i ]; From 8f7afd22e50b1718e062c54701e258e5ca2ad715 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Wed, 23 Oct 2019 10:39:04 -0400 Subject: [PATCH 06/14] doc/hacking.texi (TypeScript Migration): .d.ts reference The original suggestion for interfaces (see diff) is much more confusing. We've found .d.ts files to work well for incremental migration. --- doc/hacking.texi | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/doc/hacking.texi b/doc/hacking.texi index b445e10..f848bb6 100644 --- a/doc/hacking.texi +++ b/doc/hacking.texi @@ -341,14 +341,17 @@ If this is a concern, in conjunction with ease.js' @url{https://www.gnu.org/software/easejs/manual/easejs.html#Type-Checks-and-Polymorphism,@samp{Class.isA}}. -Interfaces do not exist at runtime in TypeScript, - but they do in easejs. -Consequently, - you can continue to export an ease.js interface while also exporting - a TypeScript interface. +Often times you will need to reference a class or interface as a + dependency before it has been migrated away from ease.js. To do this, - continue to export using @samp{module.exports} rather than - TypeScript's @samp{export =}. + create a corresponding @code{.d.ts} file in the same directory + as the dependency. +For example, + if a class @code{Foo} is contained in @file{Foo.js}, + create a sibling @file{Foo.d.ts} file. +For more information, + see @url{https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html,Declaration Files} + in the TypeScript handbook. ease.js implements stackable Scala-like traits. Traits are @emph{not} provided by TypeScript. From 767a248e44db49ee3dee0b5131c2af00d264a93b Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Wed, 23 Oct 2019 12:09:27 -0400 Subject: [PATCH 07/14] RatingService: Convert to TypeScript This was an adventure, and was also used as a peer programming exercise to introduce TypeScript to other programmers in the office. This class has far too many dependencies, which made this difficult. The approach was to create .d.ts files for dependencies and wait on moving those over for now, otherwise the task will never get done. The RatingServicePublic trait was left as such for the time being; I was able to work around a bug that was making it difficult to mix it into a prototype. There were no logic changes; this was just type refactoring. --- src/client/action/ClientAction.ts | 39 +++ src/program/Program.d.ts | 27 ++ src/quote/BaseQuote.d.ts | 93 ++++++ src/quote/Quote.d.ts | 33 +++ src/server/Server.d.ts | 66 +++++ src/server/daemon/controller.js | 6 +- src/server/db/MongoServerDao.d.ts | 146 +++++++++ src/server/log/PriorityLog.d.ts | 45 +++ src/server/quote/ServerSideQuote.d.ts | 46 +++ src/server/rater/ProcessManager.d.ts | 35 +++ src/server/rater/ProcessManager.js | 3 + src/server/rater/Rater.d.ts | 121 ++++++++ src/server/request/UserRequest.d.ts | 36 +++ src/server/request/UserResponse.d.ts | 28 ++ src/server/request/UserSession.d.ts | 34 +++ .../{RatingService.js => RatingService.ts} | 280 ++++++++++++------ src/server/service/RatingServicePublish.js | 25 +- ...ingServiceTest.js => RatingServiceTest.ts} | 19 +- 18 files changed, 959 insertions(+), 123 deletions(-) create mode 100644 src/client/action/ClientAction.ts create mode 100644 src/program/Program.d.ts create mode 100644 src/quote/BaseQuote.d.ts create mode 100644 src/quote/Quote.d.ts create mode 100644 src/server/Server.d.ts create mode 100644 src/server/db/MongoServerDao.d.ts create mode 100644 src/server/log/PriorityLog.d.ts create mode 100644 src/server/quote/ServerSideQuote.d.ts create mode 100644 src/server/rater/ProcessManager.d.ts create mode 100644 src/server/rater/Rater.d.ts create mode 100644 src/server/request/UserRequest.d.ts create mode 100644 src/server/request/UserResponse.d.ts create mode 100644 src/server/request/UserSession.d.ts rename src/server/service/{RatingService.js => RatingService.ts} (57%) rename test/server/service/{RatingServiceTest.js => RatingServiceTest.ts} (82%) diff --git a/src/client/action/ClientAction.ts b/src/client/action/ClientAction.ts new file mode 100644 index 0000000..da57ebb --- /dev/null +++ b/src/client/action/ClientAction.ts @@ -0,0 +1,39 @@ +/** + * Representation of actions to be performed by the client + * + * Copyright (C) 2010-2019 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 . + */ + + +/** + * Action to be performed by the client + * + * TODO: More specific types + */ +export interface ClientAction +{ + /** Action to be performed */ + action: string; + + /** Action arguments */ + [P: string]: any; +} + + +/** Set of actions */ +export type ClientActions = ClientAction[]; diff --git a/src/program/Program.d.ts b/src/program/Program.d.ts new file mode 100644 index 0000000..de3f45a --- /dev/null +++ b/src/program/Program.d.ts @@ -0,0 +1,27 @@ +/** + * Contains Program base class + * + * Copyright (C) 2010-2019 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 . + */ + +export declare abstract class Program +{ + readonly ineligibleLockCount: number; + + getId(): string; +} diff --git a/src/quote/BaseQuote.d.ts b/src/quote/BaseQuote.d.ts new file mode 100644 index 0000000..9af47ee --- /dev/null +++ b/src/quote/BaseQuote.d.ts @@ -0,0 +1,93 @@ +/** + * Contains program Quote class + * + * Copyright (C) 2010-2019 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 . + * + * @todo Use ``document'' terminology in place of ``quote'' + */ + +import { Program } from "../program/Program"; +import { Quote, QuoteId } from "./Quote"; + + +export declare class BaseQuote implements Quote +{ + /** + * Retrieve Program associated with quote + * + * @return quote program + */ + getProgram(): Program; + + + /** + * Returns the program id associated with the quote + * + * @return program id + */ + getProgramId(): string; + + + /** + * Returns the quote id + * + * The quote id is immutable. A different quote id would represent a + * different quote, therefore a new object should be created with the + * desired quote id. + * + * @return quote id + */ + getId(): QuoteId; + + + /** + * Returns the id of the current step + * + * @return id of current step + */ + getCurrentStepId(): number; + + + /** + * Sets an explicit lock, providing a reason for doing so + * + * @param reason - lock reason + * @param step - step that user may not navigate prior + * + * @return self + */ + setExplicitLock( reason: string, step: number ): this; + + + /** + * Set the date that the premium was calculated as a Unix timestamp + * + * @param timestamp - Unix timestamp representing premium date + * + * @return self + */ + setLastPremiumDate( timestamp: UnixTimestamp ): this; + + + /** + * Retrieve the last time the premium was calculated + * + * @return last calculated time or 0 + */ + getLastPremiumDate(): UnixTimestamp; +} diff --git a/src/quote/Quote.d.ts b/src/quote/Quote.d.ts new file mode 100644 index 0000000..9324d0f --- /dev/null +++ b/src/quote/Quote.d.ts @@ -0,0 +1,33 @@ +/** + * Generic interface to represent quotes + * + * Copyright (C) 2010-2019 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 . + * + * @todo Use ``document'' terminology in place of ``quote'' + */ + +import { Program } from "../program/Program"; +import { QuoteId } from "../document/Document"; + + +export declare interface Quote +{ + // TODO: the easejs interface is empty; is this actually needed? +} + +export { QuoteId }; diff --git a/src/server/Server.d.ts b/src/server/Server.d.ts new file mode 100644 index 0000000..e165d69 --- /dev/null +++ b/src/server/Server.d.ts @@ -0,0 +1,66 @@ +/** + * General server actions + * + * Copyright (C) 2010-2019 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 . + */ + +import { ClientActions } from "../client/action/ClientAction"; +import { ServerSideQuote } from "./quote/ServerSideQuote"; +import { UserRequest } from "./request/UserRequest"; + + +/** + * General server actions + */ +export declare class Server +{ + /** + * Send response to user + * + * @param request - request to respond to + * @param quote - quote associated with request + * @param data - data with which to reply + * @param actions - optional client actions + * + * @return self + */ + sendResponse( + request: UserRequest, + quote: ServerSideQuote, + data: Record, + actions?: ClientActions, + ): this; + + + /** + * Send response to user + * + * @param request - request to respond to + * @param message - message to display to user + * @param actions - optional client actions + * @param btn_caption - optional caption for acknowledgement button + * + * @return self + */ + sendError( + request: UserRequest, + message: string, + actions?: ClientActions, + btn_caption?: string, + ): this; +} diff --git a/src/server/daemon/controller.js b/src/server/daemon/controller.js index 5b1b6b0..c027e4e 100644 --- a/src/server/daemon/controller.js +++ b/src/server/daemon/controller.js @@ -28,6 +28,8 @@ const { ReplSetServers: ReplSetServers, } = require( 'mongodb/lib/mongodb' ); +const easejs = require( 'easejs' ); + const regex_base = /^\/quote\/([a-z0-9-]+)\/?(?:\/(\d+)\/?(?:\/(.*))?|\/(program.js))?$/; const regex_step = /^step\/(\d+)\/?(?:\/(post|visit))?$/; @@ -83,7 +85,7 @@ const { ExportService, }, - RatingService, + RatingService: { RatingService }, RatingServicePublish, TokenedService, }, @@ -136,7 +138,7 @@ exports.init = function( logger, enc_service, conf ) server.init( server_cache, exports.rater ); // TODO: temporary proof-of-concept - rating_service = RatingService.use( + rating_service = easejs( RatingService ).use( RatingServicePublish( amqplib, exports.post_rate_publish, logger ) )( logger, dao, server, exports.rater diff --git a/src/server/db/MongoServerDao.d.ts b/src/server/db/MongoServerDao.d.ts new file mode 100644 index 0000000..fbf62c0 --- /dev/null +++ b/src/server/db/MongoServerDao.d.ts @@ -0,0 +1,146 @@ +/** + * Mongo DB DAO for program server + * + * Copyright (C) 2010-2019 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 . + */ + +import { ClassificationData, WorksheetData } from "../rater/Rater"; +import { PositiveInteger } from "../../numeric"; +import { QuoteId } from "../../document/Document"; +import { ServerSideQuote } from "../quote/ServerSideQuote"; + +/** Success or failure callback */ +type Callback = ( quote: ServerSideQuote ) => void; + + +/** + * MongoDB-backed data store + */ +export declare class MongoServerDao +{ + /** + * Saves a quote to the database + * + * A full save will include all metadata. + * + * @param quote - the quote to save + * @param success - function to call on success + * @param failure - function to call if save fails + * @param save_data - quote data to save (optional) + */ + saveQuote( + quote: ServerSideQuote, + success: Callback, + failure: Callback, + save_data: Record, + ): this; + + + /** + * Merges bucket data with the existing bucket (rather than overwriting the + * entire bucket) + * + * @param quote - quote to save + * @param data - bucket data + * @param success - successful callback + * @param failure - failure callback + */ + mergeBucket( + quote: ServerSideQuote, + data: Record, + success: Callback, + failure: Callback, + ): this; + + + /** + * Save quote classification data + * + * @param quote - quote to save + * @param classes - classification data + * @param success - successful callback + * @param failure - failure callback + */ + saveQuoteClasses( + quote: ServerSideQuote, + classes: ClassificationData, + success: Callback, + failure: Callback, + ): this; + + + /** + * Saves the quote state to the database + * + * The quote state includes the current step, the top visited step and the + * explicit lock message. + * + * @param quote - the quote to save + * @param success - function to call on success + * @param failure - function to call if save fails + */ + saveQuoteState( + quote: ServerSideQuote, + success: Callback, + failure: Callback, + ): this; + + + /** + * Saves the quote lock state to the database + * + * @param quote - the quote to save + * @param success - function to call on success + * @param failure - function to call if save fails + */ + saveQuoteLockState( + quote: ServerSideQuote, + success: Callback, + failure: Callback, + ): this + + + /** + * Save worksheet data + * + * @param qid - quote identifier + * @param data - worksheet data + * @param callback - callback + */ + setWorksheets( + qid: QuoteId, + data: WorksheetData, + callback: NodeCallback, + ): this; + + + /** + * Retrieve worksheet data + * + * @param qid - quote identifier + * @param supplier - supplier id + * @param index - worksheet index + * @param callback - callback + */ + getWorksheet( + qid: QuoteId, + supplier: string, + index: PositiveInteger, + callback: ( data: WorksheetData | null ) => void, + ): this; +} diff --git a/src/server/log/PriorityLog.d.ts b/src/server/log/PriorityLog.d.ts new file mode 100644 index 0000000..60819a4 --- /dev/null +++ b/src/server/log/PriorityLog.d.ts @@ -0,0 +1,45 @@ +/** + * Priority log typescript type definitions + * + * Copyright (C) 2010-2019 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 . + */ + +export declare interface PriorityLog +{ + readonly PRIORITY_ERROR: number; + readonly PRIORITY_IMPORTANT: number; + readonly PRIORITY_DB: number; + readonly PRIORITY_INFO: number; + readonly PRIORITY_SOCKET: number; + + /** + * Write to the log at the given priority + * + * If the priority is less than or equal to the set priority for this + * object, it will be logged. Otherwise, the message will be ignored. + * + * The first argument should be the priority. The remaining arguments should + * be provided in a sprintf()-style fashion + * + * @param priority - logging priority + * @param ...args - sprintf-style aruments + * + * @return self + */ + log( priority: number, ...args: Array ): this; +} diff --git a/src/server/quote/ServerSideQuote.d.ts b/src/server/quote/ServerSideQuote.d.ts new file mode 100644 index 0000000..98eff98 --- /dev/null +++ b/src/server/quote/ServerSideQuote.d.ts @@ -0,0 +1,46 @@ +/** + * Augments a quote with additional data for use by the quote server + * + * Copyright (C) 2010-2019 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 . + * + * @todo Use ``document'' terminology in place of ``quote'' + */ + +import { Program } from "../../program/Program"; +import { BaseQuote } from "../../quote/BaseQuote"; + + +export declare class ServerSideQuote extends BaseQuote +{ + /** + * Last rated date, if any + * + * @return last rated date + */ + getRatedDate(): UnixTimestamp; + + + /** + * Set the timestamp of the first time quote was rated + * + * @param timestamp - Unix timestamp representing first rated date + * + * @return self + */ + setRatedDate( timestamp: UnixTimestamp ): this; +} diff --git a/src/server/rater/ProcessManager.d.ts b/src/server/rater/ProcessManager.d.ts new file mode 100644 index 0000000..23d6705 --- /dev/null +++ b/src/server/rater/ProcessManager.d.ts @@ -0,0 +1,35 @@ +/** + * Rating process manager + * + * Copyright (C) 2010-2019 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 . + */ + +import { Rater } from "./Rater"; + + +export declare class ProcessManager +{ + /** + * Returns the rater associated with the given id + * + * @param id - rater id + * + * @return requested rater + */ + byId( id: string ): Rater; +} diff --git a/src/server/rater/ProcessManager.js b/src/server/rater/ProcessManager.js index a5a67b1..13c7fe2 100644 --- a/src/server/rater/ProcessManager.js +++ b/src/server/rater/ProcessManager.js @@ -69,6 +69,9 @@ const _signum = { * * Handles formatting and sending requests to the rating process; and * processing replies. + * + * TODO: Rename this class and provide a more generic interface. The caller + * does not care that this sends data to another process for rating. */ module.exports = Class( 'ProcessManager', { diff --git a/src/server/rater/Rater.d.ts b/src/server/rater/Rater.d.ts new file mode 100644 index 0000000..d9dcb1b --- /dev/null +++ b/src/server/rater/Rater.d.ts @@ -0,0 +1,121 @@ +/** + * Contains Rater interface + * + * Copyright (C) 2010-2019 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 . + */ + +import { ClientActions } from "../../client/action/ClientAction"; +import { ServerSideQuote } from "../quote/ServerSideQuote"; +import { UserSession } from "../request/UserSession"; + + + +/** Result of rating */ +export interface RateResult +{ + /** Whether all suppliers were not able to provide rates */ + _unavailable_all: '0' | '1'; + + /** Result data */ + [P: string]: any; +} + + +/** + * Result of rating with an individual supplier + * + * This gets combined into a single RateResult prefixed with each supplier + * id and an underscore. + */ +export interface SupplierRateResult +{ + /** Rating worksheet data */ + __worksheet?: WorksheetData, + + /** Classification system results */ + __classes?: ClassificationData, + + /** Basic profiling data */ + __perf: PerformanceData, + + /** Ineligible message, if any */ + ineligible: string, + + /** Submit message, if any */ + submit: string, + + /** Final premium */ + premium: number, + + /** Rating data */ + [P: string]: any; +} + + +/** Basic profiling data */ +export interface PerformanceData +{ + /** Timestamp of beginning of rating */ + start: UnixTimestampMillis; + + /** Timestamp of end of rating */ + end: UnixTimestampMillis; + + /** Total rating time */ + total: Milliseconds; +} + + +/** + * Worksheet data from rater + * + * These data come from the compiled raters. + * + * TODO: Fill in a schema here + */ +export type WorksheetData = Record; + + +/** Classification results */ +export type ClassificationData = Record; + + +/** + * Represents a rater that will generate a quote from a given set of values + */ +export interface Rater +{ + /** + * Asynchronously performs rating + * + * @param quote - quote to perform rating on + * @param session - user session + * @param indv - individual supplier to rate (otherwise empty) + * @param success - continuation when rating is successful + * @param failure - continuation when rating fails + * + * @return self + */ + rate( + quote: ServerSideQuote, + session: UserSession, + indv: string, + success: ( rate_data: RateResult, actions: ClientActions ) => void, + failure: ( message: string ) => void, + ): this; +} diff --git a/src/server/request/UserRequest.d.ts b/src/server/request/UserRequest.d.ts new file mode 100644 index 0000000..3449963 --- /dev/null +++ b/src/server/request/UserRequest.d.ts @@ -0,0 +1,36 @@ +/** + * User request abstraction + * + * Copyright (C) 2010-2019 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 . + */ + +import { UserSession } from "./UserSession"; + + +/** + * Representation of request from user + */ +export declare class UserRequest +{ + /** + * Retrieve the current session + * + * @return current session + */ + getSession(): UserSession; +} diff --git a/src/server/request/UserResponse.d.ts b/src/server/request/UserResponse.d.ts new file mode 100644 index 0000000..7ba776f --- /dev/null +++ b/src/server/request/UserResponse.d.ts @@ -0,0 +1,28 @@ +/** + * UserResponse class + * + * Copyright (C) 2010-2019 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 . + */ + + +/** + * Manipulates response to user request + */ +export declare class UserResponse +{ +} diff --git a/src/server/request/UserSession.d.ts b/src/server/request/UserSession.d.ts new file mode 100644 index 0000000..01937d7 --- /dev/null +++ b/src/server/request/UserSession.d.ts @@ -0,0 +1,34 @@ +/** + * UserSession class + * + * Copyright (C) 2010-2019 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 . + */ + + +/** + * Session management + */ +export declare class UserSession +{ + /** + * Whether the user is logged in as an internal user + * + * @return true if internal user, otherwise false + */ + isInternal(): boolean; +} diff --git a/src/server/service/RatingService.js b/src/server/service/RatingService.ts similarity index 57% rename from src/server/service/RatingService.js rename to src/server/service/RatingService.ts index aa9e53f..d0b33dc 100644 --- a/src/server/service/RatingService.js +++ b/src/server/service/RatingService.ts @@ -19,33 +19,60 @@ * along with this program. If not, see . */ -var Class = require( 'easejs' ).Class; +import { ClientActions } from "../../client/action/ClientAction"; +import { MongoServerDao } from "../db/MongoServerDao"; +import { PositiveInteger } from "../../numeric"; +import { PriorityLog } from "../log/PriorityLog"; +import { ProcessManager } from "../rater/ProcessManager"; +import { Program } from "../../program/Program"; +import { QuoteId } from "../../quote/Quote"; +import { ClassificationData, RateResult, WorksheetData } from "../rater/Rater"; +import { Server } from "../Server"; +import { ServerSideQuote } from "../quote/ServerSideQuote"; +import { UserRequest } from "../request/UserRequest"; +import { UserResponse } from "../request/UserResponse"; + +type RequestCallback = () => void; /** - * XXX: Half-assed, quick refactoring to extract from Server class; this is not - * yet complete! + * Handle rating requests + * + * XXX: This class was extracted from Server and needs additional + * refactoring, testing, and cleanup. * * TODO: Logging should be implemented by observers */ -module.exports = Class( 'RatingService', +export class RatingService { - logger: null, - - dao: null, - - _server: null, - - _raters: null, + /** + * Initialize rating service + * + * @param _logger - logging system + * @param _dao - database connection + * @param _server - server actions + * @param _rater_manager - rating manager + */ + constructor( + private readonly _logger: PriorityLog, + private readonly _dao: MongoServerDao, + private readonly _server: Server, + private readonly _rater_manager: ProcessManager, + ) {} - __construct: function( logger, dao, server, raters ) + /** + * TODO: Remove once traits subtypes are converted to TS + * + * This works around an easejs bug where prototype constructors are not + * properly invoked. Note that technically the constructor above is + * invoked twice by easejs: once with no arguments, and again when + * calling this method with the proper arguments. + */ + __construct() { - this._logger = logger; - this._dao = dao; - this._server = server; - this._raters = raters; - }, + (RatingService ).apply( this, arguments ); + } /** @@ -54,15 +81,21 @@ module.exports = Class( 'RatingService', * Note that the continuation will be called after all data saving is * complete; the request will be sent back to the client before then. * - * @param {UserRequest} request user request to satisfy - * @param {UserResponse} response pending response - * @param {Quote} quote quote to export - * @param {string} cmd applicable of command request - * @param {Function} callback continuation after saving is complete + * @param request - user request to satisfy + * @param _response - pending response + * @param quote - quote to export + * @param cmd - applicable of command request + * @param callback - continuation after saving is complete * * @return Server self to allow for method chaining */ - 'public request': function( request, response, quote, cmd, callback ) + request( + request: UserRequest, + _response: UserResponse, + quote: ServerSideQuote, + cmd: string, + callback: RequestCallback + ) { // cmd represents a request for a single rater if ( !cmd && this._isQuoteValid( quote ) ) @@ -89,12 +122,12 @@ module.exports = Class( 'RatingService', } return this; - }, + } - _getProgramRater: function( program, quote ) + private _getProgramRater( program: Program, quote: ServerSideQuote ) { - var rater = this._raters.byId( program.getId() ); + var rater = this._rater_manager.byId( program.getId() ); // if a rater could not be found, we can't do any rating if ( rater === null ) @@ -107,10 +140,20 @@ module.exports = Class( 'RatingService', } return rater; - }, + } - _isQuoteValid: function( quote ) + /** + * Whether quote is still valid + * + * TODO: This class shouldn't be making this determination, and this + * method is nondeterministic. + * + * @param quote - quote to check + * + * @return whether quote is still valid + */ + private _isQuoteValid( quote: ServerSideQuote ): boolean { // quotes are valid for 30 days var re_date = Math.round( ( ( new Date() ).getTime() / 1000 ) - @@ -129,14 +172,19 @@ module.exports = Class( 'RatingService', } return false; - }, + } - _performRating: function( request, program, quote, indv, c ) + private _performRating( + request: UserRequest, + program: Program, + quote: ServerSideQuote, + indv: string, + c: RequestCallback, + ) { - var _self = this; + var rater = this._getProgramRater( program, quote ); - var rater = this._getProgramRater( program ); if ( !rater ) { this._server.sendError( request, 'Unable to perform rating.' ); @@ -150,49 +198,51 @@ module.exports = Class( 'RatingService', ); rater.rate( quote, request.getSession(), indv, - function( rate_data, actions ) + ( rate_data: RateResult, actions: ClientActions ) => { actions = actions || []; - _self.postProcessRaterData( + this.postProcessRaterData( request, rate_data, actions, program, quote ); const class_dest = {}; - const cleaned = _self._cleanRateData( + const cleaned = this._cleanRateData( rate_data, class_dest ); // TODO: move me during refactoring - _self._dao.saveQuoteClasses( quote, class_dest ); + this._dao.saveQuoteClasses( + quote, class_dest, () => {}, () => {} + ); // save all data server-side (important: do after // post-processing); async - _self._saveRatingData( quote, rate_data, indv, function() + this._saveRatingData( quote, rate_data, indv, function() { // we're done c(); } ); // no need to wait for the save; send the response - _self._server.sendResponse( request, quote, { + this._server.sendResponse( request, quote, { data: cleaned, initialRatedDate: quote.getRatedDate(), lastRatedDate: quote.getLastPremiumDate() }, actions ); }, - function( message ) + ( message: string ) => { - _self._sendRatingError( request, quote, program, + this._sendRatingError( request, quote, program, Error( message ) ); c(); } ); - }, + } /** @@ -202,32 +252,32 @@ module.exports = Class( 'RatingService', * this is to allow us to reference the data (e.g. for reporting) even if * the client does not save it. * - * @param {Quote} quote quote to save data to - * @param {Object} data rating data - * - * @return {undefined} + * @param quote - quote to save data to + * @param data - rating data + * @param indv - individual supplier, or empty + * @param c - callback */ - _saveRatingData: function( quote, data, indv, c ) + private _saveRatingData( + quote: ServerSideQuote, + data: RateResult, + indv: string, + c: RequestCallback + ): void { // only update the last premium calc date on the initial request if ( !indv ) { - var cur_date = Math.round( + var cur_date = Math.round( ( new Date() ).getTime() / 1000 ); quote.setLastPremiumDate( cur_date ); quote.setRatedDate( cur_date ); - function done() - { - c(); - } - // save the last prem status (we pass an empty object as the save // data argument to ensure that we do not save the actual bucket // data, which may cause a race condition with the below merge call) - this._dao.saveQuote( quote, done, done, {} ); + this._dao.saveQuote( quote, c, c, {} ); } else { @@ -239,24 +289,26 @@ module.exports = Class( 'RatingService', // user a rate (if this save fails, it's likely we have bigger problems // anyway); this can also be done concurrently with the above request // since it only modifies a portion of the bucket - this._dao.mergeBucket( quote, data ); - }, + this._dao.mergeBucket( quote, data, () => {}, () => {} ); + } /** * Process rater data returned from a rater * - * @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} + * @param _request - user request to satisfy + * @param data - rating data returned + * @param actions - actions to send to client + * @param program - program used to perform rating + * @param quote - quote used for rating */ - 'virtual protected postProcessRaterData': function( - request, data, actions, program, quote - ) + protected postProcessRaterData( + _request: UserRequest, + data: RateResult, + actions: ClientActions, + program: Program, + quote: ServerSideQuote, + ): void { var meta = data._cmpdata || {}; @@ -284,18 +336,18 @@ module.exports = Class( 'RatingService', // have a race condition with async. rating (the /visit request may // be made while we're rating, and when we come back we would then // update the step id with a prior, incorrect step) - this._dao.saveQuoteLockState( quote ); + this._dao.saveQuoteLockState( quote, () => {}, () => {} ); } // if any have been deferred, instruct the client to request them // individually if ( Array.isArray( meta.deferred ) && ( meta.deferred.length > 0 ) ) { - var torate = []; + var torate: string[] = []; - meta.deferred.forEach( function( alias ) + meta.deferred.forEach( ( alias: string ) => { - actions.push( { action: 'indvRate', id: alias } ); + actions.push( { action: 'indvRate', after: alias } ); torate.push( alias ); } ); @@ -308,17 +360,29 @@ module.exports = Class( 'RatingService', torate.join( ',' ) ); } - }, + } - _sendRatingError: function( request, quote, program, err ) + /** + * Send rating error to user and log + * + * @param request - user request to satisfy + * @param quote - problem quote + * @param err - error + */ + private _sendRatingError( + request: UserRequest, + quote: ServerSideQuote, + program: Program, + err: Error, + ): void { // well that's no good this._logger.log( this._logger.PRIORITY_ERROR, "Rating for quote %d (program %s) failed: %s", quote.getId(), program.getId(), - err.message + '\n-!' + err.stack.replace( /\n/g, '\n-!' ) + err.message + '\n-!' + ( err.stack || "" ).replace( /\n/g, '\n-!' ) ); this._server.sendError( request, @@ -328,18 +392,25 @@ module.exports = Class( 'RatingService', // show details for internal users ( ( request.getSession().isInternal() ) ? '

[Internal] ' + err.message + '

' + - '
' + err.stack.replace( /\n/g, '
' ) + '
' + ( err.stack || "" ).replace( /\n/g, '
' ) : '' ) ); - }, + } - _processWorksheetData: function( qid, data ) + /** + * Process and save worksheet data from rating results + * + * @param qid - quote id + * @param data - rating result + */ + private _processWorksheetData( qid: QuoteId, data: RateResult ): void { // TODO: this should be done earlier on, so that this is not necessary - var wre = /^(.+)___worksheet$/, - worksheets = {}; + const wre = /^(.+)___worksheet$/; + + const worksheets: Record = {}; // extract worksheets for each supplier for ( var field in data ) @@ -354,33 +425,44 @@ module.exports = Class( 'RatingService', } } - var _self = this; - this._dao.setWorksheets( qid, worksheets, function( err ) + this._dao.setWorksheets( qid, worksheets, ( err: Error | null ) => { if ( err ) { - _self._logger.log( this._logger.PRIORITY_ERROR, + this._logger.log( this._logger.PRIORITY_ERROR, "Failed to save rating worksheets for quote %d", - quote.getId(), - err.message + '\n-!' + err.stack.replace( /\n/g, '\n-!' ) + qid, + err.message + '\n-!' + ( err.stack || "" ).replace( /\n/g, '\n-!' ) ); } } ); - }, + } - serveWorksheet: function( request, quote, supplier, index ) + /** + * Serve worksheet data to user + * + * @param request - user request to satisfy + * @param quote - quote from which to look up worksheet data + * @param supplier - supplier name + * @param index - worksheet index + */ + serveWorksheet( + request: UserRequest, + quote: ServerSideQuote, + supplier: string, + index: PositiveInteger, + ): void { - var qid = quote.getId(), - _self = this; + var qid = quote.getId(); - this._dao.getWorksheet( qid, supplier, index, function( data ) + this._dao.getWorksheet( qid, supplier, index, data => { - _self._server.sendResponse( request, quote, { + this._server.sendResponse( request, quote, { data: data } ); } ); - }, + } /** @@ -389,15 +471,18 @@ module.exports = Class( 'RatingService', * There are certain data saved server-side that there is no use serving to * the client. * - * @param {Object} data rate data + * @param data - rate data + * @param classes - classification data * - * @return {Object} modified rate data + * @return modified rate data */ - 'private _cleanRateData': function( data, classes ) + private _cleanRateData( + data: RateResult, + classes: ClassificationData + ): RateResult { - classes = classes || {}; - - var result = {}; + // forceful cast because the below loop will copy everything + const result = {}; // clear class data for ( var key in data ) @@ -415,6 +500,5 @@ module.exports = Class( 'RatingService', } return result; - }, -} ); - + } +} diff --git a/src/server/service/RatingServicePublish.js b/src/server/service/RatingServicePublish.js index fd5a539..ce843f0 100644 --- a/src/server/service/RatingServicePublish.js +++ b/src/server/service/RatingServicePublish.js @@ -21,8 +21,8 @@ 'use strict'; -const { Trait } = require( 'easejs' ); -const RatingService = require( './RatingService' ); +const { Interface, Trait } = require( 'easejs' ); +const { RatingService } = require( './RatingService' ); /** @@ -51,7 +51,8 @@ const RatingService = require( './RatingService' ); * See the body of `#_sendMessage' for their values. */ module.exports = Trait( 'RatingServicePublish' ) - .extend( RatingService, + .implement( Interface( { 'postProcessRaterData': [] } ) ) + .extend( { /** * AMQP library (amqplib API) @@ -75,7 +76,7 @@ module.exports = Trait( 'RatingServicePublish' ) * * @type {DebugLog} */ - 'private _logger': null, + 'private _log': null, /** @@ -87,9 +88,9 @@ module.exports = Trait( 'RatingServicePublish' ) */ __mixin( amqp, conf, logger ) { - this._amqp = amqp; - this._conf = conf; - this._logger = logger; + this._amqp = amqp; + this._conf = conf; + this._log = logger; }, @@ -104,7 +105,7 @@ module.exports = Trait( 'RatingServicePublish' ) * * @return {undefined} */ - 'override protected postProcessRaterData'( + 'abstract override postProcessRaterData'( request, data, actions, program, quote ) { @@ -127,13 +128,13 @@ module.exports = Trait( 'RatingServicePublish' ) quote ); } ) - .then( () => this._logger.log( - this._logger.PRIORITY_INFO, + .then( () => this._log.log( + this._log.PRIORITY_INFO, "Published quote " + quote.getId() + " to post-rate exchange '" + exchange + "'" ) ) - .catch( e => this._logger.log( - this._logger.PRIORITY_ERROR, + .catch( e => this._log.log( + this._log.PRIORITY_ERROR, "Post-rate exchange publish failure for quote " + quote.getId() + ": " + e.message ) ); diff --git a/test/server/service/RatingServiceTest.js b/test/server/service/RatingServiceTest.ts similarity index 82% rename from test/server/service/RatingServiceTest.js rename to test/server/service/RatingServiceTest.ts index b04c354..8330bea 100644 --- a/test/server/service/RatingServiceTest.js +++ b/test/server/service/RatingServiceTest.ts @@ -19,10 +19,9 @@ * along with this program. If not, see . */ -'use strict' +import { expect } from 'chai'; +import { RatingService as Sut } from "../../../src/server/service/RatingService"; -const { expect } = require( 'chai' ); -const Sut = require( '../../../' ).server.service.RatingService; const RatingServiceStub = require( '../../../' ).test.server.service.RatingServiceStub; describe( 'RatingService', () => @@ -49,15 +48,13 @@ describe( 'RatingService', () => done(); }; - const sut = Sut.extend( + const sut = new class extends Sut { - 'override postProcessRaterData'( - request, data, actions, program, quote - ) + postProcessRaterData() { processed = true; } - } )( logger, dao, server, raters ); + }( logger, dao, server, raters ); sut.request( request, response, quote, 'something', () => {} ); } ); @@ -87,9 +84,9 @@ describe( 'RatingService', () => quote.getRatedDate = () => initial_date; - const sut = Sut( logger, dao, server, raters ); + const sut = new Sut( logger, dao, server, raters ); - server.sendResponse = ( request, quote, resp, actions ) => + server.sendResponse = ( _request: any, _quote: any, resp: any, _actions: any ) => { expect( getLastPremiumDateCallCount ).to.equal( 2 ); expect( resp.initialRatedDate ).to.equal( initial_date ); @@ -98,7 +95,7 @@ describe( 'RatingService', () => done(); }; - sut.request( request, response, quote, null, () => {} ); + sut.request( request, response, quote, "", () => {} ); } ); } ); From 312142b3e8072771fba6f617cec04b5d62614152 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Thu, 24 Oct 2019 11:01:13 -0400 Subject: [PATCH 08/14] RatingService: Use ServerDao I had forgotten an interface exists, and this will be needed for the next commit. Unfortunately it requires a bit of duplication with MongoServerDao.d.ts, for now. --- src/server/db/MongoServerDao.d.ts | 7 +- src/server/db/ServerDao.d.ts | 149 ++++++++++++++++++ src/server/service/RatingService.ts | 6 +- ...ingServiceStub.js => RatingServiceStub.ts} | 0 4 files changed, 155 insertions(+), 7 deletions(-) create mode 100644 src/server/db/ServerDao.d.ts rename src/test/server/service/{RatingServiceStub.js => RatingServiceStub.ts} (100%) diff --git a/src/server/db/MongoServerDao.d.ts b/src/server/db/MongoServerDao.d.ts index fbf62c0..18492cc 100644 --- a/src/server/db/MongoServerDao.d.ts +++ b/src/server/db/MongoServerDao.d.ts @@ -19,19 +19,18 @@ * along with this program. If not, see . */ +import { ServerDao, Callback } from "./ServerDao"; + import { ClassificationData, WorksheetData } from "../rater/Rater"; import { PositiveInteger } from "../../numeric"; import { QuoteId } from "../../document/Document"; import { ServerSideQuote } from "../quote/ServerSideQuote"; -/** Success or failure callback */ -type Callback = ( quote: ServerSideQuote ) => void; - /** * MongoDB-backed data store */ -export declare class MongoServerDao +export declare class MongoServerDao implements ServerDao { /** * Saves a quote to the database diff --git a/src/server/db/ServerDao.d.ts b/src/server/db/ServerDao.d.ts new file mode 100644 index 0000000..9c8ce32 --- /dev/null +++ b/src/server/db/ServerDao.d.ts @@ -0,0 +1,149 @@ +/** + * General server database operations + * + * Copyright (C) 2010-2019 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 . + * + * This interface was created to satisfy MongoServerDao and may not be + * sufficiently general for other database abstractions. + */ + +import { ClassificationData, WorksheetData } from "../rater/Rater"; +import { PositiveInteger } from "../../numeric"; +import { QuoteId } from "../../document/Document"; +import { ServerSideQuote } from "../quote/ServerSideQuote"; + +/** Success or failure callback */ +export type Callback = ( quote: ServerSideQuote ) => void; + + +/** + * Database abstraction + */ +export interface ServerDao +{ + /** + * Saves a quote to the database + * + * A full save will include all metadata. + * + * @param quote - the quote to save + * @param success - function to call on success + * @param failure - function to call if save fails + * @param save_data - quote data to save (optional) + */ + saveQuote( + quote: ServerSideQuote, + success: Callback, + failure: Callback, + save_data: Record, + ): this; + + + /** + * Merges bucket data with the existing bucket (rather than overwriting the + * entire bucket) + * + * @param quote - quote to save + * @param data - bucket data + * @param success - successful callback + * @param failure - failure callback + */ + mergeBucket( + quote: ServerSideQuote, + data: Record, + success: Callback, + failure: Callback, + ): this; + + + /** + * Save quote classification data + * + * @param quote - quote to save + * @param classes - classification data + * @param success - successful callback + * @param failure - failure callback + */ + saveQuoteClasses( + quote: ServerSideQuote, + classes: ClassificationData, + success: Callback, + failure: Callback, + ): this; + + + /** + * Saves the quote state to the database + * + * The quote state includes the current step, the top visited step and the + * explicit lock message. + * + * @param quote - the quote to save + * @param success - function to call on success + * @param failure - function to call if save fails + */ + saveQuoteState( + quote: ServerSideQuote, + success: Callback, + failure: Callback, + ): this; + + + /** + * Saves the quote lock state to the database + * + * @param quote - the quote to save + * @param success - function to call on success + * @param failure - function to call if save fails + */ + saveQuoteLockState( + quote: ServerSideQuote, + success: Callback, + failure: Callback, + ): this + + + /** + * Save worksheet data + * + * @param qid - quote identifier + * @param data - worksheet data + * @param callback - callback + */ + setWorksheets( + qid: QuoteId, + data: WorksheetData, + callback: NodeCallback, + ): this; + + + /** + * Retrieve worksheet data + * + * @param qid - quote identifier + * @param supplier - supplier id + * @param index - worksheet index + * @param callback - callback + */ + getWorksheet( + qid: QuoteId, + supplier: string, + index: PositiveInteger, + callback: ( data: WorksheetData | null ) => void, + ): this; +} diff --git a/src/server/service/RatingService.ts b/src/server/service/RatingService.ts index d0b33dc..327e790 100644 --- a/src/server/service/RatingService.ts +++ b/src/server/service/RatingService.ts @@ -19,15 +19,15 @@ * along with this program. If not, see . */ +import { ClassificationData, RateResult, WorksheetData } from "../rater/Rater"; import { ClientActions } from "../../client/action/ClientAction"; -import { MongoServerDao } from "../db/MongoServerDao"; import { PositiveInteger } from "../../numeric"; import { PriorityLog } from "../log/PriorityLog"; import { ProcessManager } from "../rater/ProcessManager"; import { Program } from "../../program/Program"; import { QuoteId } from "../../quote/Quote"; -import { ClassificationData, RateResult, WorksheetData } from "../rater/Rater"; import { Server } from "../Server"; +import { ServerDao } from "../db/ServerDao"; import { ServerSideQuote } from "../quote/ServerSideQuote"; import { UserRequest } from "../request/UserRequest"; import { UserResponse } from "../request/UserResponse"; @@ -55,7 +55,7 @@ export class RatingService */ constructor( private readonly _logger: PriorityLog, - private readonly _dao: MongoServerDao, + private readonly _dao: ServerDao, private readonly _server: Server, private readonly _rater_manager: ProcessManager, ) {} diff --git a/src/test/server/service/RatingServiceStub.js b/src/test/server/service/RatingServiceStub.ts similarity index 100% rename from src/test/server/service/RatingServiceStub.js rename to src/test/server/service/RatingServiceStub.ts From cffd9ddeb0143d027ed7407a819552881b0fefb3 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Thu, 24 Oct 2019 12:07:23 -0400 Subject: [PATCH 09/14] RatingServiceTest: Add type information for stubs This also gets rid of the RatingServiceStub module, which is not used by anything else. I suspect that I originally added it to be shared by traits, but that's no longer going to be the case (and the only remaining trait is unfortunately untested atm, and will be going away). --- src/test/server/service/RatingServiceStub.ts | 92 ----------- test/server/service/RatingServiceTest.ts | 154 ++++++++++++++++++- 2 files changed, 149 insertions(+), 97 deletions(-) delete mode 100644 src/test/server/service/RatingServiceStub.ts diff --git a/src/test/server/service/RatingServiceStub.ts b/src/test/server/service/RatingServiceStub.ts deleted file mode 100644 index 4d2f285..0000000 --- a/src/test/server/service/RatingServiceStub.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Tests RatingService - * - * Copyright (C) 2010-2019 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 . - */ - -'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: () => {}, - saveQuote: () => {}, - }; - - const session = { - isInternal: () => false, - }; - - const request = { - getSession: () => session, - getSessionIdName: () => {}, - }; - const response = {}; - - const quote = { - getProgramId: () => program_id, - getProgram: () => program, - getId: () => 0, - setLastPremiumDate: () => {}, - setRatedDate: () => {}, - getRatedDate: () => 0, - getLastPremiumDate: () => 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, - }; -}; diff --git a/test/server/service/RatingServiceTest.ts b/test/server/service/RatingServiceTest.ts index 8330bea..dfee767 100644 --- a/test/server/service/RatingServiceTest.ts +++ b/test/server/service/RatingServiceTest.ts @@ -22,7 +22,19 @@ import { expect } from 'chai'; import { RatingService as Sut } from "../../../src/server/service/RatingService"; -const RatingServiceStub = require( '../../../' ).test.server.service.RatingServiceStub; +import { ClientActions } from "../../../src/client/action/ClientAction"; +import { PriorityLog } from "../../../src/server/log/PriorityLog"; +import { ProcessManager } from "../../../src/server/rater/ProcessManager"; +import { Program } from "../../../src/program/Program"; +import { QuoteId } from "../../../src/quote/Quote"; +import { Rater, RateResult } from "../../../src/server/rater/Rater"; +import { Server } from "../../../src/server/Server"; +import { ServerDao } from "../../../src/server/db/ServerDao"; +import { ServerSideQuote } from "../../../src/server/quote/ServerSideQuote"; +import { UserRequest } from "../../../src/server/request/UserRequest"; +import { UserResponse } from "../../../src/server/request/UserResponse"; +import { UserSession } from "../../../src/server/request/UserSession"; + describe( 'RatingService', () => { @@ -40,12 +52,14 @@ describe( 'RatingService', () => request, response, quote, - } = RatingServiceStub.getStubs(); + } = getStubs(); dao.mergeBucket = () => { expect( processed ).to.equal( true ); done(); + + return dao; }; const sut = new class extends Sut @@ -63,8 +77,8 @@ describe( 'RatingService', () => { let getLastPremiumDateCallCount = 0; - const last_date = 1234; - const initial_date = 2345; + const last_date = 1234; + const initial_date = 2345; const { logger, @@ -74,7 +88,7 @@ describe( 'RatingService', () => request, response, quote, - } = RatingServiceStub.getStubs(); + } = getStubs(); quote.getLastPremiumDate = () => { @@ -93,6 +107,8 @@ describe( 'RatingService', () => expect( resp.lastRatedDate ).to.equal( last_date ); done(); + + return server; }; sut.request( request, response, quote, "", () => {} ); @@ -100,3 +116,131 @@ describe( 'RatingService', () => } ); } ); + + +function getStubs() +{ + const program_id = 'foo'; + + const program = { + getId: () => program_id, + ineligibleLockCount: 0, + }; + + // rate reply + const stub_rate_data: RateResult = { + _unavailable_all: '0', + }; + + const rater = new class implements Rater + { + rate( + _quote: ServerSideQuote, + _session: UserSession, + _indv: string, + success: ( data: RateResult, actions: ClientActions ) => void, + ) + { + success( stub_rate_data, [] ); + return this; + } + }; + + const raters = { + byId: () => rater, + }; + + const logger = new class implements PriorityLog + { + readonly PRIORITY_ERROR: number = 0; + readonly PRIORITY_IMPORTANT: number = 1; + readonly PRIORITY_DB: number = 2; + readonly PRIORITY_INFO: number = 3; + readonly PRIORITY_SOCKET: number = 4; + + log(): this + { + return this; + } + }; + + const server = { + sendResponse: () => server, + sendError: () => server, + }; + + const dao = new class implements ServerDao + { + saveQuote(): this + { + return this; + } + + mergeBucket(): this + { + return this; + } + + saveQuoteClasses(): this + { + return this; + } + + setWorksheets(): this + { + return this; + } + + saveQuoteState(): this + { + throw new Error( "Unused method" ); + } + + saveQuoteLockState(): this + { + throw new Error( "Unused method" ); + } + + getWorksheet(): this + { + throw new Error( "Unused method" ); + } + }; + + const session = { + isInternal: () => false, + }; + + const request = { + getSession: () => session, + getSessionIdName: () => {}, + }; + + const response = {}; + + const quote = { + getProgramId: () => program_id, + getProgram: () => program, + getId: () => 0, + setLastPremiumDate: () => quote, + setRatedDate: () => quote, + getRatedDate: () => 0, + getLastPremiumDate: () => 0, + getCurrentStepId: () => 0, + setExplicitLock: () => quote, + }; + + 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, + }; +}; From 1aa69c2a56fe2ad46dfdf10061b9726c18f0e8d8 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Thu, 24 Oct 2019 14:34:11 -0400 Subject: [PATCH 10/14] RatingService: Save rating data to separate field (ratedata) This adds ratedata to the quote collection. We use the existing saveQuote method so that this operation is atomic. We're also continuing to save to the bucket for now so as not to break existing code, but the intent in the future will be to remove all but necessary data that should be exposed to the client. --- src/server/service/RatingService.ts | 4 +- test/server/service/RatingServiceTest.ts | 56 +++++++++++++++++++++++- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/src/server/service/RatingService.ts b/src/server/service/RatingService.ts index 327e790..e811075 100644 --- a/src/server/service/RatingService.ts +++ b/src/server/service/RatingService.ts @@ -277,7 +277,9 @@ export class RatingService // save the last prem status (we pass an empty object as the save // data argument to ensure that we do not save the actual bucket // data, which may cause a race condition with the below merge call) - this._dao.saveQuote( quote, c, c, {} ); + this._dao.saveQuote( quote, c, c, { + ratedata: data, + } ); } else { diff --git a/test/server/service/RatingServiceTest.ts b/test/server/service/RatingServiceTest.ts index dfee767..c40ea06 100644 --- a/test/server/service/RatingServiceTest.ts +++ b/test/server/service/RatingServiceTest.ts @@ -29,15 +29,61 @@ import { Program } from "../../../src/program/Program"; import { QuoteId } from "../../../src/quote/Quote"; import { Rater, RateResult } from "../../../src/server/rater/Rater"; import { Server } from "../../../src/server/Server"; -import { ServerDao } from "../../../src/server/db/ServerDao"; import { ServerSideQuote } from "../../../src/server/quote/ServerSideQuote"; import { UserRequest } from "../../../src/server/request/UserRequest"; import { UserResponse } from "../../../src/server/request/UserResponse"; import { UserSession } from "../../../src/server/request/UserSession"; +import { + ServerDao, + Callback as ServerDaoCallback +} from "../../../src/server/db/ServerDao"; + describe( 'RatingService', () => { + it( "saves rate data to own field", done => + { + const { + logger, + server, + raters, + dao, + request, + response, + quote, + stub_rate_data, + } = getStubs(); + + let saved_rates = false; + + dao.saveQuote = ( + quote: ServerSideQuote, + success: ServerDaoCallback, + _failure: ServerDaoCallback, + save_data: Record, + ) => + { + expect( save_data ).to.deep.equal( { + ratedata: stub_rate_data, + } ); + + saved_rates = true; + success( quote ); + + return dao; + }; + + const sut = new Sut( logger, dao, server, raters ); + + sut.request( request, response, quote, "", () => + { + expect( saved_rates ).to.be.true; + done(); + } ); + } ); + + describe( "protected API", () => { it( "calls #postProcessRaterData after rating before save", done => @@ -171,8 +217,14 @@ function getStubs() const dao = new class implements ServerDao { - saveQuote(): this + saveQuote( + quote: ServerSideQuote, + success: ServerDaoCallback, + _failure: ServerDaoCallback, + _save_data: Record, + ): this { + success( quote ); return this; } From 1ed8ad1cd716531a0ab573b61599a7baf8a96a9a Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Thu, 24 Oct 2019 15:10:16 -0400 Subject: [PATCH 11/14] RatingService: Return promise This only replaces the callbacks at the highest level and starts to move toward proper error handling. The private methods do not yet properly propagate errors. --- src/server/daemon/controller.js | 8 ++--- src/server/service/RatingService.ts | 32 ++++++++--------- test/server/service/RatingServiceTest.ts | 44 +++++++++++++++++++----- 3 files changed, 53 insertions(+), 31 deletions(-) diff --git a/src/server/daemon/controller.js b/src/server/daemon/controller.js index c027e4e..0f9cfa3 100644 --- a/src/server/daemon/controller.js +++ b/src/server/daemon/controller.js @@ -536,11 +536,9 @@ function doRoute( program, request, data, resolve, reject ) { var response = UserResponse( request ); - rating_service.request( request, response, quote, alias, function() - { - // we're done; free the lock - free(); - } ); + rating_service.request( request, response, quote, alias ) + .catch( () => {} ) + .then( () => free() ); } ); }, true ); } diff --git a/src/server/service/RatingService.ts b/src/server/service/RatingService.ts index e811075..9338758 100644 --- a/src/server/service/RatingService.ts +++ b/src/server/service/RatingService.ts @@ -78,24 +78,22 @@ export class RatingService /** * Sends rates to the client * - * Note that the continuation will be called after all data saving is + * Note that the promise will be resolved after all data saving is * complete; the request will be sent back to the client before then. * * @param request - user request to satisfy * @param _response - pending response * @param quote - quote to export * @param cmd - applicable of command request - * @param callback - continuation after saving is complete * - * @return Server self to allow for method chaining + * @return result promise */ request( request: UserRequest, _response: UserResponse, quote: ServerSideQuote, cmd: string, - callback: RequestCallback - ) + ): Promise { // cmd represents a request for a single rater if ( !cmd && this._isQuoteValid( quote ) ) @@ -105,23 +103,23 @@ export class RatingService data: {}, }, [] ); - callback(); - return this; + return Promise.resolve(); } var program = quote.getProgram(); - try + return new Promise( ( resolve, reject ) => { - this._performRating( request, program, quote, cmd, callback ); - } - catch ( err ) - { - this._sendRatingError( request, quote, program, err ); - callback(); - } - - return this; + try + { + this._performRating( request, program, quote, cmd, resolve ); + } + catch ( err ) + { + this._sendRatingError( request, quote, program, err ); + reject( err ); + } + } ); } diff --git a/test/server/service/RatingServiceTest.ts b/test/server/service/RatingServiceTest.ts index c40ea06..c9bb108 100644 --- a/test/server/service/RatingServiceTest.ts +++ b/test/server/service/RatingServiceTest.ts @@ -19,7 +19,6 @@ * along with this program. If not, see . */ -import { expect } from 'chai'; import { RatingService as Sut } from "../../../src/server/service/RatingService"; import { ClientActions } from "../../../src/client/action/ClientAction"; @@ -39,10 +38,13 @@ import { Callback as ServerDaoCallback } from "../../../src/server/db/ServerDao"; +import { expect, use as chai_use } from 'chai'; +chai_use( require( 'chai-as-promised' ) ); + describe( 'RatingService', () => { - it( "saves rate data to own field", done => + it( "saves rate data to own field", () => { const { logger, @@ -76,11 +78,35 @@ describe( 'RatingService', () => const sut = new Sut( logger, dao, server, raters ); - sut.request( request, response, quote, "", () => - { - expect( saved_rates ).to.be.true; - done(); - } ); + return sut.request( request, response, quote, "" ) + .then( () => + { + expect( saved_rates ).to.be.true; + } ); + } ); + + + it( "rejects with error", () => + { + const { + logger, + server, + raters, + dao, + request, + response, + quote, + rater, + } = getStubs(); + + const expected_error = new Error( "expected error" ); + + rater.rate = () => { throw expected_error; }; + + const sut = new Sut( logger, dao, server, raters ); + + return expect( sut.request( request, response, quote, "" ) ) + .to.eventually.rejectedWith( expected_error ); } ); @@ -116,7 +142,7 @@ describe( 'RatingService', () => } }( logger, dao, server, raters ); - sut.request( request, response, quote, 'something', () => {} ); + return sut.request( request, response, quote, 'something' ); } ); it( "calls getLastPremiumDate during #_performRating", done => @@ -157,7 +183,7 @@ describe( 'RatingService', () => return server; }; - sut.request( request, response, quote, "", () => {} ); + return sut.request( request, response, quote, "" ); } ); } ); From a23f2040ddfcb4b1fc93f8a004d9537a5ba15a10 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Thu, 24 Oct 2019 16:22:31 -0400 Subject: [PATCH 12/14] RatingService: Remove unreachable error check --- src/server/service/RatingService.ts | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/src/server/service/RatingService.ts b/src/server/service/RatingService.ts index 9338758..0bf4fc8 100644 --- a/src/server/service/RatingService.ts +++ b/src/server/service/RatingService.ts @@ -123,24 +123,6 @@ export class RatingService } - private _getProgramRater( program: Program, quote: ServerSideQuote ) - { - var rater = this._rater_manager.byId( program.getId() ); - - // if a rater could not be found, we can't do any rating - if ( rater === null ) - { - this._logger.log( this._logger.PRIORITY_ERROR, - "Rating for quote %d (program %s) failed; missing module", - quote.getId(), - program.getId() - ); - } - - return rater; - } - - /** * Whether quote is still valid * @@ -181,13 +163,7 @@ export class RatingService c: RequestCallback, ) { - var rater = this._getProgramRater( program, quote ); - - if ( !rater ) - { - this._server.sendError( request, 'Unable to perform rating.' ); - c(); - } + var rater = this._rater_manager.byId( program.getId() ); this._logger.log( this._logger.PRIORITY_INFO, "Performing '%s' rating for quote #%s", From 65e7880c81c4d3c1ce79f654b540bd2285257dda Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Mon, 28 Oct 2019 10:56:13 -0400 Subject: [PATCH 13/14] RatingService: Improved error handling This does only a slightly better job than before. --- src/server/service/RatingService.ts | 177 +++++++++++++---------- test/server/service/RatingServiceTest.ts | 111 ++++++++++++-- 2 files changed, 201 insertions(+), 87 deletions(-) diff --git a/src/server/service/RatingService.ts b/src/server/service/RatingService.ts index 0bf4fc8..690dcb2 100644 --- a/src/server/service/RatingService.ts +++ b/src/server/service/RatingService.ts @@ -34,6 +34,13 @@ import { UserResponse } from "../request/UserResponse"; type RequestCallback = () => void; +/** Result of rating */ +export type RateRequestResult = { + data: RateResult, + initialRatedDate: UnixTimestamp, + lastRatedDate: UnixTimestamp, +}; + /** * Handle rating requests @@ -93,32 +100,38 @@ export class RatingService _response: UserResponse, quote: ServerSideQuote, cmd: string, - ): Promise + ): Promise { - // cmd represents a request for a single rater - if ( !cmd && this._isQuoteValid( quote ) ) + const program = quote.getProgram(); + + return new Promise( resolve => { - // send an empty reply (keeps what is currently in the bucket) - this._server.sendResponse( request, quote, { - data: {}, - }, [] ); + // cmd represents a request for a single rater + if ( !cmd && this._isQuoteValid( quote ) ) + { + // send an empty reply (keeps what is currently in the + // bucket) + this._server.sendResponse( request, quote, { + data: {}, + }, [] ); - return Promise.resolve(); - } + // XXX: When this class is no longer responsible for + // sending the response to the server, this below data needs + // to represent the _current_ values, since as it is written + // now, it'll overwrite what is currently in the bucket + return resolve( { + data: { _unavailable_all: '0' }, + initialRatedDate: 0, + lastRatedDate: 0, + } ); + } - var program = quote.getProgram(); - - return new Promise( ( resolve, reject ) => + resolve( this._performRating( request, program, quote, cmd ) ); + } ) + .catch( err => { - try - { - this._performRating( request, program, quote, cmd, resolve ); - } - catch ( err ) - { - this._sendRatingError( request, quote, program, err ); - reject( err ); - } + this._sendRatingError( request, quote, program, err ); + throw err; } ); } @@ -155,67 +168,81 @@ export class RatingService } + /** + * Perform rating and process result + * + * @param request - user request to satisfy + * @param program - quote program + * @param quote - quote to process + * @param indv - individual supplier to rate (or empty) + * + * @return promise for results of rating + */ private _performRating( - request: UserRequest, - program: Program, - quote: ServerSideQuote, - indv: string, - c: RequestCallback, - ) + request: UserRequest, + program: Program, + quote: ServerSideQuote, + indv: string, + ): Promise { - var rater = this._rater_manager.byId( program.getId() ); + return new Promise( ( resolve, reject ) => + { + var rater = this._rater_manager.byId( program.getId() ); - this._logger.log( this._logger.PRIORITY_INFO, - "Performing '%s' rating for quote #%s", - quote.getProgramId(), - quote.getId() - ); + this._logger.log( this._logger.PRIORITY_INFO, + "Performing '%s' rating for quote #%s", + quote.getProgramId(), + quote.getId() + ); - rater.rate( quote, request.getSession(), indv, - ( rate_data: RateResult, actions: ClientActions ) => - { - actions = actions || []; - - this.postProcessRaterData( - request, rate_data, actions, program, quote - ); - - const class_dest = {}; - - const cleaned = this._cleanRateData( - rate_data, - class_dest - ); - - // TODO: move me during refactoring - this._dao.saveQuoteClasses( - quote, class_dest, () => {}, () => {} - ); - - // save all data server-side (important: do after - // post-processing); async - this._saveRatingData( quote, rate_data, indv, function() + rater.rate( quote, request.getSession(), indv, + ( rate_data: RateResult, actions: ClientActions ) => { - // we're done - c(); - } ); + actions = actions || []; - // no need to wait for the save; send the response - this._server.sendResponse( request, quote, { - data: cleaned, - initialRatedDate: quote.getRatedDate(), - lastRatedDate: quote.getLastPremiumDate() - }, actions ); - }, - ( message: string ) => - { - this._sendRatingError( request, quote, program, - Error( message ) - ); + this.postProcessRaterData( + request, rate_data, actions, program, quote + ); - c(); - } - ); + const class_dest = {}; + + const cleaned = this._cleanRateData( + rate_data, + class_dest + ); + + // TODO: move me during refactoring + this._dao.saveQuoteClasses( + quote, class_dest, () => {}, () => {} + ); + + const result = { + data: cleaned, + initialRatedDate: quote.getRatedDate(), + lastRatedDate: quote.getLastPremiumDate() + }; + + // save all data server-side (important: do after + // post-processing); async + this._saveRatingData( quote, rate_data, indv, function() + { + // we're done + resolve( result ); + } ); + + // no need to wait for the save; send the response + this._server.sendResponse( request, quote, result, actions ); + }, + ( message: string ) => + { + this._sendRatingError( request, quote, program, + Error( message ) + ); + + reject( Error( message ) ); + } + ); + } ); } diff --git a/test/server/service/RatingServiceTest.ts b/test/server/service/RatingServiceTest.ts index c9bb108..fd659d6 100644 --- a/test/server/service/RatingServiceTest.ts +++ b/test/server/service/RatingServiceTest.ts @@ -44,6 +44,31 @@ chai_use( require( 'chai-as-promised' ) ); describe( 'RatingService', () => { + it( "returns rating results", () => + { + const { + logger, + server, + raters, + dao, + request, + response, + quote, + stub_rate_data, + } = getStubs(); + + const sut = new Sut( logger, dao, server, raters ); + + const expected = { + data: stub_rate_data, + initialRatedDate: quote.getRatedDate(), + lastRatedDate: quote.getLastPremiumDate(), + }; + + return expect( sut.request( request, response, quote, "" ) ) + .to.eventually.deep.equal( expected ); + } ); + it( "saves rate data to own field", () => { const { @@ -86,17 +111,18 @@ describe( 'RatingService', () => } ); - it( "rejects with error", () => + it( "rejects and responds with error", () => { const { - logger, - server, - raters, dao, - request, - response, + logger, + program, quote, rater, + raters, + request, + response, + server, } = getStubs(); const expected_error = new Error( "expected error" ); @@ -105,8 +131,66 @@ describe( 'RatingService', () => const sut = new Sut( logger, dao, server, raters ); + let logged = false; + + logger.log = function( + priority: number, + _format: string, + qid: QuoteId, + program_id: string, + message: string, + ) + { + if ( typeof message === 'string' ) + { + expect( priority ).to.equal( logger.PRIORITY_ERROR ); + expect( qid ).to.equal( quote.getId() ); + expect( program_id ).to.equal( program.getId() ); + expect( message ).to.contain( expected_error.message ); + + logged = true; + } + + return logger; + }; + return expect( sut.request( request, response, quote, "" ) ) - .to.eventually.rejectedWith( expected_error ); + .to.eventually.rejectedWith( expected_error ) + .then( () => expect( logged ).to.be.true ); + } ); + + + it( "returns error message from rater", () => + { + const { + dao, + logger, + quote, + rater, + raters, + request, + response, + server, + } = getStubs(); + + const expected_message = 'expected foo'; + + const sut = new Sut( logger, dao, server, raters ); + + rater.rate = ( + _quote: ServerSideQuote, + _session: UserSession, + _indv: string, + _success: ( data: RateResult, actions: ClientActions ) => void, + failure: ( message: string ) => void, + ) => + { + failure( expected_message ); + return rater; + }; + + return expect( sut.request( request, response, quote, "" ) ) + .to.eventually.rejectedWith( Error, expected_message ); } ); @@ -142,7 +226,7 @@ describe( 'RatingService', () => } }( logger, dao, server, raters ); - return sut.request( request, response, quote, 'something' ); + sut.request( request, response, quote, 'something' ); } ); it( "calls getLastPremiumDate during #_performRating", done => @@ -183,9 +267,8 @@ describe( 'RatingService', () => return server; }; - return sut.request( request, response, quote, "" ); + sut.request( request, response, quote, "" ); } ); - } ); } ); @@ -211,9 +294,13 @@ function getStubs() _session: UserSession, _indv: string, success: ( data: RateResult, actions: ClientActions ) => void, + _failure: ( message: string ) => void, ) { - success( stub_rate_data, [] ); + // force to be async so that the tests resemble how the code + // actually runs + process.nextTick( () => success( stub_rate_data, [] ) ); + return this; } }; @@ -230,7 +317,7 @@ function getStubs() readonly PRIORITY_INFO: number = 3; readonly PRIORITY_SOCKET: number = 4; - log(): this + log( _priority: number, ..._args: Array ): this { return this; } From cefd6e95cba8c7358ccc9eb044471dbfb40fdc43 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Tue, 29 Oct 2019 11:43:33 -0400 Subject: [PATCH 14/14] RatingService: Remove unnecessary program param It was kept for postProcessRaterData for now since that would involve modifying a subtype as well. --- src/server/service/RatingService.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/server/service/RatingService.ts b/src/server/service/RatingService.ts index 690dcb2..df27f31 100644 --- a/src/server/service/RatingService.ts +++ b/src/server/service/RatingService.ts @@ -102,8 +102,6 @@ export class RatingService cmd: string, ): Promise { - const program = quote.getProgram(); - return new Promise( resolve => { // cmd represents a request for a single rater @@ -126,11 +124,11 @@ export class RatingService } ); } - resolve( this._performRating( request, program, quote, cmd ) ); + resolve( this._performRating( request, quote, cmd ) ); } ) .catch( err => { - this._sendRatingError( request, quote, program, err ); + this._sendRatingError( request, quote, err ); throw err; } ); } @@ -172,7 +170,6 @@ export class RatingService * Perform rating and process result * * @param request - user request to satisfy - * @param program - quote program * @param quote - quote to process * @param indv - individual supplier to rate (or empty) * @@ -180,14 +177,13 @@ export class RatingService */ private _performRating( request: UserRequest, - program: Program, quote: ServerSideQuote, indv: string, ): Promise { return new Promise( ( resolve, reject ) => { - var rater = this._rater_manager.byId( program.getId() ); + const rater = this._rater_manager.byId( quote.getProgramId() ); this._logger.log( this._logger.PRIORITY_INFO, "Performing '%s' rating for quote #%s", @@ -201,7 +197,7 @@ export class RatingService actions = actions || []; this.postProcessRaterData( - request, rate_data, actions, program, quote + request, rate_data, actions, quote.getProgram(), quote ); const class_dest = {}; @@ -235,7 +231,7 @@ export class RatingService }, ( message: string ) => { - this._sendRatingError( request, quote, program, + this._sendRatingError( request, quote, Error( message ) ); @@ -376,7 +372,6 @@ export class RatingService private _sendRatingError( request: UserRequest, quote: ServerSideQuote, - program: Program, err: Error, ): void { @@ -384,7 +379,7 @@ export class RatingService this._logger.log( this._logger.PRIORITY_ERROR, "Rating for quote %d (program %s) failed: %s", quote.getId(), - program.getId(), + quote.getProgramId(), err.message + '\n-!' + ( err.stack || "" ).replace( /\n/g, '\n-!' ) );