1
0
Fork 0

Data API liberation and improvment

Not all aspects of the Data API are liberated quite yet, but this is the
bulk of it.
master
Mike Gerwitz 2016-04-18 10:10:29 -04:00
commit 2722b0b29a
7 changed files with 1358 additions and 4 deletions

View File

@ -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' ]
} );

View File

@ -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;
}
} );

View File

@ -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;
}
} );

View File

@ -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;
}
} );

View File

@ -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;
}
} );

View File

@ -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'
);
}
}, },

View File

@ -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;