Data API liberation and improvment
Not all aspects of the Data API are liberated quite yet, but this is the bulk of it.master
commit
2722b0b29a
|
@ -0,0 +1,117 @@
|
||||||
|
/**
|
||||||
|
* Generalized key-value store
|
||||||
|
*
|
||||||
|
* Copyright (C) 2016 LoVullo Associates, Inc.
|
||||||
|
*
|
||||||
|
* This file is part of the Liza Data Collection Framework
|
||||||
|
*
|
||||||
|
* liza is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* There's a hole in my bucket, dear Liza, dear Liza [...]
|
||||||
|
*/
|
||||||
|
|
||||||
|
var Interface = require( 'easejs' ).Interface;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an object that is able to store key-value data with multiple
|
||||||
|
* indexes per value
|
||||||
|
*/
|
||||||
|
module.exports = Interface( 'Bucket',
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Explicitly sets the contents of the bucket
|
||||||
|
*
|
||||||
|
* @param {Object.<string,Array>} data associative array of the data
|
||||||
|
*
|
||||||
|
* @param {boolean} merge_index whether to merge indexes individually
|
||||||
|
* @param {boolean} merge_null whether to merge undefined values (vs
|
||||||
|
* ignore)
|
||||||
|
*
|
||||||
|
* @return {Bucket} self
|
||||||
|
*/
|
||||||
|
'public setValues': [ 'data', 'merge_index', 'merge_null' ],
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overwrites values in the original bucket
|
||||||
|
*
|
||||||
|
* @param {Object.<string,Array>} data associative array of the data
|
||||||
|
*
|
||||||
|
* @return {Bucket} self
|
||||||
|
*/
|
||||||
|
'public overwriteValues': [ 'data' ],
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all data from the bucket
|
||||||
|
*
|
||||||
|
* @return {Bucket} self
|
||||||
|
*/
|
||||||
|
'public clear': [],
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls a function for each each of the values in the bucket
|
||||||
|
*
|
||||||
|
* Note: This format is intended to be consistent with Array.forEach()
|
||||||
|
*
|
||||||
|
* @param {function( Object, number )} callback function to call for each
|
||||||
|
* value in the bucket
|
||||||
|
*
|
||||||
|
* @return {Bucket} self
|
||||||
|
*/
|
||||||
|
'public each': [ 'callback' ],
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the data for the requested field
|
||||||
|
*
|
||||||
|
* @param {string} name field name (with or without trailing brackets)
|
||||||
|
*
|
||||||
|
* @return {Array} data for the field, or empty array if none
|
||||||
|
*/
|
||||||
|
'public getDataByName': [ 'name' ],
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the data as a JSON string
|
||||||
|
*
|
||||||
|
* @return {string} data represented as JSON
|
||||||
|
*/
|
||||||
|
'public getDataJson': [],
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return raw bucket data
|
||||||
|
*
|
||||||
|
* TODO: remove; breaks encapsulation
|
||||||
|
*
|
||||||
|
* @return {Object} raw bucket data
|
||||||
|
*/
|
||||||
|
'public getData': [],
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls a function for each each of the values in the bucket matching the
|
||||||
|
* given predicate
|
||||||
|
*
|
||||||
|
* @param {function(string)} pred predicate
|
||||||
|
* @param {function( Object, number )} callback function to call for each
|
||||||
|
* value in the bucket
|
||||||
|
*
|
||||||
|
* @return {Bucket} self
|
||||||
|
*/
|
||||||
|
'public filter': [ 'pred', 'callback' ]
|
||||||
|
} );
|
|
@ -0,0 +1,90 @@
|
||||||
|
/**
|
||||||
|
* Retrieves API data from bucket
|
||||||
|
*
|
||||||
|
* Copyright (C) 2016 LoVullo Associates, Inc.
|
||||||
|
*
|
||||||
|
* This file is part of the Liza Data Collection Framework
|
||||||
|
*
|
||||||
|
* liza is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var Class = require( 'easejs' ).Class,
|
||||||
|
DataApi = require( './DataApi' ),
|
||||||
|
Bucket = require( '../bucket/Bucket' );
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve data from the bucket
|
||||||
|
*/
|
||||||
|
module.exports = Class( 'BucketDataApi' )
|
||||||
|
.implement( DataApi )
|
||||||
|
.extend(
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Bucket to use as data source
|
||||||
|
* @type {Bucket}
|
||||||
|
*/
|
||||||
|
'private _bucket': null,
|
||||||
|
|
||||||
|
'private _params': {},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize data API
|
||||||
|
*
|
||||||
|
* @param {string} url service URL
|
||||||
|
* @param {RestDataApiStrategy} strategy request strategy
|
||||||
|
*/
|
||||||
|
__construct: function( bucket, params )
|
||||||
|
{
|
||||||
|
if ( !( Class.isA( Bucket, bucket ) ) )
|
||||||
|
{
|
||||||
|
throw Error( "Invalid bucket provided" );
|
||||||
|
}
|
||||||
|
|
||||||
|
this._bucket = bucket;
|
||||||
|
this._params = params;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request data from the bucket
|
||||||
|
*
|
||||||
|
* @param {Object} data request params
|
||||||
|
* @param {function(Object)} callback server response callback
|
||||||
|
*
|
||||||
|
* @return {BucketDataApi} self
|
||||||
|
*/
|
||||||
|
'public request': function( data, callback )
|
||||||
|
{
|
||||||
|
var _self = this.__inst,
|
||||||
|
rows = [];
|
||||||
|
|
||||||
|
for ( var i in this._params )
|
||||||
|
{
|
||||||
|
var field = this._params[ i ],
|
||||||
|
fdata = this._bucket.getDataByName( field );
|
||||||
|
|
||||||
|
for ( var index in fdata )
|
||||||
|
{
|
||||||
|
rows[ index ] = rows[ index ] || {};
|
||||||
|
rows[ index ][ field ] = fdata[ index ];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callback( null, rows );
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
} );
|
|
@ -0,0 +1,683 @@
|
||||||
|
/**
|
||||||
|
* Manages DataAPI requests and return data
|
||||||
|
*
|
||||||
|
* Copyright (C) 2016 LoVullo Associates, Inc.
|
||||||
|
*
|
||||||
|
* This file is part of the Liza Data Collection Framework
|
||||||
|
*
|
||||||
|
* liza is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var Class = require( 'easejs' ).Class,
|
||||||
|
EventEmitter = require( 'events' ).EventEmitter;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pends and manages API calls and return data
|
||||||
|
*
|
||||||
|
* TODO: Extracted pretty much verbatim from Program; needs refactoring
|
||||||
|
*/
|
||||||
|
module.exports = Class( 'DataApiManager' )
|
||||||
|
.extend( EventEmitter,
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Factory used to create data APIs
|
||||||
|
* @type {DataApiFactory}
|
||||||
|
*/
|
||||||
|
'private _dataApiFactory': null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DataApi instances, indexed by API id
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
'private _dataApis': {},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data returned for fields via a data API, per index, formatted
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
'private _fieldData': {},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data returned for fields via a data API, per index, unformatted
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
'private _fieldRawData': {},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pending API calls (by tracking identifier, not API id)
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
'private _pendingApiCall': {},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API calls queued for request
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
'private _queuedApiCall': {},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stack depth for field updates (recursion detection)
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
'private _fieldUpdateDepth': {},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether new field data has been emitted
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
'private _fieldDataEmitted': {},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Id of timer to process API queue
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
'private _fieldApiTimer': 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fields that require API requests
|
||||||
|
* @type {Object}}
|
||||||
|
*/
|
||||||
|
'private _fieldStale': {},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API descriptions
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
'private _apis': {},
|
||||||
|
|
||||||
|
|
||||||
|
__construct: function( api_factory )
|
||||||
|
{
|
||||||
|
this._dataApiFactory = api_factory;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set available APIs
|
||||||
|
*
|
||||||
|
* TODO: Remove me; pass via ctor
|
||||||
|
* TODO: Document API definition format
|
||||||
|
*
|
||||||
|
* @param {Object} apis API definitions
|
||||||
|
*
|
||||||
|
* @return {DataApiManager} self
|
||||||
|
*/
|
||||||
|
'public setApis': function( apis )
|
||||||
|
{
|
||||||
|
this._apis = apis;
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve data from the API identified by the given id
|
||||||
|
*
|
||||||
|
* The optional request id permits cancelling requests if necessary.
|
||||||
|
*
|
||||||
|
* TODO: refactor argument list; it's just been built upon too much and
|
||||||
|
* needs reordering
|
||||||
|
*
|
||||||
|
* @param {string} api API id
|
||||||
|
* @param {Object} data API arguments (key-value)
|
||||||
|
* @param {function(Object)} callback callback to contain response
|
||||||
|
* @param {string} name element name for tracking
|
||||||
|
* @param {number} index index for tracking
|
||||||
|
* @param {bucket} bucket optional bucket to use as data source
|
||||||
|
* @param {function(Error)} fc failure continuation
|
||||||
|
*
|
||||||
|
* @return {Program} self
|
||||||
|
*/
|
||||||
|
'public getApiData': function( api, data, callback, name, index, bucket, fc )
|
||||||
|
{
|
||||||
|
var id = ( name === undefined )
|
||||||
|
? ( ( new Date() ).getTime() )
|
||||||
|
: name + '_' + index;
|
||||||
|
|
||||||
|
var _self = this;
|
||||||
|
|
||||||
|
if ( !( this._apis[ api ] ) )
|
||||||
|
{
|
||||||
|
this.emit( 'error', Error( 'Unknown data API: ' + api ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the API if necessary (lazy-load); otherwise, use the existing
|
||||||
|
// instance
|
||||||
|
var api = this._dataApis[ api ] || ( function()
|
||||||
|
{
|
||||||
|
var apidesc = _self._apis[ api ];
|
||||||
|
|
||||||
|
// create a new instance of the API
|
||||||
|
return _self._dataApis[ api ] = _self._dataApiFactory.fromType(
|
||||||
|
apidesc.type, apidesc, bucket
|
||||||
|
).on( 'error', function( e )
|
||||||
|
{
|
||||||
|
_self.emit( 'error', e );
|
||||||
|
} );
|
||||||
|
} )();
|
||||||
|
|
||||||
|
// this has the effect of wiping out previous requests of the same id,
|
||||||
|
// ensuring that we will make only the most recent request
|
||||||
|
this._queuedApiCall[ id ] = function()
|
||||||
|
{
|
||||||
|
// mark this request as pending (note that we aren't storing and
|
||||||
|
// references to this object because we do not want a reference to
|
||||||
|
// be used---the entire object may be reassigned by something else
|
||||||
|
// in order to wipe out all values)
|
||||||
|
var uid = ( ( new Date() ).getTime() );
|
||||||
|
_self._pendingApiCall[ id ] = {
|
||||||
|
uid: uid,
|
||||||
|
name: name,
|
||||||
|
index: index
|
||||||
|
};
|
||||||
|
|
||||||
|
// process the request; we'll let them know when it comes back
|
||||||
|
try
|
||||||
|
{
|
||||||
|
api.request( data, function()
|
||||||
|
{
|
||||||
|
// we only wish to populate the field if the request should
|
||||||
|
// still be considered pending
|
||||||
|
var curuid = ( _self._pendingApiCall[ id ] || {} ).uid;
|
||||||
|
if ( curuid === uid )
|
||||||
|
{
|
||||||
|
// forward to the caller
|
||||||
|
callback.apply( this, arguments );
|
||||||
|
|
||||||
|
// clear the pending flag
|
||||||
|
_self._pendingApiCall[ id ] = undefined;
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
catch ( e )
|
||||||
|
{
|
||||||
|
fc( e );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// field is about to be re-loaded
|
||||||
|
this.fieldStale( name, index, false );
|
||||||
|
|
||||||
|
this._setFieldApiTimer();
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get pending API calls
|
||||||
|
*
|
||||||
|
* TODO: Added to support a progressive refactoring; this breaks
|
||||||
|
* encapsulation and should be removed, or formalized.
|
||||||
|
*
|
||||||
|
* Returned object contains uid, name, and index fields.
|
||||||
|
*
|
||||||
|
* @return {Object} pending API calls
|
||||||
|
*/
|
||||||
|
'public getPendingApiCalls': function()
|
||||||
|
{
|
||||||
|
return this._pendingApiCall;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks field for re-loading
|
||||||
|
*
|
||||||
|
* Stale fields will not be considered to have data, but the data
|
||||||
|
* will remain in memory until the next request.
|
||||||
|
*
|
||||||
|
* @param {string} field field name
|
||||||
|
* @param {number} index field index
|
||||||
|
* @param {?boolean} stale whether field is stale
|
||||||
|
*
|
||||||
|
* @return {DataApiManager} self
|
||||||
|
*/
|
||||||
|
'public fieldStale': function( field, index, stale )
|
||||||
|
{
|
||||||
|
stale = ( stale === undefined ) ? true : !!stale;
|
||||||
|
|
||||||
|
this._fieldStale[ field ] = this.fieldStale[ field ] || [];
|
||||||
|
this._fieldStale[ field ][ index ] = stale;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether field is marked stale
|
||||||
|
*
|
||||||
|
* @param {string} field field name
|
||||||
|
* @param {number} index field index
|
||||||
|
*
|
||||||
|
* @return {boolean} whether field is stale
|
||||||
|
*/
|
||||||
|
'protected isFieldStale': function( field, index )
|
||||||
|
{
|
||||||
|
return ( this._fieldStale[ field ] || [] )[ index ] === true;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
'public fieldNotReady': function( id, i, bucket )
|
||||||
|
{
|
||||||
|
if ( !( this.hasFieldData( id, i ) ) )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// failure means that we don't have all the necessary params; clear the
|
||||||
|
// field
|
||||||
|
this.clearFieldData( id, i );
|
||||||
|
|
||||||
|
// clear the value of this field (IMPORTANT: do this *after* clearing
|
||||||
|
// the field data, since the empty value may otherwise be invalid);
|
||||||
|
// ***note that this will also clear any bucket values associated with
|
||||||
|
// this field, because this will trigger the change event for this
|
||||||
|
// field***
|
||||||
|
if ( bucket.hasIndex( id, i ) )
|
||||||
|
{
|
||||||
|
var data={};
|
||||||
|
data[ id ] = [];
|
||||||
|
data[ id ][ i ] = '';
|
||||||
|
|
||||||
|
// the second argument ensures that we merge indexes, rather than
|
||||||
|
// overwrite the entire value (see FS#11224)
|
||||||
|
bucket.setValues( data, true );
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
'private _setFieldApiTimer': function()
|
||||||
|
{
|
||||||
|
// no use in re-setting
|
||||||
|
if ( this._fieldApiTimer )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var _self = this;
|
||||||
|
this._fieldApiTimer = setTimeout( function()
|
||||||
|
{
|
||||||
|
_self.processFieldApiCalls();
|
||||||
|
}, 0 );
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
'public processFieldApiCalls': function()
|
||||||
|
{
|
||||||
|
// this may trigger more requests, so be prepared with a fresh queue
|
||||||
|
var oldqueue = this._queuedApiCall;
|
||||||
|
this._fieldApiTimer = 0;
|
||||||
|
this._queuedApiCall = {};
|
||||||
|
|
||||||
|
for ( var c in oldqueue )
|
||||||
|
{
|
||||||
|
if ( oldqueue[c] === undefined )
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// perform the API call.
|
||||||
|
oldqueue[c]();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set API return data for a given field
|
||||||
|
*
|
||||||
|
* @param {string} name field name
|
||||||
|
* @param {number} index field index
|
||||||
|
* @param {Array.<Object>} data return data set
|
||||||
|
* @param {string} value param to map to value
|
||||||
|
* @param {string} label param to map to label
|
||||||
|
*
|
||||||
|
* @return {Program} self
|
||||||
|
*/
|
||||||
|
'public setFieldData': function( name, index, data, value, label, unchanged )
|
||||||
|
{
|
||||||
|
if ( !this._fieldData[ name ] )
|
||||||
|
{
|
||||||
|
this._fieldData[ name ] = [];
|
||||||
|
this._fieldDataEmitted[ name ] = [];
|
||||||
|
}
|
||||||
|
if ( !( this._fieldRawData[ name ] ) )
|
||||||
|
{
|
||||||
|
this._fieldRawData[ name ] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var fdata = this._fieldData[ name ][ index ] = {};
|
||||||
|
|
||||||
|
// store the raw return data in addition to our own formatted data below
|
||||||
|
this._fieldRawData[ name ][ index ] = data;
|
||||||
|
this._fieldDataEmitted[ name ][ index ] = false;
|
||||||
|
|
||||||
|
// store the data by value, not by index (as it is currently stored); we
|
||||||
|
// will not have access to that information without querying the DOM or
|
||||||
|
// iterating through the array, both of which are terrible ideas
|
||||||
|
for ( var i in data )
|
||||||
|
{
|
||||||
|
var data_value = data[ i ][ value ];
|
||||||
|
|
||||||
|
// if this value is already set, then it is not unique and will
|
||||||
|
// cause some obvious problems
|
||||||
|
if ( fdata[ data_value ] )
|
||||||
|
{
|
||||||
|
this.emit( 'error', Error(
|
||||||
|
'Value is not unique for ' + name + ': ' + data_value
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// simply index the same data by the value field
|
||||||
|
fdata[ data_value ] = data[ i ];
|
||||||
|
}
|
||||||
|
|
||||||
|
// empty flag
|
||||||
|
fdata.___empty = ( data.length === 0 );
|
||||||
|
|
||||||
|
// generate the field data that may be used to populate the UI (note
|
||||||
|
// that we include fdata since that allows the caller to quickly look up
|
||||||
|
// if a given value is in the list)
|
||||||
|
this.triggerFieldUpdate( name, index, value, label, unchanged );
|
||||||
|
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
'public triggerFieldUpdate': function(
|
||||||
|
name, index, value, label, unchanged
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var fdata = this._fieldData[ name ][ index ],
|
||||||
|
data = this._fieldRawData[ name ][ index ];
|
||||||
|
|
||||||
|
// if no data could be found, try the "combined" index
|
||||||
|
if ( !fdata )
|
||||||
|
{
|
||||||
|
fdata = this._fieldData[ name ][ -1 ];
|
||||||
|
}
|
||||||
|
if ( !data )
|
||||||
|
{
|
||||||
|
data = this._fieldRawData[ name ][ -1 ];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !data || !fdata )
|
||||||
|
{
|
||||||
|
// still no data, then error
|
||||||
|
this.emit( 'error', Error(
|
||||||
|
'updateFieldData missing data for ' +
|
||||||
|
name + '[' + index + ']'
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there has no change, and we have already announced this data, then
|
||||||
|
// do nothing
|
||||||
|
if ( unchanged && this._fieldDataEmitted[ name ][ index ] )
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fdepth = this._fieldUpdateDepth[ name ] =
|
||||||
|
this._fieldUpdateDepth[ name ] || [];
|
||||||
|
|
||||||
|
// protect against recursive updates which may happen if an update hook
|
||||||
|
// triggers another update
|
||||||
|
fdepth[ index ] = fdepth[ index ] || 0;
|
||||||
|
if ( fdepth[ index ] > 0 )
|
||||||
|
{
|
||||||
|
// if the value is identical, then simply abort without displaying
|
||||||
|
// an error; otherwise, we have a problem
|
||||||
|
if ( !unchanged )
|
||||||
|
{
|
||||||
|
// this should not happen.
|
||||||
|
this.emit( 'error', RangeError(
|
||||||
|
'updateFieldData recursion on ' + name + '[' + index + ']'
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fdepth[ index ]++;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
this._fieldDataEmitted[ name ][ index ] = true;
|
||||||
|
this.emit( 'updateFieldData',
|
||||||
|
name, index, this._genUiFieldData( data, value, label ), fdata
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch ( e )
|
||||||
|
{
|
||||||
|
this.emit( 'error', Error(
|
||||||
|
'updateFieldData hook error: ' + e.message
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
fdepth[ index ]--;
|
||||||
|
return !unchanged;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the given field has any result data associated with it
|
||||||
|
*
|
||||||
|
* @param {string} name field name
|
||||||
|
* @param {number} index field index
|
||||||
|
*
|
||||||
|
* @return {boolean} true if result data exists for field, otherwise false
|
||||||
|
*/
|
||||||
|
'public hasFieldData': function( name, index )
|
||||||
|
{
|
||||||
|
// default to "combined" index of -1 if no index is provided
|
||||||
|
index = ( index === undefined ) ? -1 : +index;
|
||||||
|
|
||||||
|
if ( this.isFieldStale( name, index ) )
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ( ( this._fieldData[ name ] || {} )[ index ] )
|
||||||
|
? true
|
||||||
|
: false;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate label and value objects for the given result set
|
||||||
|
*
|
||||||
|
* This data is ideal for updating a UI and contains no extra information.
|
||||||
|
*
|
||||||
|
* @param {Array.<Object>} data return data
|
||||||
|
* @param {string} value param to map to value
|
||||||
|
* @param {string} label param to map to label
|
||||||
|
*
|
||||||
|
* @return {Array.<Object>} value and label data set
|
||||||
|
*/
|
||||||
|
'private _genUiFieldData': function( data, value, label )
|
||||||
|
{
|
||||||
|
var ret = [],
|
||||||
|
len = data.length;
|
||||||
|
|
||||||
|
for ( var i = 0; i < len; i++ )
|
||||||
|
{
|
||||||
|
var idata = data[ i ];
|
||||||
|
|
||||||
|
ret[ i ] = {
|
||||||
|
value: idata[ value ],
|
||||||
|
label: idata[ label ]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all API response data associated with a given field
|
||||||
|
*
|
||||||
|
* @param {string} name field name
|
||||||
|
* @param {number} index field index
|
||||||
|
* @param {boolean} trigger_event trigger clear event
|
||||||
|
*
|
||||||
|
* @return {Program} self
|
||||||
|
*/
|
||||||
|
'public clearFieldData': function( name, index, trigger_event )
|
||||||
|
{
|
||||||
|
// clear field data
|
||||||
|
( this._fieldData[ name ] || {} )[ index ] = undefined;
|
||||||
|
( this._fieldDataEmitted[ name ] || {} )[ index ] = undefined;
|
||||||
|
|
||||||
|
// notify our fans
|
||||||
|
if ( trigger_event !== false )
|
||||||
|
{
|
||||||
|
this.emit( 'clearFieldData', name, index );
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear API Pending status
|
||||||
|
* Preventing the result for the associated request from taking effect
|
||||||
|
* This eliminates side-effects of race conditions (e.g. clearing a field
|
||||||
|
* while a request is still pending), but does not actually cancel the API
|
||||||
|
* call itself.
|
||||||
|
*
|
||||||
|
* @param {string} id tracking identifier
|
||||||
|
*
|
||||||
|
* @return {Program} self
|
||||||
|
*/
|
||||||
|
'public clearPendingApiCall': function( id )
|
||||||
|
{
|
||||||
|
if ( id !== undefined && this._pendingApiCall[ id ] !== undefined )
|
||||||
|
{
|
||||||
|
this._pendingApiCall[ id ] = undefined;
|
||||||
|
this._queuedApiCall[ id ] = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expand the mapped field data for the given field into the bucket
|
||||||
|
*
|
||||||
|
* It is expected that the callers are intelligent enough to not call this
|
||||||
|
* method if it would result in nonsense. That is, an error will be raised
|
||||||
|
* in the event that field data cannot be found; this will help to point out
|
||||||
|
* logic errors that set crap values.
|
||||||
|
*
|
||||||
|
* The predictive parameter allows data for the field to be set when the
|
||||||
|
* caller knows that the data for the value may soon become available (e.g.
|
||||||
|
* setting the value to pre-populate the value of a pending API call).
|
||||||
|
*
|
||||||
|
* @param {string} name field name
|
||||||
|
* @param {number} index field index
|
||||||
|
* @param {Object} bucket bucket to expand into
|
||||||
|
* @param {Object} map param mapping to bucket fields
|
||||||
|
* @param {boolean} predictive allow value to be set even if data does not
|
||||||
|
* exist for it
|
||||||
|
* @param {Object} diff changeset
|
||||||
|
*
|
||||||
|
* @return {Program} self
|
||||||
|
*/
|
||||||
|
'public expandFieldData': function(
|
||||||
|
name, index, bucket, map, predictive, diff
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var update = this.getDataExpansion(
|
||||||
|
name, index, bucket, map, predictive, diff
|
||||||
|
);
|
||||||
|
|
||||||
|
// update the bucket, merging with current data (other indexes)
|
||||||
|
bucket.setValues( update, true );
|
||||||
|
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
'public getDataExpansion': function(
|
||||||
|
name, index, bucket, map, predictive, diff
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var field_data = ( this._fieldData[ name ] || {} )[ index ],
|
||||||
|
data = {};
|
||||||
|
field_value = ( diff[ name ] || bucket.getDataByName( name ) )[ index ];
|
||||||
|
|
||||||
|
// if it's undefined, then the change probably represents a delete
|
||||||
|
if ( field_value === undefined )
|
||||||
|
{
|
||||||
|
( this._fieldDataEmitted[ name ] || [] )[ index ] = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we have no field data, try the "combined" index
|
||||||
|
if ( !field_data )
|
||||||
|
{
|
||||||
|
field_data = ( this._fieldData[ name ] || [] )[ -1 ];
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we have no data, then the field has likely been cleared (so we'll
|
||||||
|
// want to clear the bucket values
|
||||||
|
if ( field_data && !( field_data.___empty ) )
|
||||||
|
{
|
||||||
|
// do we have data for the currently selected index?
|
||||||
|
var data = field_data[ field_value ];
|
||||||
|
if ( !predictive && !( data ) && ( field_value !== '' ) )
|
||||||
|
{
|
||||||
|
// hmm..that's peculiar.
|
||||||
|
this.emit( 'error', Error(
|
||||||
|
'Data missing for field ' + name + '[' + index + ']!'
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
else if ( !data )
|
||||||
|
{
|
||||||
|
// we want to ignore the failure, but need to ensure the data is
|
||||||
|
// set to something sane
|
||||||
|
data = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if ( ( field_data && field_data.___empty )
|
||||||
|
&& ( field_value !== '' )
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// we have no field data but we're trying to set a non-empty value
|
||||||
|
this.emit( 'error', Error(
|
||||||
|
'Setting non-empty value ' + name + '[' + index + '] with ' +
|
||||||
|
'no field data!'
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// we'll clear everything out (we default to an empty string in the
|
||||||
|
// loop below)
|
||||||
|
data = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// alright---set each of the bucket values
|
||||||
|
var update = {};
|
||||||
|
for ( var field in map )
|
||||||
|
{
|
||||||
|
var param = map[ field ],
|
||||||
|
fdata = [];
|
||||||
|
|
||||||
|
fdata[ index ] = ( data[ param ] || '' );
|
||||||
|
update[ field ] = fdata;
|
||||||
|
}
|
||||||
|
|
||||||
|
return update;
|
||||||
|
}
|
||||||
|
} );
|
|
@ -0,0 +1,265 @@
|
||||||
|
/**
|
||||||
|
* Restricts Data API parameters
|
||||||
|
*
|
||||||
|
* Copyright (C) 2016 LoVullo Associates, Inc.
|
||||||
|
*
|
||||||
|
* This file is part of the Liza Data Collection Framework
|
||||||
|
*
|
||||||
|
* liza is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
var Class = require( 'easejs' ).Class,
|
||||||
|
DataApi = require( './DataApi' ),
|
||||||
|
EventEmitter = require( 'events' ).EventEmitter;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restricts a DataApi such that only declared params may be used in requests,
|
||||||
|
* required params must be used and ensures that the server responds with the
|
||||||
|
* expected values.
|
||||||
|
*
|
||||||
|
* Also provides support for param defaults. Perhaps this logic doesn't belong
|
||||||
|
* here, in which case it should be extracted into a separate decorator.
|
||||||
|
*/
|
||||||
|
module.exports = Class( 'RestrictedDataApi' )
|
||||||
|
.implement( DataApi )
|
||||||
|
.extend( EventEmitter,
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* DataApi to restrict
|
||||||
|
* @type {DataApi}
|
||||||
|
*/
|
||||||
|
'private _api': null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Available params and their defaults
|
||||||
|
* @type {name,default.<{type,value}>}
|
||||||
|
*/
|
||||||
|
'private _params': null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expected return params
|
||||||
|
* @type {Array.<string>}
|
||||||
|
*/
|
||||||
|
'private _retvals': null,
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize API restriction
|
||||||
|
*
|
||||||
|
* @param {DataApi} data_api data API to wrap
|
||||||
|
* @param {Object} desc API description
|
||||||
|
*/
|
||||||
|
__construct: function( data_api, desc )
|
||||||
|
{
|
||||||
|
if ( !( Class.isA( DataApi, data_api ) ) )
|
||||||
|
{
|
||||||
|
throw Error(
|
||||||
|
'Expected object of type DataApi; given: ' + data_api
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._api = data_api;
|
||||||
|
this._params = desc.params || {};
|
||||||
|
this._retvals = desc.retvals || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request data from the service
|
||||||
|
*
|
||||||
|
* @param {Object=} data request params
|
||||||
|
* @param {function(Object)=} callback server response callback
|
||||||
|
*
|
||||||
|
* @return {DataApi} self
|
||||||
|
*/
|
||||||
|
'public request': function( data, callback )
|
||||||
|
{
|
||||||
|
data = data || {};
|
||||||
|
callback = callback || function() {};
|
||||||
|
|
||||||
|
var _self = this;
|
||||||
|
|
||||||
|
// check the given params; if any are missing, then it should be
|
||||||
|
// considered to be an error
|
||||||
|
var reqdata = this._requestParamCheck( data );
|
||||||
|
|
||||||
|
// make the request
|
||||||
|
this._api.request( reqdata, function( err, response )
|
||||||
|
{
|
||||||
|
callback.call( _self,
|
||||||
|
err,
|
||||||
|
_self._checkResponse( response, callback )
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check request params to ensure that they are (a) known, (b) all required
|
||||||
|
* params are provided and (c) contain valid values
|
||||||
|
*
|
||||||
|
* A separate object is returned to prevent side-effects.
|
||||||
|
*
|
||||||
|
* @param {Object} data request data
|
||||||
|
*
|
||||||
|
* @return {Object} request data to be sent
|
||||||
|
*/
|
||||||
|
'private _requestParamCheck': function( data )
|
||||||
|
{
|
||||||
|
var ret = {};
|
||||||
|
|
||||||
|
for ( var name in data )
|
||||||
|
{
|
||||||
|
// fail on unknown params
|
||||||
|
if ( !( this._params[ name ] ) )
|
||||||
|
{
|
||||||
|
throw Error( 'Unkown param: ' + name );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// yes, there are more efficient ways than looping through the above and
|
||||||
|
// now through this, but the actual XHR itself will make it negligable,
|
||||||
|
// so I'm going for clarity
|
||||||
|
for ( var name in this._params )
|
||||||
|
{
|
||||||
|
var def_data = this._params[ name ]['default'],
|
||||||
|
def_val = def_data && def_data.value || '';
|
||||||
|
|
||||||
|
// if the data is set, then we're good
|
||||||
|
if ( data[ name ] )
|
||||||
|
{
|
||||||
|
ret[ name ] = data[ name ];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the data is not set; if there's no default, then this is an error
|
||||||
|
if ( !( def_val ) )
|
||||||
|
{
|
||||||
|
throw Error( 'Missing param: ' + name );
|
||||||
|
}
|
||||||
|
|
||||||
|
// sorry---we only support string defaults for now
|
||||||
|
if ( def_data.type !== 'string' )
|
||||||
|
{
|
||||||
|
throw Error(
|
||||||
|
'Only string param defaults are currently supported'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// use default
|
||||||
|
ret[ name ] = def_val;
|
||||||
|
}
|
||||||
|
|
||||||
|
// return a separate object to prevent side-effects
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the response data to ensure that all the expected params have been
|
||||||
|
* returned for each item
|
||||||
|
*
|
||||||
|
* The response data should be an array of objects; if this is not the case
|
||||||
|
* for the service in question, another decorator should be used to
|
||||||
|
* transform the data *before* it gets to this decorator.
|
||||||
|
*
|
||||||
|
* The callback is not invoked by this method; instead, it will be passed to
|
||||||
|
* any error events that may be emitted, allowing the handler to associate
|
||||||
|
* it with the original request and invoke it manually if necessary.
|
||||||
|
*
|
||||||
|
* @param {Array.<Object>} response response data
|
||||||
|
* @param {Function} callback callback to be called with response
|
||||||
|
*
|
||||||
|
* @return {Object} original object if validations passed; otherwise {}
|
||||||
|
*/
|
||||||
|
'private _checkResponse': function( response, callback )
|
||||||
|
{
|
||||||
|
// the response should be an array; otherwise, we cannot process it to
|
||||||
|
// see if the return data is valid (since it would not be in the
|
||||||
|
// expected format---if the format needs conversion, a separate
|
||||||
|
// decorator should handle that job *before* the data gets to this one)
|
||||||
|
//
|
||||||
|
// since ES5 isn't an option, we'll stick with this dirty hack
|
||||||
|
if ( !response || !( response.slice ) )
|
||||||
|
{
|
||||||
|
this.emit( 'error',
|
||||||
|
TypeError( 'Response data is not an array' ),
|
||||||
|
callback,
|
||||||
|
response
|
||||||
|
);
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// for each item returned, check to ensure that it contains the expected
|
||||||
|
// params
|
||||||
|
var i = this._retvals.length;
|
||||||
|
while ( i-- )
|
||||||
|
{
|
||||||
|
// check all items for the given param
|
||||||
|
var param = this._retvals[ i ],
|
||||||
|
err = this._checkAllFor( param, response );
|
||||||
|
|
||||||
|
// were any errors found (these would be the failed indexes)
|
||||||
|
if ( err.length > 0 )
|
||||||
|
{
|
||||||
|
this.emit( 'error',
|
||||||
|
TypeError(
|
||||||
|
'Return data missing param ' + param + ': ' +
|
||||||
|
err.join( ', ' )
|
||||||
|
),
|
||||||
|
callback,
|
||||||
|
response
|
||||||
|
);
|
||||||
|
|
||||||
|
// param was not found; data is invalid
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// everything looks good
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks each item for the given param
|
||||||
|
*
|
||||||
|
* If all goes well, the caller should expect that this method will return
|
||||||
|
* an empty array.
|
||||||
|
*
|
||||||
|
* @param {string} param name of param
|
||||||
|
* @param {Array.<Object>} response response data to scan
|
||||||
|
*
|
||||||
|
* @return {Array.<number>} indexes of failed response items, if any
|
||||||
|
*/
|
||||||
|
'private _checkAllFor': function( param, response )
|
||||||
|
{
|
||||||
|
var i = response.length,
|
||||||
|
err = [];
|
||||||
|
|
||||||
|
while ( i-- )
|
||||||
|
{
|
||||||
|
if ( response[ i ][ param ] === undefined )
|
||||||
|
{
|
||||||
|
// since we're looping in reverse, unshift instead of push
|
||||||
|
err.unshift( i );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
} );
|
|
@ -0,0 +1,145 @@
|
||||||
|
/**
|
||||||
|
* Adds static data to API response
|
||||||
|
*
|
||||||
|
* Copyright (C) 2016 LoVullo Associates, Inc.
|
||||||
|
*
|
||||||
|
* This file is part of the Liza Data Collection Framework
|
||||||
|
*
|
||||||
|
* liza is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var Class = require( 'easejs' ).Class,
|
||||||
|
DataApi = require( './DataApi' ),
|
||||||
|
|
||||||
|
EventEmitter = require( 'events' ).EventEmitter;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepends static data to a response
|
||||||
|
*/
|
||||||
|
module.exports = Class( 'StaticAdditionDataApi' )
|
||||||
|
.implement( DataApi )
|
||||||
|
.extend( EventEmitter,
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* DataApi to restrict
|
||||||
|
* @type {DataApi}
|
||||||
|
*/
|
||||||
|
'private _api': null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevents augmenting empty sets
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
'private _nonempty': false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only append if more than 1 result
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
'private _multiple': false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static values to prepend
|
||||||
|
*/
|
||||||
|
'private _static': [],
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize static API response data
|
||||||
|
*
|
||||||
|
* @param {DataApi} data_api data API to wrap
|
||||||
|
* @param {Boolean} nonempty to append if any results
|
||||||
|
* @param {Boolean} multiple to append if more than 1 result
|
||||||
|
* @param {Array.<Object>} static_data static data to prepend
|
||||||
|
*/
|
||||||
|
__construct: function( data_api, nonempty, multiple, static_data )
|
||||||
|
{
|
||||||
|
if ( !( Class.isA( DataApi, data_api ) ) )
|
||||||
|
{
|
||||||
|
throw Error(
|
||||||
|
'Expected object of type DataApi; given: ' + data_api
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._api = data_api;
|
||||||
|
this._static = static_data;
|
||||||
|
this._nonempty = !!nonempty;
|
||||||
|
this._multiple = !!multiple;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request data from the service
|
||||||
|
*
|
||||||
|
* @param {Object=} data request params
|
||||||
|
* @param {function(Object)=} callback server response callback
|
||||||
|
*
|
||||||
|
* @return {DataApi} self
|
||||||
|
*/
|
||||||
|
'public request': function( data, callback )
|
||||||
|
{
|
||||||
|
data = data || {};
|
||||||
|
callback = callback || function() {};
|
||||||
|
|
||||||
|
var _self = this,
|
||||||
|
inst = this.__inst;
|
||||||
|
|
||||||
|
this._api.request( data, function( err, response )
|
||||||
|
{
|
||||||
|
// return the response with our data
|
||||||
|
callback.call( inst,
|
||||||
|
err,
|
||||||
|
_self._unshiftData( response )
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unshifts the static data onto the given data set
|
||||||
|
*
|
||||||
|
* Be warned: for performance reasons, this alters the actual array that was
|
||||||
|
* passed in (rather than returning a new one); if this is a problem, change
|
||||||
|
* it (or add a flag to the ctor).
|
||||||
|
*
|
||||||
|
* @param {Array} data data to augment
|
||||||
|
*
|
||||||
|
* @return {Array} augmented data
|
||||||
|
*/
|
||||||
|
'private _unshiftData': function( data )
|
||||||
|
{
|
||||||
|
// if the nonempty flag is set, then we should not augment empty sets
|
||||||
|
if ( ( data.length === 0 ) && ( this._nonempty ) )
|
||||||
|
{
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if multiple flag is set but result contains < 2 results, do
|
||||||
|
// not augment
|
||||||
|
if ( ( data.length < 2 ) && ( this._multiple ) )
|
||||||
|
{
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// note that this modifies the actual reference!
|
||||||
|
var i = this._static.length;
|
||||||
|
while ( i-- )
|
||||||
|
{
|
||||||
|
data.unshift( this._static[ i ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
} );
|
|
@ -177,6 +177,14 @@ module.exports = Class( 'XhrHttpImpl' )
|
||||||
{
|
{
|
||||||
// alway async
|
// alway async
|
||||||
req.open( method, url, true );
|
req.open( method, url, true );
|
||||||
|
|
||||||
|
if ( method === 'POST' )
|
||||||
|
{
|
||||||
|
req.setRequestHeader(
|
||||||
|
'Content-Type',
|
||||||
|
'application/x-www-form-urlencoded'
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -170,34 +170,79 @@ describe( 'XhrHttpImpl', function()
|
||||||
.requestData( url, 'GET', "", done );
|
.requestData( url, 'GET', "", done );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
|
||||||
|
it( 'does not set Content-Type', function( done )
|
||||||
|
{
|
||||||
|
var url = 'http://bar',
|
||||||
|
StubXhr = createStubXhr();
|
||||||
|
|
||||||
|
StubXhr.prototype.readyState = 4; // done
|
||||||
|
StubXhr.prototype.status = 200; // OK
|
||||||
|
|
||||||
|
StubXhr.prototype.setRequestHeader = function()
|
||||||
|
{
|
||||||
|
// warning: this is fragile, if additional headers are
|
||||||
|
// ever set
|
||||||
|
throw Error( 'Headers should not be set on GET' );
|
||||||
|
};
|
||||||
|
|
||||||
|
Sut( StubXhr )
|
||||||
|
.requestData( url, 'GET', "", done );
|
||||||
|
} );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
|
||||||
describe( 'HTTP method is not GET', function()
|
describe( 'HTTP method is not GET', function()
|
||||||
{
|
{
|
||||||
it( 'sends data verbatim', function( done )
|
it( 'sends data verbatim as x-www-form-urlencoded', function( done )
|
||||||
{
|
{
|
||||||
var url = 'http://bar',
|
var url = 'http://bar',
|
||||||
src = "moocow",
|
src = "moocow",
|
||||||
StubXhr = createStubXhr();
|
StubXhr = createStubXhr(),
|
||||||
|
|
||||||
|
open_called = false,
|
||||||
|
send_called = false,
|
||||||
|
header_called = false;
|
||||||
|
|
||||||
StubXhr.prototype.readyState = 4; // done
|
StubXhr.prototype.readyState = 4; // done
|
||||||
StubXhr.prototype.status = 200; // OK
|
StubXhr.prototype.status = 200; // OK
|
||||||
|
|
||||||
StubXhr.prototype.open = function( _, given_url )
|
StubXhr.prototype.open = function( _, given_url )
|
||||||
{
|
{
|
||||||
|
open_called = true;
|
||||||
|
|
||||||
// URL should be unchanged
|
// URL should be unchanged
|
||||||
expect( given_url ).to.equal( url );
|
expect( given_url ).to.equal( url );
|
||||||
};
|
};
|
||||||
|
|
||||||
StubXhr.prototype.send = function( data )
|
StubXhr.prototype.send = function( data )
|
||||||
{
|
{
|
||||||
|
send_called = true;
|
||||||
|
|
||||||
expect( data ).is.equal( src );
|
expect( data ).is.equal( src );
|
||||||
StubXhr.inst.onreadystatechange();
|
StubXhr.inst.onreadystatechange();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
StubXhr.prototype.setRequestHeader = function( name, val )
|
||||||
|
{
|
||||||
|
header_called = true;
|
||||||
|
|
||||||
|
// warning: this is fragile, if additional headers are
|
||||||
|
// ever set
|
||||||
|
expect( name ).to.equal( 'Content-Type' );
|
||||||
|
expect( val ).to.equal(
|
||||||
|
'application/x-www-form-urlencoded'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
Sut( StubXhr )
|
Sut( StubXhr )
|
||||||
.requestData( url, 'POST', src, done );
|
.requestData( url, 'POST', src, function()
|
||||||
|
{
|
||||||
|
console.log( open_called, send_called, header_called );
|
||||||
|
expect( open_called && send_called && header_called )
|
||||||
|
.to.be.true;
|
||||||
|
done();
|
||||||
|
} );
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
@ -346,7 +391,8 @@ function createStubXhr()
|
||||||
send: function( data )
|
send: function( data )
|
||||||
{
|
{
|
||||||
this.onreadystatechange();
|
this.onreadystatechange();
|
||||||
}
|
},
|
||||||
|
setRequestHeader: function() {},
|
||||||
};
|
};
|
||||||
|
|
||||||
return StubXhr;
|
return StubXhr;
|
||||||
|
|
Loading…
Reference in New Issue