diff --git a/src/quote/BaseQuote.d.ts b/src/quote/BaseQuote.d.ts index f2271f4..8273ad1 100644 --- a/src/quote/BaseQuote.d.ts +++ b/src/quote/BaseQuote.d.ts @@ -24,6 +24,7 @@ import { Program } from "../program/Program"; import { Quote, QuoteId } from "./Quote"; import { QuoteDataBucket } from "../bucket/QuoteDataBucket"; +import { PositiveInteger } from "../numeric"; export declare class BaseQuote implements Quote @@ -33,7 +34,7 @@ export declare class BaseQuote implements Quote * * @return quote program */ - getProgram(): Program; + getProgram(): Program /** @@ -41,7 +42,7 @@ export declare class BaseQuote implements Quote * * @return program id */ - getProgramId(): string; + getProgramId(): string /** @@ -53,7 +54,7 @@ export declare class BaseQuote implements Quote * * @return quote id */ - getId(): QuoteId; + getId(): QuoteId /** @@ -61,7 +62,7 @@ export declare class BaseQuote implements Quote * * @return id of current step */ - getCurrentStepId(): number; + getCurrentStepId(): number /** @@ -72,7 +73,7 @@ export declare class BaseQuote implements Quote * * @return self */ - setExplicitLock( reason: string, step: number ): this; + setExplicitLock( reason: string, step: number ): this /** @@ -82,7 +83,7 @@ export declare class BaseQuote implements Quote * * @return self */ - setLastPremiumDate( timestamp: UnixTimestamp ): this; + setLastPremiumDate( timestamp: UnixTimestamp ): this /** @@ -90,7 +91,7 @@ export declare class BaseQuote implements Quote * * @return last calculated time or 0 */ - getLastPremiumDate(): UnixTimestamp; + getLastPremiumDate(): UnixTimestamp /** @@ -99,4 +100,54 @@ export declare class BaseQuote implements Quote * @return the data bucket */ getBucket(): QuoteDataBucket + + + /** + * Retrieves the reason for an explicit lock + * + * @return lock reason + */ + getExplicitLockReason(): string + + + /** + * Returns the maximum step to which the explicit lock applies + * + * If no step restriction is set, then 0 will be returned. + * + * @return {number} locked max step or 0 if not applicable + */ + getExplicitLockStep(): PositiveInteger + + + /** + * Returns whether the quote has been imported + * + * @return true if imported, otherwise false + */ + isImported(): boolean + + + /** + * Returns whether the quote has been bound + * + * @return true if bound, otherwise false + */ + isBound(): boolean + + + /** + * Returns the id of the highest step the quote has reached + * + * @return top visited step id + */ + getTopVisitedStepId(): PositiveInteger + + + /** + * Returns the id of the highest step the quote has saved + * + * @return top saved step id + */ + getTopSavedStepId(): PositiveInteger } diff --git a/src/server/daemon/controller.js b/src/server/daemon/controller.js index 06b7a5e..116ed39 100644 --- a/src/server/daemon/controller.js +++ b/src/server/daemon/controller.js @@ -69,7 +69,7 @@ const { DocumentServer, db: { - MongoServerDao, + MongoServerDao: { MongoServerDao }, }, lock: { @@ -126,8 +126,8 @@ exports.post_rate_publish = {}; exports.init = function( logger, enc_service, conf ) { - var db = _createDB( logger ); - const dao = MongoServerDao( db ); + var db = _createDB( logger ); + const dao = new MongoServerDao( db ); db.collection( 'quotes', function( err, collection ) { diff --git a/src/server/db/MongoServerDao.js b/src/server/db/MongoServerDao.js index db6140e..34878c4 100644 --- a/src/server/db/MongoServerDao.js +++ b/src/server/db/MongoServerDao.js @@ -1,3 +1,4 @@ +"use strict"; /** * Mongo DB DAO for program server * @@ -18,86 +19,46 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ - -var Class = require( 'easejs' ).Class, - EventEmitter = require( '../../events' ).EventEmitter, - ServerDao = require( './ServerDao' ).ServerDao; - - +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +var EventEmitter = require('events').EventEmitter; /** * Uses MongoDB as a data store */ -module.exports = Class( 'MongoServerDao' ) - .implement( ServerDao ) - .extend( EventEmitter, -{ - /** - * Collection used to store quotes - * @type String - */ - 'const COLLECTION': 'quotes', - - /** - * Sequence (auto-increment) collection - * @type {string} - */ - 'const COLLECTION_SEQ': 'seq', - - /** - * Sequence key for quote ids - * - * @type {string} - * @const - */ - 'const SEQ_QUOTE_ID': 'quoteId', - - /** - * Sequence quoteId default - * - * @type {number} - * @const - */ - 'const SEQ_QUOTE_ID_DEFAULT': 200000, - - - /** - * Database instance - * @type Mongo.Db - */ - 'private _db': null, - - /** - * Whether the DAO is initialized and ready to be used - * @type Boolean - */ - 'private _ready': false, - - /** - * Collection to save data to - * @type null|Collection - */ - 'private _collection': null, - - /** - * Collection to read sequences (auto-increments) from - * @type {null|Collection} - */ - 'private _seqCollection': null, - - +var MongoServerDao = /** @class */ (function (_super) { + __extends(MongoServerDao, _super); /** * Initializes DAO * * @param {Mongo.Db} db mongo database connection - * - * @return undefined */ - 'public __construct': function( db ) - { - this._db = db; - }, - - + function MongoServerDao(_db) { + var _this = _super.call(this) || this; + _this._db = _db; + /** Collection used to store quotes */ + _this.COLLECTION = 'quotes'; + /** Sequence (auto-increment) collection */ + _this.COLLECTION_SEQ = 'seq'; + /** Sequence key for quote ids */ + _this.SEQ_QUOTE_ID = 'quoteId'; + /** Sequence quoteId default */ + _this.SEQ_QUOTE_ID_DEFAULT = 200000; + /** Whether the DAO is initialized and ready to be used */ + _this._ready = false; + return _this; + } /** * Initializes error events and attempts to connect to the database * @@ -108,24 +69,17 @@ module.exports = Class( 'MongoServerDao' ) * * @return MongoServerDao self to allow for method chaining */ - 'public init': function( callback ) - { + MongoServerDao.prototype.init = function (callback) { var dao = this; - // map db error event (on connection error) to our connectError event - this._db.on( 'error', function( err ) - { - dao._ready = false; + this._db.on('error', function (err) { + dao._ready = false; dao._collection = null; - - dao.emit( 'connectError', err ); + dao.emit('connectError', err); }); - - this.connect( callback ); + this.connect(callback); return this; - }, - - + }; /** * Attempts to connect to the database * @@ -136,138 +90,88 @@ module.exports = Class( 'MongoServerDao' ) * * @return MongoServerDao self to allow for method chaining */ - 'public connect': function( callback ) - { + MongoServerDao.prototype.connect = function (callback) { var dao = this; - // attempt to connect to the database - this._db.open( function( err, db ) - { + this._db.open(function (err, db) { // if there was an error, don't bother with anything else - if ( err ) - { + if (err) { // in some circumstances, it may just be telling us that we're // already connected (even though the connection may have been // broken) - if ( err.errno !== undefined ) - { - dao.emit( 'connectError', err ); + if (err.errno !== undefined) { + dao.emit('connectError', err); return; } } - var ready_count = 0; - var check_ready = function() - { - if ( ++ready_count < 2 ) - { + var check_ready = function () { + if (++ready_count < 2) { return; } - // we're ready to roll! dao._ready = true; - dao.emit( 'ready' ); - + dao.emit('ready'); // connection was successful; call the callback - if ( callback instanceof Function ) - { - callback.call( dao ); + if (callback instanceof Function) { + callback.call(dao); } - } - + }; // quotes collection - db.collection( dao.__self.$('COLLECTION'), function( err, collection ) - { + db.collection(dao.COLLECTION, function (_err, collection) { // for some reason this gets called more than once - if ( collection == null ) - { + if (collection == null) { return; } - // initialize indexes - collection.createIndex( - [ ['id', 1] ], - true, - function( err, index ) - { - // mark the DAO as ready to be used - dao._collection = collection; - check_ready(); - } - ); + collection.createIndex([['id', 1]], true, function (_err, _index) { + // mark the DAO as ready to be used + dao._collection = collection; + check_ready(); + }); }); - // seq collection - db.collection( dao.__self.$('COLLECTION_SEQ'), function( err, collection ) - { - if ( err ) - { - dao.emit( 'seqError', err ); + db.collection(dao.COLLECTION_SEQ, function (err, collection) { + if (err) { + dao.emit('seqError', err); return; } - - if ( collection == null ) - { + if (collection == null) { return; } - dao._seqCollection = collection; - // has the sequence we'll be referencing been initialized? - collection.find( - { _id: dao.__self.$('SEQ_QUOTE_ID') }, - { limit: 1 }, - function( err, cursor ) - { - if ( err ) - { - dao.initQuoteIdSeq( check_ready ) + collection.find({ _id: dao.SEQ_QUOTE_ID }, { limit: 1 }, function (err, cursor) { + if (err) { + dao._initQuoteIdSeq(check_ready); + return; + } + cursor.toArray(function (_err, data) { + if (data.length == 0) { + dao._initQuoteIdSeq(check_ready); return; } - - cursor.toArray( function( err, data ) - { - if ( data.length == 0 ) - { - dao.initQuoteIdSeq( check_ready ); - return; - } - - check_ready(); - }); - } - ); + check_ready(); + }); + }); }); }); - return this; - }, - - - 'public initQuoteIdSeq': function( callback ) - { + }; + MongoServerDao.prototype._initQuoteIdSeq = function (callback) { var dao = this; - - this._seqCollection.insert( - { - _id: this.__self.$('SEQ_QUOTE_ID'), - val: this.__self.$('SEQ_QUOTE_ID_DEFAULT'), - }, - function( err, docs ) - { - if ( err ) - { - dao.emit( 'seqError', err ); - return; - } - - dao.emit( 'seqInit', this.__self.$('SEQ_QUOTE_ID') ); - callback.call( this ); + this._seqCollection.insert({ + _id: this.SEQ_QUOTE_ID, + val: this.SEQ_QUOTE_ID_DEFAULT, + }, function (err, _docs) { + if (err) { + dao.emit('seqError', err); + return; } - ); - }, - - + dao.emit('seqInit', dao.SEQ_QUOTE_ID); + callback.call(dao); + }); + }; /** * Saves a quote to the database * @@ -281,109 +185,69 @@ module.exports = Class( 'MongoServerDao' ) * @param Function failure_callback function to call if save fails * @param Object save_data quote data to save (optional) * @param Object push_data quote data to push (optional) - * - * @return MongoServerDao self to allow for method chaining */ - 'public saveQuote': function( - quote, success_callback, failure_callback, save_data, push_data - ) - { - var dao = this; + MongoServerDao.prototype.saveQuote = function (quote, success_callback, failure_callback, save_data, push_data) { + var dao = this; var meta = {}; - // if we're not ready, then we can't save the quote! - if ( this._ready === false ) - { - this.emit( 'saveQuoteError', - { message: 'Database server not ready' }, - Error( 'Database not ready' ), - quote - ); - - failure_callback.call( this, quote ); - return; + if (this._ready === false) { + this.emit('saveQuoteError', { message: 'Database server not ready' }, Error('Database not ready'), quote); + failure_callback.call(this, quote); + return dao; } - - if ( save_data === undefined ) - { + if (save_data === undefined) { save_data = { data: quote.getBucket().getData(), }; - // full save will include all metadata meta = quote.getMetabucket().getData(); } - var id = quote.getId(); - // some data should always be saved because the quote will be created if // it does not yet exist - save_data.id = id; - save_data.pver = quote.getProgramVersion(); - save_data.importDirty = 1; - save_data.lastPremDate = quote.getLastPremiumDate(); - save_data.initialRatedDate = quote.getRatedDate(); - save_data.explicitLock = quote.getExplicitLockReason(); + save_data.id = id; + save_data.pver = quote.getProgramVersion(); + save_data.importDirty = 1; + save_data.lastPremDate = quote.getLastPremiumDate(); + save_data.initialRatedDate = quote.getRatedDate(); + save_data.explicitLock = quote.getExplicitLockReason(); save_data.explicitLockStepId = quote.getExplicitLockStep(); - save_data.importedInd = +quote.isImported(); - save_data.boundInd = +quote.isBound(); - save_data.lastUpdate = Math.round( - ( new Date() ).getTime() / 1000 - ); - + save_data.importedInd = +quote.isImported(); + save_data.boundInd = +quote.isBound(); + save_data.lastUpdate = Math.round((new Date()).getTime() / 1000); // meta will eventually take over for much of the above data - meta.liza_timestamp_initial_rated = [ quote.getRatedDate() ]; - + meta.liza_timestamp_initial_rated = [quote.getRatedDate()]; // save the stack so we can track this call via the oplog - save_data._stack = ( new Error() ).stack; - + save_data._stack = (new Error()).stack; // avoid wiping out other metadata (since this may not be a full set) - Object.keys( meta ).forEach( - key => save_data[ 'meta.' + key ] = meta[ key ] - ); - + Object.keys(meta).forEach(function (key) { return save_data['meta.' + key] = meta[key]; }); // do not push empty objects - const document = ( !push_data || !Object.keys( push_data ).length ) + var document = (!push_data || !Object.keys(push_data).length) ? { '$set': save_data } : { '$set': save_data, '$push': push_data }; - // update the quote data if it already exists (same id), otherwise // insert it - this._collection.update( { id: id }, - document, - - // create record if it does not yet exist - { upsert: true }, - - // on complete - function( err, docs ) - { - // if an error occurred, then we cannot continue - if ( err ) - { - dao.emit( 'saveQuoteError', err, quote ); - - // let the caller handle the error - if ( failure_callback instanceof Function ) - { - failure_callback.call( dao, quote ); - } - - return; - } - - // successful - if ( success_callback instanceof Function ) - { - success_callback.call( dao, quote ); + this._collection.update({ id: id }, document, + // create record if it does not yet exist + { upsert: true }, + // on complete + function (err, _docs) { + // if an error occurred, then we cannot continue + if (err) { + dao.emit('saveQuoteError', err, quote); + // let the caller handle the error + if (failure_callback instanceof Function) { + failure_callback.call(dao, quote); } + return; } - ); - + // successful + if (success_callback instanceof Function) { + success_callback.call(dao, quote); + } + }); return this; - }, - - + }; /** * Merges quote data with the existing (rather than overwriting) * @@ -391,45 +255,26 @@ module.exports = Class( 'MongoServerDao' ) * @param {Object} data quote data * @param {Function} scallback successful callback * @param {Function} fcallback failure callback - * - * @return {MongoServerDao} self */ - 'public mergeData': function( quote, data, scallback, fcallback ) - { + MongoServerDao.prototype.mergeData = function (quote, data, scallback, fcallback) { // we do not want to alter the original data; use it as a prototype var update = data; - // save the stack so we can track this call via the oplog var _self = this; - this._collection.update( { id: quote.getId() }, - { '$set': update }, - {}, - - function( err, docs ) - { - if ( err ) - { - _self.emit( 'saveQuoteError', err, quote ); - - if ( typeof fcallback === 'function' ) - { - fcallback( quote ); - } - - return; - } - - if ( typeof scallback === 'function' ) - { - scallback( quote ); + this._collection.update({ id: quote.getId() }, { '$set': update }, {}, function (err, _docs) { + if (err) { + _self.emit('saveQuoteError', err, quote); + if (typeof fcallback === 'function') { + fcallback(quote); } + return; } - ); - + if (typeof scallback === 'function') { + scallback(quote); + } + }); return this; - }, - - + }; /** * Merges bucket data with the existing bucket (rather than overwriting the * entire bucket) @@ -441,24 +286,16 @@ module.exports = Class( 'MongoServerDao' ) * * @return {MongoServerDao} self */ - 'public mergeBucket': function( quote, data, scallback, fcallback ) - { + MongoServerDao.prototype.mergeBucket = function (quote, data, success, failure) { var update = {}; - - for ( var field in data ) - { - if ( !field ) - { + for (var field in data) { + if (!field) { continue; } - - update[ 'data.' + field ] = data[ field ]; + update['data.' + field] = data[field]; } - - return this.mergeData( quote, update, scallback, fcallback ); - }, - - + return this.mergeData(quote, update, success, failure); + }; /** * Saves the quote state to the database * @@ -471,33 +308,17 @@ module.exports = Class( 'MongoServerDao' ) * * @return MongoServerDao self */ - 'public saveQuoteState': function( - quote, success_callback, failure_callback - ) - { + MongoServerDao.prototype.saveQuoteState = function (quote, success_callback, failure_callback) { var update = { - currentStepId: quote.getCurrentStepId(), + currentStepId: quote.getCurrentStepId(), topVisitedStepId: quote.getTopVisitedStepId(), - topSavedStepId: quote.getTopSavedStepId(), + topSavedStepId: quote.getTopSavedStepId(), }; - - return this.mergeData( - quote, update, success_callback, failure_callback - ); - }, - - - 'public saveQuoteClasses': function( quote, classes, success, failure ) - { - return this.mergeData( - quote, - { classData: classes }, - success, - failure - ); - }, - - + return this.mergeData(quote, update, success_callback, failure_callback); + }; + MongoServerDao.prototype.saveQuoteClasses = function (quote, classes, success, failure) { + return this.mergeData(quote, { classData: classes }, success, failure); + }; /** * Save document metadata (meta field on document) * @@ -511,25 +332,16 @@ module.exports = Class( 'MongoServerDao' ) * * @return {undefined} */ - 'public saveQuoteMeta'( quote, new_meta, success, failure ) - { - const update = {}; - - for ( var key in new_meta ) - { - var meta = new_meta[ key ]; - - for ( var i in meta ) - { - update[ 'meta.' + key + '.' + i ] = - new_meta[ key ][ i ]; + MongoServerDao.prototype.saveQuoteMeta = function (quote, new_meta, success, failure) { + var update = {}; + for (var key in new_meta) { + var meta = new_meta[key]; + for (var i in meta) { + update['meta.' + key + '.' + i] = new_meta[key][i]; } } - - this.mergeData( quote, update, success, failure ); - }, - - + this.mergeData(quote, update, success, failure); + }; /** * Saves the quote lock state to the database * @@ -539,15 +351,10 @@ module.exports = Class( 'MongoServerDao' ) * * @return MongoServerDao self */ - 'public saveQuoteLockState': function( - quote, success_callback, failure_callback - ) - { + MongoServerDao.prototype.saveQuoteLockState = function (quote, success_callback, failure_callback) { // lock state is saved by default - return this.saveQuote( quote, success_callback, failure_callback, {} ); - }, - - + return this.saveQuote(quote, success_callback, failure_callback, {}); + }; /** * Pulls quote data from the database * @@ -556,96 +363,54 @@ module.exports = Class( 'MongoServerDao' ) * * @return MongoServerDao self to allow for method chaining */ - 'public pullQuote': function( quote_id, callback ) - { + MongoServerDao.prototype.pullQuote = function (quote_id, callback) { var dao = this; - // XXX: TODO: Do not read whole of record into memory; filter out // revisions! - this._collection.find( { id: quote_id }, { limit: 1 }, - function( err, cursor ) - { - cursor.toArray( function( err, data ) - { - // was the quote found? - if ( data.length == 0 ) - { - callback.call( dao, null ); - return; - } - - // return the quote data - callback.call( dao, data[ 0 ] ); - }); - } - ); - - return this; - }, - - - 'public getMinQuoteId': function( callback ) - { - // just in case it's asynchronous later on - callback.call( this, this.__self.$('SEQ_QUOTE_ID_DEFAULT') ); - return this; - }, - - - 'public getMaxQuoteId': function( callback ) - { - var dao = this; - - this._seqCollection.find( - { _id: this.__self.$('SEQ_QUOTE_ID') }, - { limit: 1 }, - function( err, cursor ) - { - cursor.toArray( function( err, data ) - { - if ( data.length == 0 ) - { - callback.call( dao, 0 ); - return; - } - - // return the max quote id - callback.call( dao, data[ 0 ].val ); - }); - } - ); - }, - - - 'public getNextQuoteId': function( callback ) - { - var dao = this; - - this._seqCollection.findAndModify( - { _id: this.__self.$('SEQ_QUOTE_ID') }, - [ [ 'val', 'descending' ] ], - { $inc: { val: 1 } }, - { 'new': true }, - - function( err, doc ) - { - if ( err ) - { - dao.emit( 'seqError', err ); - - callback.call( dao, 0 ); + this._collection.find({ id: quote_id }, { limit: 1 }, function (_err, cursor) { + cursor.toArray(function (_err, data) { + // was the quote found? + if (data.length == 0) { + callback.call(dao, null); return; } - - // return the new id - callback.call( dao, doc.val ); - } - ); - + // return the quote data + callback.call(dao, data[0]); + }); + }); return this; - }, - - + }; + MongoServerDao.prototype.getMinQuoteId = function (callback) { + // just in case it's asynchronous later on + callback.call(this, this.SEQ_QUOTE_ID_DEFAULT); + return this; + }; + MongoServerDao.prototype.getMaxQuoteId = function (callback) { + var dao = this; + this._seqCollection.find({ _id: this.SEQ_QUOTE_ID }, { limit: 1 }, function (_err, cursor) { + cursor.toArray(function (_err, data) { + if (data.length == 0) { + callback.call(dao, 0); + return; + } + // return the max quote id + callback.call(dao, data[0].val); + }); + }); + }; + MongoServerDao.prototype.getNextQuoteId = function (callback) { + var dao = this; + this._seqCollection.findAndModify({ _id: this.SEQ_QUOTE_ID }, [['val', 'descending']], { $inc: { val: 1 } }, { 'new': true }, function (err, doc) { + if (err) { + dao.emit('seqError', err); + callback.call(dao, 0); + return; + } + // return the new id + callback.call(dao, doc.val); + }); + return this; + }; /** * Create a new revision with the provided quote data * @@ -654,170 +419,64 @@ module.exports = Class( 'MongoServerDao' ) * model of storing the deltas in previous revisions and the whole of the * bucket in the most recently created revision). */ - 'public createRevision': function( quote, callback ) - { - var _self = this, - qid = quote.getId(), - data = quote.getBucket().getData(); - - this._collection.update( { id: qid }, - { '$push': { revisions: { data: data } } }, - - // create record if it does not yet exist - { upsert: true }, - - // on complete - function( err ) - { - if ( err ) - { - _self.emit( 'mkrevError', err ); - } - - callback( err ); - return; + MongoServerDao.prototype.createRevision = function (quote, callback) { + var _self = this, qid = quote.getId(), data = quote.getBucket().getData(); + this._collection.update({ id: qid }, { '$push': { revisions: { data: data } } }, + // create record if it does not yet exist + { upsert: true }, + // on complete + function (err) { + if (err) { + _self.emit('mkrevError', err); } - ); - }, - - - 'public getRevision': function( quote, revid, callback ) - { + callback(err); + return; + }); + }; + MongoServerDao.prototype.getRevision = function (quote, revid, callback) { revid = +revid; - // XXX: TODO: Filter out all but the revision we want - this._collection.find( - { id: quote.getId() }, - { limit: 1 }, - function( err, cursor ) - { - cursor.toArray( function( err, data ) - { - // was the quote found? - if ( ( data.length === 0 ) - || ( data[ 0 ].revisions.length < ( revid + 1 ) ) - ) - { - callback( null ); - return; - } - - // return the quote data - callback( data[ 0 ].revisions[ revid ] ); - }); - } - ); - }, - - - 'public setWorksheets': function( qid, data, callback ) - { - this._collection.update( { id: qid }, - { '$set': { worksheets: { data: data } } }, - - // create record if it does not yet exist - { upsert: true }, - - // on complete - function( err ) - { - callback( err ); - return; - } - ); - }, - - - 'public getWorksheet': function( qid, supplier, index, callback ) - { - this._collection.find( - { id: qid }, - { limit: 1 }, - function( err, cursor ) - { - cursor.toArray( function( err, data ) - { - // was the quote found? - if ( ( data.length === 0 ) - || ( !data[ 0 ].worksheets ) - || ( !data[ 0 ].worksheets.data ) - || ( !data[ 0 ].worksheets.data[ supplier ] ) - ) - { - callback( null ); - return; - } - - // return the quote data - callback( data[ 0 ].worksheets.data[ supplier ][ index ] ); - }); - } - ); - }, - - - /** - * Set arbitrary data on a document - * - * @param {number} qid quote/document id - * @param {string} key field key - * @param {*} value field value - * @param {function(?Error)} callback completion callback - * - * @return {undefined} - */ - 'public setDocumentField'( qid, key, value, callback ) - { - this._collection.update( - { id: qid }, - { '$set': { [key]: value } }, - - // create record if it does not yet exist - { upsert: true }, - - // on complete - function( err ) - { - callback && callback( err ); - return; - } - ); - }, - - - /** - * Retrieve arbitrary data on a document - * - * @param {number} qid quote/document id - * @param {string} key field key - * @param {function(?Error)} callback completion callback - * - * @return {undefined} - */ - 'public getDocumentField'( qid, key, callback ) - { - this._collection.find( - { id: qid }, - { limit: 1 }, - function( err, cursor ) - { - if ( err !== null ) - { - callback( err, null ); + this._collection.find({ id: quote.getId() }, { limit: 1 }, function (_err, cursor) { + cursor.toArray(function (_err, data) { + // was the quote found? + if ((data.length === 0) + || (data[0].revisions.length < (revid + 1))) { + callback(null); return; } - - cursor.toArray( function( err, data ) - { - if ( err !== null ) - { - callback( err, null ); - return; - } - - callback( null, ( data[ 0 ] || {} )[ key ] ); - } ); - } - ); - }, -} ); + // return the quote data + callback(data[0].revisions[revid]); + }); + }); + }; + MongoServerDao.prototype.setWorksheets = function (qid, data, callback) { + this._collection.update({ id: qid }, { '$set': { worksheets: { data: data } } }, + // create record if it does not yet exist + { upsert: true }, + // on complete + function (err) { + callback(err); + return; + }); + }; + MongoServerDao.prototype.getWorksheet = function (qid, supplier, index, callback) { + this._collection.find({ id: qid }, { limit: 1 }, function (_err, cursor) { + cursor.toArray(function (_err, data) { + // was the quote found? + if ((data.length === 0) + || (!data[0].worksheets) + || (!data[0].worksheets.data) + || (!data[0].worksheets.data[supplier])) { + callback(null); + return; + } + // return the quote data + callback(data[0].worksheets.data[supplier][index]); + }); + }); + }; + return MongoServerDao; +}(EventEmitter)); +exports.MongoServerDao = MongoServerDao; +; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiTW9uZ29TZXJ2ZXJEYW8uanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJNb25nb1NlcnZlckRhby50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUE7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7R0FtQkc7Ozs7Ozs7Ozs7Ozs7OztBQVNILElBQU0sWUFBWSxHQUFHLE9BQU8sQ0FBRSxRQUFRLENBQUUsQ0FBQyxZQUFZLENBQUM7QUFJdEQ7O0dBRUc7QUFDSDtJQUFvQyxrQ0FBWTtJQXlCNUM7Ozs7T0FJRztJQUNILHdCQUNxQixHQUFRO1FBRDdCLFlBSUksaUJBQU8sU0FDVjtRQUpvQixTQUFHLEdBQUgsR0FBRyxDQUFLO1FBN0I3QixzQ0FBc0M7UUFDN0IsZ0JBQVUsR0FBVyxRQUFRLENBQUM7UUFFdkMsMkNBQTJDO1FBQ2xDLG9CQUFjLEdBQVcsS0FBSyxDQUFDO1FBRXhDLGlDQUFpQztRQUN4QixrQkFBWSxHQUFXLFNBQVMsQ0FBQztRQUUxQywrQkFBK0I7UUFDdEIsMEJBQW9CLEdBQVcsTUFBTSxDQUFDO1FBRy9DLDBEQUEwRDtRQUNsRCxZQUFNLEdBQVksS0FBSyxDQUFDOztJQW1CaEMsQ0FBQztJQUdEOzs7Ozs7Ozs7T0FTRztJQUNILDZCQUFJLEdBQUosVUFBTSxRQUFrQjtRQUVwQixJQUFJLEdBQUcsR0FBRyxJQUFJLENBQUM7UUFFZixxRUFBcUU7UUFDckUsSUFBSSxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUUsT0FBTyxFQUFFLFVBQVUsR0FBUTtZQUVwQyxHQUFHLENBQUMsTUFBTSxHQUFRLEtBQUssQ0FBQztZQUN4QixHQUFHLENBQUMsV0FBVyxHQUFHLElBQUksQ0FBQztZQUV2QixHQUFHLENBQUMsSUFBSSxDQUFFLGNBQWMsRUFBRSxHQUFHLENBQUUsQ0FBQztRQUNwQyxDQUFDLENBQUMsQ0FBQztRQUVILElBQUksQ0FBQyxPQUFPLENBQUUsUUFBUSxDQUFFLENBQUM7UUFDekIsT0FBTyxJQUFJLENBQUM7SUFDaEIsQ0FBQztJQUdEOzs7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 \ No newline at end of file diff --git a/src/server/db/MongoServerDao.ts b/src/server/db/MongoServerDao.ts new file mode 100644 index 0000000..2cbce84 --- /dev/null +++ b/src/server/db/MongoServerDao.ts @@ -0,0 +1,788 @@ +/** + * 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 { ServerDao, Callback } from "./ServerDao"; +import { MongoCollection, MongoUpdate } from "mongodb"; +import { PositiveInteger } from "../../numeric"; +import { ServerSideQuote } from "../quote/ServerSideQuote"; +import { QuoteId } from "../../document/Document"; +import { WorksheetData } from "../rater/Rater"; + +const EventEmitter = require( 'events' ).EventEmitter; + +type ErrorCallback = ( err: NullableError ) => void; + +/** + * Uses MongoDB as a data store + */ +export class MongoServerDao extends EventEmitter implements ServerDao +{ + /** Collection used to store quotes */ + readonly COLLECTION: string = 'quotes'; + + /** Sequence (auto-increment) collection */ + readonly COLLECTION_SEQ: string = 'seq'; + + /** Sequence key for quote ids */ + readonly SEQ_QUOTE_ID: string = 'quoteId'; + + /** Sequence quoteId default */ + readonly SEQ_QUOTE_ID_DEFAULT: number = 200000; + + + /** Whether the DAO is initialized and ready to be used */ + private _ready: boolean = false; + + /** Collection to save data to */ + private _collection?: MongoCollection | null; + + /** Collection to read sequences (auto-increments) from */ + private _seqCollection?: MongoCollection | null; + + + /** + * Initializes DAO + * + * @param {Mongo.Db} db mongo database connection + */ + constructor( + private readonly _db: any + ) + { + super(); + } + + + /** + * Initializes error events and attempts to connect to the database + * + * connectError event will be emitted on failure. + * + * @param Function callback function to call when connection is complete + * (will not be called if connection fails) + * + * @return MongoServerDao self to allow for method chaining + */ + init( callback: () => {} ): this + { + var dao = this; + + // map db error event (on connection error) to our connectError event + this._db.on( 'error', function( err: any ) + { + dao._ready = false; + dao._collection = null; + + dao.emit( 'connectError', err ); + }); + + this.connect( callback ); + return this; + } + + + /** + * Attempts to connect to the database + * + * connectError event will be emitted on failure. + * + * @param Function callback function to call when connection is complete + * (will not be called if connection fails) + * + * @return MongoServerDao self to allow for method chaining + */ + connect( callback: () => {} ): this + { + var dao = this; + + // attempt to connect to the database + this._db.open( function( err: any, db: any ) + { + // if there was an error, don't bother with anything else + if ( err ) + { + // in some circumstances, it may just be telling us that we're + // already connected (even though the connection may have been + // broken) + if ( err.errno !== undefined ) + { + dao.emit( 'connectError', err ); + return; + } + } + + var ready_count = 0; + var check_ready = function() + { + if ( ++ready_count < 2 ) + { + return; + } + + // we're ready to roll! + dao._ready = true; + dao.emit( 'ready' ); + + // connection was successful; call the callback + if ( callback instanceof Function ) + { + callback.call( dao ); + } + } + + // quotes collection + db.collection( + dao.COLLECTION, + function( + _err: any, + collection: MongoCollection, + ) { + // for some reason this gets called more than once + if ( collection == null ) + { + return; + } + + // initialize indexes + collection.createIndex( + [ ['id', 1] ], + true, + function( _err: any, _index: { [P: string]: any } ) + { + // mark the DAO as ready to be used + dao._collection = collection; + check_ready(); + } + ); + } + ); + + // seq collection + db.collection( + dao.COLLECTION_SEQ, + function( + err: any, + collection: MongoCollection, + ) { + if ( err ) + { + dao.emit( 'seqError', err ); + return; + } + + if ( collection == null ) + { + return; + } + + dao._seqCollection = collection; + + // has the sequence we'll be referencing been initialized? + collection.find( + { _id: dao.SEQ_QUOTE_ID }, + { limit: 1 }, + function( err: any, cursor ) + { + if ( err ) + { + dao._initQuoteIdSeq( check_ready ) + return; + } + + cursor.toArray( function( _err: any, data: any[] ) + { + if ( data.length == 0 ) + { + dao._initQuoteIdSeq( check_ready ); + return; + } + + check_ready(); + }); + } + ); + } + ); + }); + + return this; + } + + + private _initQuoteIdSeq( callback: () => void ) + { + var dao = this; + + this._seqCollection!.insert( + { + _id: this.SEQ_QUOTE_ID, + val: this.SEQ_QUOTE_ID_DEFAULT, + }, + function( err: any, _docs: any ) + { + if ( err ) + { + dao.emit( 'seqError', err ); + return; + } + + dao.emit( 'seqInit', dao.SEQ_QUOTE_ID ); + callback.call( dao ); + } + ); + } + + + /** + * Saves a quote to the database + * + * A full save will include all metadata. This should not cause any + * problems with race conditions for pending Data API calls on meta + * fields because those results write to individual indexes and do not + * rely on existing data. + * + * @param Quote quote the quote to save + * @param Function success_callback function to call on success + * @param Function failure_callback function to call if save fails + * @param Object save_data quote data to save (optional) + * @param Object push_data quote data to push (optional) + */ + saveQuote( + quote: ServerSideQuote, + success_callback: Callback, + failure_callback: Callback, + save_data?: any, + push_data?: any, + ): this + { + var dao = this; + var meta: Record = {}; + + // if we're not ready, then we can't save the quote! + if ( this._ready === false ) + { + this.emit( 'saveQuoteError', + { message: 'Database server not ready' }, + Error( 'Database not ready' ), + quote + ); + + failure_callback.call( this, quote ); + return dao; + } + + if ( save_data === undefined ) + { + save_data = { + data: quote.getBucket().getData(), + }; + + // full save will include all metadata + meta = quote.getMetabucket().getData(); + } + + var id = quote.getId(); + + // some data should always be saved because the quote will be created if + // it does not yet exist + save_data.id = id; + save_data.pver = quote.getProgramVersion(); + save_data.importDirty = 1; + save_data.lastPremDate = quote.getLastPremiumDate(); + save_data.initialRatedDate = quote.getRatedDate(); + save_data.explicitLock = quote.getExplicitLockReason(); + save_data.explicitLockStepId = quote.getExplicitLockStep(); + save_data.importedInd = +quote.isImported(); + save_data.boundInd = +quote.isBound(); + save_data.lastUpdate = Math.round( + ( new Date() ).getTime() / 1000 + ); + + // meta will eventually take over for much of the above data + meta.liza_timestamp_initial_rated = [ quote.getRatedDate() ]; + + // save the stack so we can track this call via the oplog + save_data._stack = ( new Error() ).stack; + + // avoid wiping out other metadata (since this may not be a full set) + Object.keys( meta ).forEach( + key => save_data[ 'meta.' + key ] = meta[ key ] + ); + + // do not push empty objects + const document = ( !push_data || !Object.keys( push_data ).length ) + ? { '$set': save_data } + : { '$set': save_data, '$push': push_data }; + + // update the quote data if it already exists (same id), otherwise + // insert it + this._collection!.update( { id: id }, + document, + + // create record if it does not yet exist + { upsert: true }, + + // on complete + function( err, _docs ) + { + // if an error occurred, then we cannot continue + if ( err ) + { + dao.emit( 'saveQuoteError', err, quote ); + + // let the caller handle the error + if ( failure_callback instanceof Function ) + { + failure_callback.call( dao, quote ); + } + + return; + } + + // successful + if ( success_callback instanceof Function ) + { + success_callback.call( dao, quote ); + } + } + ); + + return this; + } + + + /** + * Merges quote data with the existing (rather than overwriting) + * + * @param {Quote} quote quote to save + * @param {Object} data quote data + * @param {Function} scallback successful callback + * @param {Function} fcallback failure callback + */ + mergeData( + quote: ServerSideQuote, + data: MongoUpdate, + scallback: Callback, + fcallback: Callback, + ): this + { + // we do not want to alter the original data; use it as a prototype + var update = data; + + // save the stack so we can track this call via the oplog + var _self = this; + this._collection!.update( { id: quote.getId() }, + { '$set': update }, + {}, + + function( err, _docs ) + { + if ( err ) + { + _self.emit( 'saveQuoteError', err, quote ); + + if ( typeof fcallback === 'function' ) + { + fcallback( quote ); + } + + return; + } + + if ( typeof scallback === 'function' ) + { + scallback( quote ); + } + } + ); + + return this; + } + + + /** + * Merges bucket data with the existing bucket (rather than overwriting the + * entire bucket) + * + * @param {Quote} quote quote to save + * @param {Object} data bucket data + * @param {Function} scallback successful callback + * @param {Function} fcallback failure callback + * + * @return {MongoServerDao} self + */ + mergeBucket( + quote: ServerSideQuote, + data: MongoUpdate, + success: Callback, + failure: Callback, + ): this + { + var update: MongoUpdate = {}; + + for ( var field in data ) + { + if ( !field ) + { + continue; + } + + update[ 'data.' + field ] = data[ field ]; + } + + return this.mergeData( quote, update, success, failure ); + } + + + /** + * 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 quote the quote to save + * @param Function success_callback function to call on success + * @param Function failure_callback function to call if save fails + * + * @return MongoServerDao self + */ + saveQuoteState( + quote: ServerSideQuote, + success_callback: any, + failure_callback: any, + ) + { + var update = { + currentStepId: quote.getCurrentStepId(), + topVisitedStepId: quote.getTopVisitedStepId(), + topSavedStepId: quote.getTopSavedStepId(), + }; + + return this.mergeData( + quote, update, success_callback, failure_callback + ); + } + + + saveQuoteClasses( + quote: ServerSideQuote, + classes: any, + success: any, + failure: any, + ) + { + return this.mergeData( + quote, + { classData: classes }, + success, + failure + ); + } + + + /** + * Save document metadata (meta field on document) + * + * Only the provided indexes will be modified (that is---data will be + * merged with what is already in the database). + * + * @param {Quote} quote destination quote + * @param {Object} new_meta bucket-formatted data to write + * @param {Function} success callback on success + * @param {Function} failure callback on error + * + * @return {undefined} + */ + saveQuoteMeta( + quote: ServerSideQuote, + new_meta: any, + success: Callback, + failure: Callback, + ): void + { + const update: MongoUpdate = {}; + + for ( var key in new_meta ) + { + var meta = new_meta[ key ]; + + for ( var i in meta ) + { + update[ 'meta.' + key + '.' + i ] = new_meta[ key ][ i ]; + } + } + + this.mergeData( quote, update, success, failure ); + } + + + /** + * Saves the quote lock state to the database + * + * @param Quote quote the quote to save + * @param Function success_callback function to call on success + * @param Function failure_callback function to call if save fails + * + * @return MongoServerDao self + */ + saveQuoteLockState( + quote: ServerSideQuote, + success_callback: Callback, + failure_callback: Callback, + ): this + { + // lock state is saved by default + return this.saveQuote( + quote, + success_callback, + failure_callback, + {} + ); + } + + + /** + * Pulls quote data from the database + * + * @param Integer quote_id id of quote + * @param Function( data ) callback function to call when data is available + * + * @return MongoServerDao self to allow for method chaining + */ + pullQuote( + quote_id: PositiveInteger, + callback: ( data: Record | null ) => void + ): this + { + var dao = this; + + // XXX: TODO: Do not read whole of record into memory; filter out + // revisions! + this._collection!.find( { id: quote_id }, { limit: 1 }, + function( _err, cursor ) + { + cursor.toArray( function( _err: NullableError, data: any[] ) + { + // was the quote found? + if ( data.length == 0 ) + { + callback.call( dao, null ); + return; + } + + // return the quote data + callback.call( dao, data[ 0 ] ); + }); + } + ); + + return this; + } + + + getMinQuoteId( callback: ( min_id: number ) => void ): this + { + // just in case it's asynchronous later on + callback.call( this, this.SEQ_QUOTE_ID_DEFAULT ); + + return this; + } + + + getMaxQuoteId( callback: ( min_id: number ) => void ): void + { + var dao = this; + + this._seqCollection!.find( + { _id: this.SEQ_QUOTE_ID }, + { limit: 1 }, + function( _err, cursor ) + { + cursor.toArray( function( _err: NullableError, data: any[] ) + { + if ( data.length == 0 ) + { + callback.call( dao, 0 ); + return; + } + + // return the max quote id + callback.call( dao, data[ 0 ].val ); + }); + } + ); + } + + + getNextQuoteId( callback: ( quote_id: number ) => void ): this + { + var dao = this; + + this._seqCollection!.findAndModify( + { _id: this.SEQ_QUOTE_ID }, + [ [ 'val', 'descending' ] ], + { $inc: { val: 1 } }, + { 'new': true }, + + function( err, doc ) + { + if ( err ) + { + dao.emit( 'seqError', err ); + + callback.call( dao, 0 ); + return; + } + + // return the new id + callback.call( dao, doc.val ); + } + ); + + return this; + } + + + /** + * Create a new revision with the provided quote data + * + * The revision will contain the whole the quote. If space is a concern, we + * can (in the future) calculate a delta instead (Mike recommends the Git + * model of storing the deltas in previous revisions and the whole of the + * bucket in the most recently created revision). + */ + createRevision( + quote: ServerSideQuote, + callback: ErrorCallback, + ): void + { + var _self = this, + qid = quote.getId(), + data = quote.getBucket().getData(); + + this._collection!.update( { id: qid }, + { '$push': { revisions: { data: data } } }, + + // create record if it does not yet exist + { upsert: true }, + + // on complete + function( err ) + { + if ( err ) + { + _self.emit( 'mkrevError', err ); + } + + callback( err ); + return; + } + ); + } + + + getRevision( + quote: ServerSideQuote, + revid: PositiveInteger, + callback: ErrorCallback, + ): void + { + revid = +revid; + + // XXX: TODO: Filter out all but the revision we want + this._collection!.find( + { id: quote.getId() }, + { limit: 1 }, + function( _err, cursor ) + { + cursor.toArray( function( _err: NullableError, data: any[] ) + { + // was the quote found? + if ( ( data.length === 0 ) + || ( data[ 0 ].revisions.length < ( revid + 1 ) ) + ) + { + callback( null ); + return; + } + + // return the quote data + callback( data[ 0 ].revisions[ revid ] ); + }); + } + ); + } + + + setWorksheets( + qid: QuoteId, + data: MongoUpdate, + callback: NodeCallback, + ): void + { + this._collection!.update( { id: qid }, + { '$set': { worksheets: { data: data } } }, + + // create record if it does not yet exist + { upsert: true }, + + // on complete + function( err ) + { + callback( err ); + return; + } + ); + } + + + getWorksheet( + qid: QuoteId, + supplier: string, + index: PositiveInteger, + callback: ( data: WorksheetData | null ) => void, + ): void + { + this._collection!.find( + { id: qid }, + { limit: 1 }, + function( _err, cursor ) + { + cursor.toArray( function( _err: NullableError, data: any[] ) + { + // was the quote found? + if ( ( data.length === 0 ) + || ( !data[ 0 ].worksheets ) + || ( !data[ 0 ].worksheets.data ) + || ( !data[ 0 ].worksheets.data[ supplier ] ) + ) + { + callback( null ); + return; + } + + // return the quote data + callback( data[ 0 ].worksheets.data[ supplier ][ index ] ); + } ); + } + ); + } +}; diff --git a/src/server/db/ServerDao.d.ts b/src/server/db/ServerDao.d.ts index 6cc8025..59228c3 100644 --- a/src/server/db/ServerDao.d.ts +++ b/src/server/db/ServerDao.d.ts @@ -131,7 +131,7 @@ export interface ServerDao qid: QuoteId, data: WorksheetData, callback: NodeCallback, - ): this; + ): void; /** @@ -147,5 +147,5 @@ export interface ServerDao supplier: string, index: PositiveInteger, callback: ( data: WorksheetData | null ) => void, - ): this; + ): void; } diff --git a/src/server/quote/ServerSideQuote.d.ts b/src/server/quote/ServerSideQuote.d.ts index c8bdf2f..1e00d3f 100644 --- a/src/server/quote/ServerSideQuote.d.ts +++ b/src/server/quote/ServerSideQuote.d.ts @@ -68,4 +68,20 @@ export declare class ServerSideQuote extends BaseQuote * @return rating data */ getRatingData(): Record; + + + /** + * Metadata bucket + * + * @return the metadata bucket + */ + getMetabucket(): QuoteDataBucket; + + + /** + * Get the program version + * + * @return program version + */ + getProgramVersion(): string; } diff --git a/src/server/service/RatingService.ts b/src/server/service/RatingService.ts index ea4667e..83cbd10 100644 --- a/src/server/service/RatingService.ts +++ b/src/server/service/RatingService.ts @@ -433,7 +433,7 @@ export class RatingService } } - this._dao.setWorksheets( qid, worksheets, ( err: Error | null ) => + this._dao.setWorksheets( qid, worksheets, ( err: NullableError ) => { if ( err ) { diff --git a/src/server/service/RatingServicePublish.js b/src/server/service/RatingServicePublish.js index 2ce2d98..ce843f0 100644 --- a/src/server/service/RatingServicePublish.js +++ b/src/server/service/RatingServicePublish.js @@ -1,4 +1,3 @@ -"use strict"; /** * Publishes message to queue after rating * @@ -19,22 +18,13 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -var __extends = (this && this.__extends) || (function () { - var extendStatics = function (d, b) { - extendStatics = Object.setPrototypeOf || - ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || - function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; - return extendStatics(d, b); - }; - return function (d, b) { - extendStatics(d, b); - function __() { this.constructor = d; } - d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -var RatingService_1 = require("./RatingService"); -var amqplib_1 = require("amqplib"); + +'use strict'; + +const { Interface, Trait } = require( 'easejs' ); +const { RatingService } = require( './RatingService' ); + + /** * Publish message to a queue after rating * @@ -60,11 +50,35 @@ var amqplib_1 = require("amqplib"); * * See the body of `#_sendMessage' for their values. */ -var RatingServicePublish = /** @class */ (function (_super) { - __extends(RatingServicePublish, _super); - function RatingServicePublish() { - return _super !== null && _super.apply(this, arguments) || this; - } +module.exports = Trait( 'RatingServicePublish' ) + .implement( Interface( { 'postProcessRaterData': [] } ) ) + .extend( +{ + /** + * AMQP library (amqplib API) + * + * @type {amqplib} + */ + 'private _amqp': null, + + /** + * AMQP configuration + * + * This should be the configuration expected by amqplib's #connect. It + * should additionally contain a `queueName' field. + * + * @type {Object} + */ + 'private _conf': {}, + + /** + * Logger + * + * @type {DebugLog} + */ + 'private _log': null, + + /** * Initialize trait * @@ -72,13 +86,14 @@ var RatingServicePublish = /** @class */ (function (_super) { * @param {Object} conf AMQP configuration * @param {DebugLog} logger logger instance */ - RatingServicePublish.prototype.__mixin = function ( - // constructor( - // private readonly _amqp: Connection, - _conf) { - // super(); - this._conf = _conf; - }; + __mixin( amqp, conf, logger ) + { + this._amqp = amqp; + this._conf = conf; + this._log = logger; + }, + + /** * Publish quote message to exchange post-rating * @@ -90,25 +105,44 @@ var RatingServicePublish = /** @class */ (function (_super) { * * @return {undefined} */ - RatingServicePublish.prototype.postProcessRaterData = function (request, data, actions, program, quote) { - var _this = this; + 'abstract override postProcessRaterData'( + request, data, actions, program, quote + ) + { // check both as we transition from one to the other - var exchange = this._conf.exchange; - amqplib_1.connect(this._conf) - .then(function (conn) { - setTimeout(function () { return conn.close(); }, 10000); - return conn.createChannel(); - }) - .then(function (ch) { - ch.assertExchange(exchange, 'fanout', { durable: true }); - return _this._sendMessage(ch, exchange, request.getSession(), quote); - }) - .then(function () { return _this._logger.log(_this._logger.PRIORITY_INFO, "Published quote " + quote.getId() + - " to post-rate exchange '" + exchange + "'"); }) - .catch(function (e) { return _this._logger.log(_this._logger.PRIORITY_ERROR, "Post-rate exchange publish failure for quote " + - quote.getId() + ": " + e.message); }); - _super.prototype.postProcessRaterData.call(this, request, data, actions, program, quote); - }; + const exchange = this._conf.exchange || this._conf.queueName; + + this._amqp.connect( this._conf ) + .then( conn => + { + setTimeout( () => conn.close(), 10000 ); + return conn.createChannel(); + } ) + .then( ch => { + ch.assertExchange( exchange, 'fanout', { durable: true } ); + + return this._sendMessage( + ch, + exchange, + request.getSession(), + quote + ); + } ) + .then( () => this._log.log( + this._log.PRIORITY_INFO, + "Published quote " + quote.getId() + + " to post-rate exchange '" + exchange + "'" + ) ) + .catch( e => this._log.log( + this._log.PRIORITY_ERROR, + "Post-rate exchange publish failure for quote " + + quote.getId() + ": " + e.message + ) ); + + this.__super( request, data, actions, program, quote ); + }, + + /** * Send message to exchange * @@ -117,25 +151,31 @@ var RatingServicePublish = /** @class */ (function (_super) { * @param {UserSession} session user session * @param {Quote} quote rated quote * - * @return whether publish was successful + * @return {Promise} whether publish was successful */ - RatingServicePublish.prototype._sendMessage = function (channel, exchange, session, quote) { - var headers = { + 'private _sendMessage'( channel, exchange, session, quote ) + { + const headers = { version: 1, created: Date.now(), }; - var data = new Buffer(JSON.stringify({ - quote_id: quote.getId(), - program_id: quote.getProgramId(), - agent_id: session.agentId(), - entity_id: session.agentEntityId(), + + const data = new Buffer( JSON.stringify( { + quote_id: quote.getId(), + program_id: quote.getProgramId(), + agent_id: session.agentId(), + entity_id: session.agentEntityId(), entity_name: session.agentName(), - })); + } ) ); + // we don't use a routing key; fanout exchange - var routing_key = ''; - return channel.publish(exchange, routing_key, data, { headers: headers }); - }; - return RatingServicePublish; -}(RatingService_1.RatingService)); -exports.RatingServicePublish = RatingServicePublish; -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiUmF0aW5nU2VydmljZVB1Ymxpc2guanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJSYXRpbmdTZXJ2aWNlUHVibGlzaC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUE7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7R0FtQkc7Ozs7Ozs7Ozs7Ozs7OztBQUdILGlEQUFnRDtBQVFoRCxtQ0FJaUI7QUFRakI7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQXdCRztBQUNIO0lBQTBDLHdDQUFhO0lBQXZEOztJQTJIQSxDQUFDO0lBOUdHOzs7Ozs7T0FNRztJQUNILHNDQUFPLEdBQVA7SUFDQSxlQUFlO0lBQ1gsd0NBQXdDO0lBQ3hDLEtBQW1CO1FBRW5CLFdBQVc7UUFDWCxJQUFJLENBQUMsS0FBSyxHQUFHLEtBQUssQ0FBQztJQUN2QixDQUFDO0lBR0Q7Ozs7Ozs7Ozs7TUFVRTtJQUNPLG1EQUFvQixHQUE5QixVQUNLLE9BQW9CLEVBQ3BCLElBQW1CLEVBQ25CLE9BQXNCLEVBQ3RCLE9BQWdCLEVBQ2hCLEtBQXdCO1FBTDdCLGlCQXVDRTtRQS9CRyxvREFBb0Q7UUFDcEQsSUFBTSxRQUFRLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxRQUFRLENBQUM7UUFFckMsaUJBQVcsQ0FBRSxJQUFJLENBQUMsS0FBSyxDQUFFO2FBQ3BCLElBQUksQ0FBRSxVQUFBLElBQUk7WUFFUCxVQUFVLENBQUUsY0FBTSxPQUFBLElBQUksQ0FBQyxLQUFLLEVBQUUsRUFBWixDQUFZLEVBQUUsS0FBSyxDQUFFLENBQUM7WUFDeEMsT0FBTyxJQUFJLENBQUMsYUFBYSxFQUFFLENBQUM7UUFDaEMsQ0FBQyxDQUFFO2FBQ0YsSUFBSSxDQUFFLFVBQUEsRUFBRTtZQUNMLEVBQUUsQ0FBQyxjQUFjLENBQUUsUUFBUSxFQUFFLFFBQVEsRUFBRSxFQUFFLE9BQU8sRUFBRSxJQUFJLEVBQUUsQ0FBRSxDQUFDO1lBRTNELE9BQU8sS0FBSSxDQUFDLFlBQVksQ0FDcEIsRUFBRSxFQUNGLFFBQVEsRUFDUixPQUFPLENBQUMsVUFBVSxFQUFFLEVBQ3BCLEtBQUssQ0FDUixDQUFDO1FBQ04sQ0FBQyxDQUFFO2FBQ0YsSUFBSSxDQUFFLGNBQU0sT0FBQSxLQUFJLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FDekIsS0FBSSxDQUFDLE9BQU8sQ0FBQyxhQUFhLEVBQzFCLGtCQUFrQixHQUFHLEtBQUssQ0FBQyxLQUFLLEVBQUU7WUFDOUIsMEJBQTBCLEdBQUcsUUFBUSxHQUFHLEdBQUcsQ0FDbEQsRUFKWSxDQUlaLENBQUU7YUFDRixLQUFLLENBQUUsVUFBQSxDQUFDLElBQUksT0FBQSxLQUFJLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FDekIsS0FBSSxDQUFDLE9BQU8sQ0FBQyxjQUFjLEVBQzNCLCtDQUErQztZQUMzQyxLQUFLLENBQUMsS0FBSyxFQUFFLEdBQUcsSUFBSSxHQUFHLENBQUMsQ0FBQyxPQUFPLENBQ3ZDLEVBSlksQ0FJWixDQUFFLENBQUM7UUFFUixpQkFBTSxvQkFBb0IsWUFBRSxPQUFPLEVBQUUsSUFBSSxFQUFFLE9BQU8sRUFBRSxPQUFPLEVBQUUsS0FBSyxDQUFFLENBQUM7SUFDekUsQ0FBQztJQUdEOzs7Ozs7Ozs7T0FTRztJQUNILDJDQUFZLEdBQVosVUFDSSxPQUFpQixFQUNqQixRQUFnQixFQUNoQixPQUFxQixFQUNyQixLQUF5QjtRQUd6QixJQUFNLE9BQU8sR0FBRztZQUNaLE9BQU8sRUFBRSxDQUFDO1lBQ1YsT0FBTyxFQUFFLElBQUksQ0FBQyxHQUFHLEVBQUU7U0FDdEIsQ0FBQztRQUVGLElBQU0sSUFBSSxHQUFHLElBQUksTUFBTSxDQUFFLElBQUksQ0FBQyxTQUFTLENBQUU7WUFDckMsUUFBUSxFQUFLLEtBQUssQ0FBQyxLQUFLLEVBQUU7WUFDMUIsVUFBVSxFQUFHLEtBQUssQ0FBQyxZQUFZLEVBQUU7WUFDakMsUUFBUSxFQUFLLE9BQU8sQ0FBQyxPQUFPLEVBQUU7WUFDOUIsU0FBUyxFQUFJLE9BQU8sQ0FBQyxhQUFhLEVBQUU7WUFDcEMsV0FBVyxFQUFFLE9BQU8sQ0FBQyxTQUFTLEVBQUU7U0FDbkMsQ0FBRSxDQUFFLENBQUM7UUFFTiw4Q0FBOEM7UUFDOUMsSUFBTSxXQUFXLEdBQUcsRUFBRSxDQUFDO1FBRXZCLE9BQU8sT0FBTyxDQUFDLE9BQU8sQ0FDbEIsUUFBUSxFQUNSLFdBQVcsRUFDWCxJQUFJLEVBQ0osRUFBRSxPQUFPLEVBQUUsT0FBTyxFQUFFLENBQ3ZCLENBQUM7SUFDTixDQUFDO0lBQ0wsMkJBQUM7QUFBRCxDQUFDLEFBM0hELENBQTBDLDZCQUFhLEdBMkh0RDtBQTNIWSxvREFBb0IifQ== \ No newline at end of file + const routing_key = ''; + + return channel.publish( + exchange, + routing_key, + data, + { headers: headers } + ); + }, +} ); diff --git a/src/server/service/RatingServicePublish.ts b/src/server/service/RatingServicePublish.ts deleted file mode 100644 index 1966759..0000000 --- a/src/server/service/RatingServicePublish.ts +++ /dev/null @@ -1,191 +0,0 @@ -/** - * Publishes message to queue after rating - * - * 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 . - */ - - -import { RatingService } from "./RatingService"; -import { UserRequest } from "../request/UserRequest"; -import { RateResult } from "../rater/Rater"; -import { ClientActions } from "../../client/action/ClientAction"; -import { Program } from "../../program/Program"; -import { ServerSideQuote } from "../quote/ServerSideQuote"; -import { UserSession } from "../request/UserSession"; - -import { - connect as amqpConnect, - Options, - Channel, -} from 'amqplib'; - - -export interface AmqpConfig extends Options.Connect { - /** The name of a queue or exchange to publish to */ - exchange: string; -} - -/** - * Publish message to a queue after rating - * - * This is an initial proof-of-concept implementation. In particular, we - * have the following considerations: - * - * - A fresh connection is made for each message until we can ensure that - * we can auto-reconnect on failure; - * - This trait is not yet tested; - * - It does not use an exchange; - * - It does a poor job checking for and reporting errors. - * - * The message consists of a `version' header that is set to 1. Future - * changes to the message format will bump this version. There is also a - * `created' header holding a Unix timestamp of the moment that the message - * was created. - * - * Version 1 of the body consists of four fields: - * - quote_id - * - agent_id - * - entity_id - * - entity_name - * - * See the body of `#_sendMessage' for their values. - */ -export class RatingServicePublish extends RatingService -{ - /** - * AMQP configuration - * - * This should be the configuration expected by amqplib's #connect. It - * should additionally contain a `queueName' field. - * - * @type {Object} - */ - private _conf: AmqpConfig; - - - /** - * Initialize trait - * - * @param {amqplib} AMQP library - * @param {Object} conf AMQP configuration - * @param {DebugLog} logger logger instance - */ - __mixin( - // constructor( - // private readonly _amqp: Connection, - _conf: AmqpConfig, - ) { - // super(); - this._conf = _conf; - } - - - /** - * Publish quote message to exchange post-rating - * - * @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} - */ - protected postProcessRaterData( - request: UserRequest, - data: RateResult, - actions: ClientActions, - program: Program, - quote: ServerSideQuote, - ): void - { - // check both as we transition from one to the other - const exchange = this._conf.exchange; - - amqpConnect( this._conf ) - .then( conn => - { - setTimeout( () => conn.close(), 10000 ); - return conn.createChannel(); - } ) - .then( ch => { - ch.assertExchange( exchange, 'fanout', { durable: true } ); - - return this._sendMessage( - ch, - exchange, - request.getSession(), - quote - ); - } ) - .then( () => this._logger.log( - this._logger.PRIORITY_INFO, - "Published quote " + quote.getId() + - " to post-rate exchange '" + exchange + "'" - ) ) - .catch( e => this._logger.log( - this._logger.PRIORITY_ERROR, - "Post-rate exchange publish failure for quote " + - quote.getId() + ": " + e.message - ) ); - - super.postProcessRaterData( request, data, actions, program, quote ); - } - - - /** - * Send message to exchange - * - * @param {Channel} channel AMQP channel - * @param {string} exchange exchange name - * @param {UserSession} session user session - * @param {Quote} quote rated quote - * - * @return whether publish was successful - */ - _sendMessage( - channel: Channel, - exchange: string, - session: UserSession, - quote: ServerSideQuote - ): boolean - { - const headers = { - version: 1, - created: Date.now(), - }; - - const data = new Buffer( JSON.stringify( { - quote_id: quote.getId(), - program_id: quote.getProgramId(), - agent_id: session.agentId(), - entity_id: session.agentEntityId(), - entity_name: session.agentName(), - } ) ); - - // we don't use a routing key; fanout exchange - const routing_key = ''; - - return channel.publish( - exchange, - routing_key, - data, - { headers: headers } - ); - } -} diff --git a/src/server/token/MongoTokenDao.ts b/src/server/token/MongoTokenDao.ts index ee90d7a..5d3dede 100644 --- a/src/server/token/MongoTokenDao.ts +++ b/src/server/token/MongoTokenDao.ts @@ -34,6 +34,8 @@ import { DocumentId } from "../../document/Document"; import { TokenId, TokenNamespace, TokenState } from "./Token"; import { UnknownTokenError } from "./UnknownTokenError"; import { context } from "../../error/ContextError"; +import { MongoCollection } from "mongodb"; + /** @@ -118,7 +120,7 @@ export class MongoTokenDao implements TokenDao }, }, - ( err: Error|null, prev_data ) => + ( err: NullableError, prev_data ) => { if ( err ) { @@ -250,7 +252,7 @@ export class MongoTokenDao implements TokenDao this._collection.findOne( { id: +doc_id }, { fields: fields }, - ( err: Error|null, data: TokenQueryResult ) => + ( err: NullableError, data: TokenQueryResult ) => { if ( err || !data ) { diff --git a/src/types/misc.d.ts b/src/types/misc.d.ts index 5572739..b26d827 100644 --- a/src/types/misc.d.ts +++ b/src/types/misc.d.ts @@ -65,4 +65,7 @@ type UnixTimestampMillis = NominalType; * reduce the boilerplate of these function definitions, and to clearly * document that this pattern is something that used to be done frequently. */ -type NodeCallback = ( e: Error | null, result: T | null ) => R; +type NodeCallback = ( e: NullableError, result: T | null ) => R; + +/** Nullable error */ +type NullableError = Error | null; diff --git a/src/types/mongodb.d.ts b/src/types/mongodb.d.ts index 808b458..4b0e21f 100644 --- a/src/types/mongodb.d.ts +++ b/src/types/mongodb.d.ts @@ -23,13 +23,15 @@ * front. */ +import { PositiveInteger } from "../numeric"; + declare module "mongodb"; /** * Node-style callback for queries */ -type MongoCallback = ( err: Error|null, data: { [P: string]: any } ) => void; +type MongoCallback = ( err: NullableError, data: { [P: string]: any } ) => void; /** @@ -52,10 +54,24 @@ interface MongoQueryUpdateOptions */ interface MongoFindOneOptions { + /** Fields to select */ fields?: MongoFieldSelector, } +/** + * Options for `find` queries + * + * This is not at all comprehensive; it covers only the fields we actually + * make use of. + */ +interface MongoFindOptions +{ + /** Limit results returned */ + limit?: PositiveInteger, +} + + /** * Options for `findAndModify` queries * @@ -76,21 +92,26 @@ interface MongoFindAndModifyOptions /** Mongo query selector */ -type MongoSelector = { [P: string]: any }; - +export type MongoSelector = { [P: string]: any }; /** Field selector */ type MongoFieldSelector = { [P: string]: number }; +/** Mongo index specification */ +type MongoIndexSpecification = Array< Array < string | number >>; /** Mongo update clause */ -type MongoUpdate = MongoSelector; +export type MongoUpdate = MongoSelector; +/** Mongo object */ +type MongoObject = { [P: string]: any }; + +/** Mongo update clause */ +type MongoInsertSpecification = MongoObject | MongoObject[]; /** Sorting clause **/ type MongoSortClause = Array; - /** Sort direction */ type MongoSortDirection = -1 | 1 | 'ascending' | 'descending' | 'asc' | 'desc'; @@ -126,6 +147,23 @@ declare interface MongoCollection ): void; + /** + * Execute a query and return the first result + * + * Unlike `update`, the callback return value is not propagated, and so + * the callback ought not return anything. + * + * @param selector document query + * @param fields fields to return + * @param callback continuation on completion + */ + find( + selector: MongoSelector, + fields: MongoFindOptions, + callback: MongoCallback + ): void; + + /** * Execute a query and return the first result * @@ -158,4 +196,23 @@ declare interface MongoCollection options: MongoFindAndModifyOptions, callback: MongoCallback, ): void; + + + /** + * Creates an index on the collection + */ + createIndex( + fieldOrSpec: MongoIndexSpecification, + options: boolean, + callback: MongoCallback, + ): void; + + + /** + * Creates an index on the collection + */ + insert( + docs: MongoInsertSpecification, + callback: MongoCallback, + ): void; } diff --git a/test/bucket/delta.ts b/test/bucket/delta.ts index a729f29..ba1d192 100644 --- a/test/bucket/delta.ts +++ b/test/bucket/delta.ts @@ -89,96 +89,6 @@ describe( 'Delta', () => dest_data: { foo: [ '' ] }, expected: {}, }, - { - label: "", - src_data: { foo: [ 'bar', 'baz' ] }, - dest_data: { foo: [ 'change', 'baz' ] }, - data: { foo: [ 'change', 'baz' ] }, - diff: { foo: [ 'change' ] }, - }, - { - label: "", - src_data: { foo: [ 'bar', 'baz' ] }, - dest_data: { foo: [ 'bar', 'change' ] }, - data: { foo: [ 'bar', 'change' ] }, - diff: { foo: [ , 'change' ] }, - }, - { - label: "", - src_data: { foo: [ 'bar', 'baz' ] }, - dest_data: { foo: [ undefined, 'change' ] }, - data: { foo: [ 'bar', 'change' ] }, - diff: { foo: [ , 'change' ] }, - }, - { - label: "", - src_data: { foo: [ 'bar', 'baz' ] }, - dest_data: { foo: [ undefined, 'baz' ] }, - data: { foo: [ 'bar', 'baz' ] }, - diff: {}, - }, - { - label: "", - src_data: { foo: [ 'bar', 'baz' ] }, - dest_data: { foo: [ 'bar', undefined ] }, - data: { foo: [ 'bar', 'baz' ] }, - diff: {}, - }, - { - label: "", - src_data: { foo: [ 'bar', 'baz' ] }, - dest_data: { foo: [ 'bar', null ] }, - data: { foo: [ 'bar' ] }, - diff: { foo: [ , null ] }, - }, - { - label: "", - src_data: { foo: [ 'bar', 'baz' ] }, - dest_data: { foo: [ 'bar', 'baz', null ] }, - data: { foo: [ 'bar', 'baz' ] }, - diff: {}, - }, - { - label: "", - src_data: { foo: [ 'bar', 'baz' ] }, - dest_data: { foo: [ 'bar', 'baz', 'quux' ] }, - data: { foo: [ 'bar', 'baz', 'quux' ] }, - diff: { foo: [ , , 'quux' ]}, - }, - { - label: "", - src_data: { foo: [ 'bar', 'baz' ] }, - dest_data: { foo: [] }, - data: { foo: [ 'bar', 'baz' ] }, - diff: {}, - }, - - // null not at end of set means unchanged - { - label: "", - src_data: { foo: [ 'bar', 'baz', 'quux' ] }, - dest_data: { foo: [ null, null, 'quux' ] }, - data: { foo: [ 'bar', 'baz', 'quux' ] }, - diff: {}, - }, - // but the last one is - { - label: "", - src_data: { foo: [ 'bar', 'baz', 'quux' ] }, - dest_data: { foo: [ null, 'baz', null ] }, - data: { foo: [ 'bar', 'baz' ] }, - diff: { foo: [ , , null ] }, - }, - // given a string of nulls, only the last one is terminating; the - // rest are interpreted as undefined (because JSON serializes - // undefined values to `null' -_-) - { - label: "", - src_data: { foo: [ 'bar', 'baz', 'quux' ] }, - dest_data: { foo: [ null, null, null ] }, - data: { foo: [ 'bar', 'baz' ] }, - diff: { foo: [ , , null ] }, - }, ] ).forEach( ( { label, src_data, dest_data, expected } ) => { it( label, () => diff --git a/test/server/dapi/TokenedDataApiTest.ts b/test/server/dapi/TokenedDataApiTest.ts index 27375fa..0d0a3ad 100644 --- a/test/server/dapi/TokenedDataApiTest.ts +++ b/test/server/dapi/TokenedDataApiTest.ts @@ -40,7 +40,7 @@ describe( 'TokenedDataApi', () => const expected_ns = 'foo_ns'; - ( <[string, boolean, ( e: Error|null ) => void][]>[ + ( <[string, boolean, ( e: NullableError ) => void][]>[ [ "creates token and returns data if last_created", true, diff --git a/test/server/db/MongoServerDaoTest.js b/test/server/db/MongoServerDaoTest.js index 0fa5d30..d6c8bf1 100644 --- a/test/server/db/MongoServerDaoTest.js +++ b/test/server/db/MongoServerDaoTest.js @@ -18,173 +18,140 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - 'use strict'; - -const chai = require( 'chai' ); -const expect = chai.expect; -const { MongoServerDao: Sut } = require( '../../../' ).server.db; - - -describe( 'MongoServerDao', () => -{ - describe( '#saveQuote', () => - { - describe( "with no save data or push data", () => - { - it( "saves entire metabucket record individually", done => - { - const metadata = { - foo: [ 'bar', 'baz' ], - bar: [ { quux: 'quuux' } ], +Object.defineProperty(exports, "__esModule", { value: true }); +var MongoServerDao_1 = require("../../../src/server/db/MongoServerDao"); +var chai_1 = require("chai"); +chai_1.use(require('chai-as-promised')); +describe('MongoServerDao', function () { + describe('#saveQuote', function () { + describe("with no save data or push data", function () { + it("saves entire metabucket record individually", function (done) { + var metadata = { + foo: ['bar', 'baz'], + bar: [{ quux: 'quuux' }], }; - - const quote = createStubQuote( metadata ); - - const sut = Sut( createMockDb( - // update - ( selector, data ) => - { - expect( data.$set[ 'meta.foo' ] ) - .to.deep.equal( metadata.foo ); - - expect( data.$set[ 'meta.bar' ] ) - .to.deep.equal( metadata.bar ); - - - expect( data.$push ).to.equal( undefined ); - - done(); - } - ) ); - - sut.init( () => - sut.saveQuote( quote, () => {}, () => {} ) - ); - } ); - } ); - - describe( "with push data", () => - { - it( "adds push data to the collection", done => - { - const push_data = { - foo: [ 'bar', 'baz' ], - bar: [ { quux: 'quuux' } ], + var quote = createStubQuote(metadata); + var sut = new MongoServerDao_1.MongoServerDao(createMockDb( + // update + function (_selector, data) { + chai_1.expect(data.$set['meta.foo']) + .to.deep.equal(metadata.foo); + chai_1.expect(data.$set['meta.bar']) + .to.deep.equal(metadata.bar); + chai_1.expect(data.$push).to.equal(undefined); + done(); + })); + sut.init(function () { + return sut.saveQuote(quote, function () { }, function () { }); + }); + }); + }); + describe("with push data", function () { + it("adds push data to the collection", function (done) { + var push_data = { + foo: ['bar', 'baz'], + bar: [{ quux: 'quuux' }], }; - - const quote = createStubQuote( {} ); - - const sut = Sut( createMockDb( - // update - ( selector, data ) => - { - expect( data.$push[ 'foo' ] ) - .to.deep.equal( push_data.foo ); - - expect( data.$push[ 'bar' ] ) - .to.deep.equal( push_data.bar ); - - done(); - } - ) ); - - sut.init( () => - sut.saveQuote( - quote, - () => {}, - () => {}, - undefined, - push_data - ) - ); - } ); - - it( "skips push data when it is an empty object", done => - { - const push_data = {}; - - const quote = createStubQuote( {} ); - - const sut = Sut( createMockDb( - // update - ( selector, data ) => - { - expect( data.$push ).to.equal( undefined ); - - done(); - } - ) ); - - sut.init( () => - sut.saveQuote( - quote, - () => {}, - () => {}, - undefined, - push_data - ) - ); - } ); - } ); - } ); -} ); - - -function createMockDb( on_update ) -{ - const collection_quotes = { + var quote = createStubQuote({}); + var sut = new MongoServerDao_1.MongoServerDao(createMockDb( + // update + function (_selector, data) { + chai_1.expect(data.$push['foo']) + .to.deep.equal(push_data.foo); + chai_1.expect(data.$push['bar']) + .to.deep.equal(push_data.bar); + done(); + })); + sut.init(function () { + return sut.saveQuote(quote, function () { }, function () { }, undefined, push_data); + }); + }); + it("skips push data when it is an empty object", function (done) { + var push_data = {}; + var quote = createStubQuote({}); + var sut = new MongoServerDao_1.MongoServerDao(createMockDb( + // update + function (_selector, data) { + chai_1.expect(data.$push).to.equal(undefined); + done(); + })); + sut.init(function () { + return sut.saveQuote(quote, function () { }, function () { }, undefined, push_data); + }); + }); + }); + }); +}); +function createMockDb(on_update) { + var collection_quotes = { update: on_update, - createIndex: ( _, __, c ) => c(), + createIndex: function (_, __, c) { return c(); }, }; - - const collection_seq = { - find( _, __, c ) - { - c( null, { - toArray: c => c( null, { length: 5 } ), - } ); + var collection_seq = { + find: function (_, __, c) { + c(null, { + toArray: function (c) { return c(null, { length: 5 }); }, + }); }, }; - - const db = { - collection( id, c ) - { - const coll = ( id === 'quotes' ) + var db = { + collection: function (id, c) { + var coll = (id === 'quotes') ? collection_quotes : collection_seq; - - c( null, coll ); + c(null, coll); }, }; - - const driver = { - open: c => c( null, db ), - on: () => {}, + var driver = { + open: function (c) { return c(null, db); }, + on: function () { }, }; - return driver; } - - -function createStubQuote( metadata ) -{ - return { - getBucket: () => ( { - getData: () => {}, - } ), - - getMetabucket: () => ( { - getData: () => metadata, - } ), - - getId: () => 1, - getProgramVersion: () => 0, - getLastPremiumDate: () => 0, - getRatedDate: () => 0, - getExplicitLockReason: () => "", - getExplicitLockStep: () => 0, - isImported: () => false, - isBound: () => false, +function createStubQuote(metadata) { + var program = { + getId: function () { return '1'; }, + ineligibleLockCount: 0, + apis: {}, + internal: {}, + meta: { + arefs: {}, + fields: {}, + groups: {}, + qdata: {}, + qtypes: {}, + }, + mapis: {}, + initQuote: function () { }, }; + var quote = { + getBucket: function () { return ({ + getData: function () { }, + }); }, + getMetabucket: function () { return ({ + getData: function () { return metadata; }, + }); }, + getId: function () { return 123; }, + getProgramVersion: function () { return 'Foo'; }, + getLastPremiumDate: function () { return 0; }, + getRatedDate: function () { return 0; }, + getExplicitLockReason: function () { return ""; }, + getExplicitLockStep: function () { return 1; }, + isImported: function () { return false; }, + isBound: function () { return false; }, + getTopVisitedStepId: function () { return 1; }, + getTopSavedStepId: function () { return 1; }, + setRatedDate: function () { return quote; }, + setRateBucket: function () { return quote; }, + setRatingData: function () { return quote; }, + getRatingData: function () { return ({ _unavailable_all: '0' }); }, + getProgram: function () { return program; }, + setExplicitLock: function () { return quote; }, + getProgramId: function () { return 'Foo'; }, + getCurrentStepId: function () { return 0; }, + setLastPremiumDate: function () { return quote; }, + }; + return quote; } +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiTW9uZ29TZXJ2ZXJEYW9UZXN0LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiTW9uZ29TZXJ2ZXJEYW9UZXN0LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBbUJHO0FBRUgsWUFBWSxDQUFDOztBQUViLHdFQUE4RTtBQUU5RSw2QkFBK0M7QUFRL0MsVUFBUSxDQUFFLE9BQU8sQ0FBRSxrQkFBa0IsQ0FBRSxDQUFFLENBQUM7QUFHMUMsUUFBUSxDQUFFLGdCQUFnQixFQUFFO0lBRXhCLFFBQVEsQ0FBRSxZQUFZLEVBQUU7UUFFcEIsUUFBUSxDQUFFLGdDQUFnQyxFQUFFO1lBRXhDLEVBQUUsQ0FBRSw2Q0FBNkMsRUFBRSxVQUFBLElBQUk7Z0JBRW5ELElBQU0sUUFBUSxHQUFHO29CQUNiLEdBQUcsRUFBRSxDQUFFLEtBQUssRUFBRSxLQUFLLENBQUU7b0JBQ3JCLEdBQUcsRUFBRSxDQUFFLEVBQUUsSUFBSSxFQUFFLE9BQU8sRUFBRSxDQUFFO2lCQUM3QixDQUFDO2dCQUVGLElBQU0sS0FBSyxHQUFHLGVBQWUsQ0FBRSxRQUFRLENBQUUsQ0FBQztnQkFFMUMsSUFBTSxHQUFHLEdBQUcsSUFBSSwrQkFBRyxDQUFFLFlBQVk7Z0JBQzdCLFNBQVM7Z0JBQ1QsVUFBRSxTQUF3QixFQUFFLElBQWlCO29CQUV6QyxhQUFNLENBQUUsSUFBSSxDQUFDLElBQUksQ0FBRSxVQUFVLENBQUUsQ0FBRTt5QkFDNUIsRUFBRSxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUUsUUFBUSxDQUFDLEdBQUcsQ0FBRSxDQUFDO29CQUVuQyxhQUFNLENBQUUsSUFBSSxDQUFDLElBQUksQ0FBRSxVQUFVLENBQUUsQ0FBRTt5QkFDNUIsRUFBRSxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUUsUUFBUSxDQUFDLEdBQUcsQ0FBRSxDQUFDO29CQUduQyxhQUFNLENBQUUsSUFBSSxDQUFDLEtBQUssQ0FBRSxDQUFDLEVBQUUsQ0FBQyxLQUFLLENB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 \ No newline at end of file diff --git a/test/server/db/MongoServerDaoTest.ts b/test/server/db/MongoServerDaoTest.ts new file mode 100644 index 0000000..b0a83bb --- /dev/null +++ b/test/server/db/MongoServerDaoTest.ts @@ -0,0 +1,227 @@ +/** + * Tests MongoServerDao + * + * 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 . + */ + +'use strict'; + +import { MongoServerDao as Sut } from "../../../src/server/db/MongoServerDao"; +import { MongoSelector, MongoUpdate } from "mongodb"; +import { expect, use as chai_use } from 'chai'; +import { ServerSideQuote } from "../../../src/server/quote/ServerSideQuote"; +import { PositiveInteger } from "../../../src/numeric"; +import { Program } from "../../../src/program/Program"; +import { RateResult } from "../../../src/server/rater/Rater"; +import { QuoteDataBucket } from "../../../src/bucket/QuoteDataBucket"; +import { QuoteId } from "../../../src/quote/Quote"; + +chai_use( require( 'chai-as-promised' ) ); + + +describe( 'MongoServerDao', () => +{ + describe( '#saveQuote', () => + { + describe( "with no save data or push data", () => + { + it( "saves entire metabucket record individually", done => + { + const metadata = { + foo: [ 'bar', 'baz' ], + bar: [ { quux: 'quuux' } ], + }; + + const quote = createStubQuote( metadata ); + + const sut = new Sut( createMockDb( + // update + ( _selector: MongoSelector, data: MongoUpdate ) => + { + expect( data.$set[ 'meta.foo' ] ) + .to.deep.equal( metadata.foo ); + + expect( data.$set[ 'meta.bar' ] ) + .to.deep.equal( metadata.bar ); + + + expect( data.$push ).to.equal( undefined ); + + done(); + } + ) ); + + sut.init( () => + sut.saveQuote( quote, () => {}, () => {} ) + ); + } ); + } ); + + describe( "with push data", () => + { + it( "adds push data to the collection", done => + { + const push_data = { + foo: [ 'bar', 'baz' ], + bar: [ { quux: 'quuux' } ], + }; + + const quote = createStubQuote( {} ); + + const sut = new Sut( createMockDb( + // update + (_selector: MongoSelector, data: MongoUpdate ) => + { + expect( data.$push[ 'foo' ] ) + .to.deep.equal( push_data.foo ); + + expect( data.$push[ 'bar' ] ) + .to.deep.equal( push_data.bar ); + + done(); + } + ) ); + + sut.init( () => + sut.saveQuote( + quote, + () => {}, + () => {}, + undefined, + push_data + ) + ); + } ); + + it( "skips push data when it is an empty object", done => + { + const push_data = {}; + + const quote = createStubQuote( {} ); + + const sut = new Sut( createMockDb( + // update + ( _selector: MongoSelector, data: MongoUpdate ) => + { + expect( data.$push ).to.equal( undefined ); + + done(); + } + ) ); + + sut.init( () => + sut.saveQuote( + quote, + () => {}, + () => {}, + undefined, + push_data + ) + ); + } ); + } ); + } ); +} ); + + +function createMockDb( on_update: any ) +{ + const collection_quotes = { + update: on_update, + createIndex: ( _: any, __: any, c: any ) => c(), + }; + + const collection_seq = { + find( _: any, __: any, c: any ) + { + c( null, { + toArray: ( c: any ) => c( null, { length: 5 } ), + } ); + }, + }; + + const db = { + collection( id: any, c: any ) + { + const coll = ( id === 'quotes' ) + ? collection_quotes + : collection_seq; + + c( null, coll ); + }, + }; + + const driver = { + open: ( c: any ) => c( null, db ), + on: () => {}, + }; + + return driver; +} + + +function createStubQuote( metadata: Record ) +{ + const program = { + getId: () => '1', + ineligibleLockCount: 0, + apis: {}, + internal: {}, + meta: { + arefs: {}, + fields: {}, + groups: {}, + qdata: {}, + qtypes: {}, + }, + mapis: {}, + initQuote: () => {}, + }; + + const quote = { + getBucket: () => ( { + getData: () => {}, + } ), + + getMetabucket: () => ( { + getData: () => metadata, + } ), + + getId: () => 123, + getProgramVersion: () => 'Foo', + getLastPremiumDate: () => 0, + getRatedDate: () => 0, + getExplicitLockReason: () => "", + getExplicitLockStep: () => 1, + isImported: () => false, + isBound: () => false, + getTopVisitedStepId: () => 1, + getTopSavedStepId: () => 1, + setRatedDate: () => quote, + setRateBucket: () => quote, + setRatingData: () => quote, + getRatingData: () => { _unavailable_all: '0' }, + getProgram: () => program, + setExplicitLock: () => quote, + getProgramId: () => 'Foo', + getCurrentStepId: () => 0, + setLastPremiumDate: () => quote, + }; + + return quote; +} diff --git a/test/server/request/DataProcessorTest.ts b/test/server/request/DataProcessorTest.ts index a3a4d0e..2be9d61 100644 --- a/test/server/request/DataProcessorTest.ts +++ b/test/server/request/DataProcessorTest.ts @@ -713,7 +713,44 @@ function createStubQuote() getBucket() { return new QuoteDataBucket(); - } + }, + + getMetabucket(){ + return new QuoteDataBucket(); + }, + + getProgramVersion(){ + return 'Foo'; + }, + + getExplicitLockReason(){ + return 'Reason'; + }, + + getExplicitLockStep() + { + return 1; + }, + + isImported() + { + return true; + }, + + isBound() + { + return true; + }, + + getTopVisitedStepId() + { + return 1; + }, + + getTopSavedStepId() + { + return 1; + }, }; } diff --git a/test/server/service/RatingServiceTest.ts b/test/server/service/RatingServiceTest.ts index 8896c49..546118c 100644 --- a/test/server/service/RatingServiceTest.ts +++ b/test/server/service/RatingServiceTest.ts @@ -36,6 +36,7 @@ import { UserRequest } from "../../../src/server/request/UserRequest"; import { UserResponse } from "../../../src/server/request/UserResponse"; import { UserSession } from "../../../src/server/request/UserSession"; import { QuoteDataBucket } from "../../../src/bucket/QuoteDataBucket"; +import { PositiveInteger } from "../../../src/numeric"; import { Kv } from "../../../src/bucket/delta"; import { @@ -573,19 +574,27 @@ function getStubs() const response = {}; const quote = { - getProgramId: () => program_id, - getProgram: () => program, - getId: () => 0, - setLastPremiumDate: () => quote, - setRatedDate: () => quote, - getRatedDate: () => 0, - getLastPremiumDate: () => 0, - getCurrentStepId: () => 0, - setExplicitLock: () => quote, - setRateBucket: () => quote, - setRatingData: () => quote, - getRatingData: () => stub_rate_data, - getBucket: () => new QuoteDataBucket(), + getProgramId: () => program_id, + getProgram: () => program, + getId: () => 0, + setLastPremiumDate: () => quote, + setRatedDate: () => quote, + getRatedDate: () => 0, + getLastPremiumDate: () => 0, + getCurrentStepId: () => 0, + setExplicitLock: () => quote, + setRateBucket: () => quote, + setRatingData: () => quote, + getRatingData: () => stub_rate_data, + getBucket: () => new QuoteDataBucket(), + getMetabucket: () => new QuoteDataBucket(), + getProgramVersion: () => 'Foo', + getExplicitLockReason: () => 'Reason', + getExplicitLockStep: () => 1, + isImported: () => true, + isBound: () => true, + getTopVisitedStepId: () => 1, + getTopSavedStepId: () => 1, }; return { diff --git a/test/server/token/MongoTokenDaoTest.ts b/test/server/token/MongoTokenDaoTest.ts index 167685a..23e403e 100644 --- a/test/server/token/MongoTokenDaoTest.ts +++ b/test/server/token/MongoTokenDaoTest.ts @@ -26,7 +26,7 @@ import { } from "../../../src/server/token/TokenDao"; import { MongoTokenDao as Sut } from "../../../src/server/token/MongoTokenDao"; - +import { MongoCollection } from "mongodb"; import { TokenId, TokenNamespace, @@ -248,6 +248,9 @@ describe( 'server.token.TokenDao', () => update() {}, findOne() {}, + find() {}, + createIndex() {}, + insert() {}, }; return expect( @@ -269,6 +272,9 @@ describe( 'server.token.TokenDao', () => update() {}, findOne() {}, + find() {}, + createIndex() {}, + insert() {}, }; return expect( @@ -477,6 +483,9 @@ describe( 'server.token.TokenDao', () => update() {}, findAndModify() {}, + find() {}, + createIndex() {}, + insert() {}, }; const result = new Sut( coll, field, () => 0 ) @@ -520,6 +529,9 @@ describe( 'server.token.TokenDao', () => update() {}, findAndModify() {}, + find() {}, + createIndex() {}, + insert() {}, }; return expect(