mbo-api
Version:
Implements the MINDBODY Online API.
476 lines (415 loc) • 15.1 kB
JavaScript
/**
* @author Brian Carlsen
* @version 1.0.0
*
* Serves as a base class for interacting with the actual
* MINDBODY services: Client, Class, Sale, Staff, and Site.
*/
var EventEmitter = require( 'events' ).EventEmitter,
soap = require( 'soap' ),
Promise = require( 'bluebird' ),
OperationalError = Promise.OperationalError;
var Credentials = require( '../classes/mbo_Credentials' ),
SOAPError = require( '../classes/SOAPError' ),
mboLogger = require( '../../logger/mbo_Logger' );
//******************************//
/**
* This class is not meant to be instantiated, and only serves as a base class for the actual Service classes.
*
* Creates a new MBO Service.
* Retrieves the WSDL of the given service and creates two methods for each SOAP method in the WSDL.
* For each SOAP method a function is created:
* 1) Bearing the same name that either extracts the result of the same name, and
* 2) Bearing the name with 'Response' post-fixed, which returns an array of the
* i) Raw response converted to a JS object
* ii) Raw XML response
* iii) Raw XML SOAP header info
* Each of these functions returns an A+ Promise.
*
* Each method of the service methods accepts an Object as a parameter as well.
* Each element of the object will be included in the Request section of the SOAP Request.
*
* Emits an 'initialized' event once all methods have been defined.
* A 'ready' event is triggered by the ServiceFactory once User Credentails have been set.
*
* @constructor
* @param {string|boolean} service The full name of the service to be implemented. E.g 'SaleService'
Set to False if no service is desired.
* @param {object|Credentials} credentials
An object containing your MINDBODY developer credentials, included in all service calls.
* If has the keys [ 'sourcename', 'password' ] will use SourceCredentials.
If has the keys [ 'apiKey' ] will use the API-key header.
* @param {int[]} [siteIds] An array containg site Ids to add.
* @param {number} apiVersion The MINDBODY API version you wish to use.
* Defaults to the most recent stable release.
*
* @return {mbo_Service} An absract service to interact with the MINDBODY API service.
*
* @throws {Error} Throws an error if invalid credentials object is passed.
*/
function mbo_Service( service, credentials, siteIds, apiVersion = 5.1 ) {
var self = this;
self.name = service;
self.mboLogger = undefined;
self.emitter = new EventEmitter();
self.ready = false;
self.on( 'ready', function() { self.ready = true; } );
if ( credentials instanceof Credentials ) {
// credentials already Credentials
self.sourceCredentials = credentials;
}
else {
self.sourceCredentials = new Credentials( credentials, siteIds, 'source' );
}
self.userCredentials = undefined;
self._useDefaultUserCredentials = false;
// Request Defaults
self.requestDefaults = {
XMLDetail: 'Full',
PageSize: '1000'
};
self.apiVersion = apiVersion;
self.apiVersionString = ( function( version ) {
switch( version ) {
case 5.0: return '0_5'; break;
case 5.1: return '0_5_1'; break;
case 6.0: return '0_6'; break;
};
} )( apiVersion )
if ( service === false ) {
self.emit( 'initialized' );
return self;
}
// Setup SOAP Client
soap.createClientAsync( 'https://api.mindbodyonline.com/' + self.apiVersionString + '/' + service + '.asmx?wsdl' )
.then( function( client ) {
// add api key credentials, if provided
if ( self.sourceCredentials.authentication.apiKey ) {
client.addHttpHeader( 'API-key', self.sourceCredentials.credentials.apiKey );
}
Promise.promisifyAll( client );
self.service = client;
// Add Service Functions to the Class
var description = client.describe();
for ( var service in description ) {
for ( var port in description[ service ] ) {
for ( var fcn in description[ service ][ port ] ) {
if ( self[ fcn ] === undefined ) { // Only create function if name is not taken.
// Defines the standard funtion, with extracted results
self[ fcn ] = self._defineMethod( fcn, description[ service ][ port ][ fcn ], true );
}
if ( self[ fcn + 'Response' ] === undefined ) { // Only create function if name is not taken.
// Defines the full function, returning the entire reponse, without extracting results
self[ fcn + 'Response' ] = self._defineMethod( fcn, description[ service ][ port ][ fcn ], false );
}
}
}
}
self.emit( 'initialized' );
} )
.catch( function( err ) {
throw err;
} );
}
// Array of meta info keys common to all responses
mbo_Service.metaInfoKeys = [
'Status',
'ErrorCode',
'Message',
'XMLDetail',
'ResultCount',
'CurrentPageIndex',
'TotalPageCount',
'targetNSAlias',
'targetNamespace'
];
/**
* Enables or disables the logging of calls to the API.
*
* @param {string|boolean} type The type of Logger to use -- 'local' or 'remote' --
* or false to disable.
* @param {string}
*
* @throws {Error}
*/
mbo_Service.prototype.log = function( type, path, host, port ) {
try {
this.mboLogger = ( type === false ) ? undefined : new mboLogger( type, this.name );
this.mboLogger.setPath( path );
// remote only
if( type === 'remote' ) {
this.mboLogger.setHost( host, port );
}
}
catch( err ) {
throw err;
}
}
/**
* Sets the User Credentials to use for any call, and sets them to be used.
* Not all calls require user credentials.
*
* @param {string} username The username of the MINDOBDY client you're interacting with.
* @param {string} password The password of the MINDBODY client you're interacting with.
* @param {number|number[]} siteIds A single, or array of, Site ID(s) which the client can interact with.
*/
mbo_Service.prototype.setUserCredentials = function( username, password, siteIds ) {
if ( this._useDefaultUserCredentials ) {
this._useDefaultUserCredentials = false;
}
this.userCredentials = new Credentials( { username: username, password: password } );
if ( siteIds ) {
this.addSiteIds( siteIds );
}
};
/**
* Sets the default credentials to the default value.
* This appends an underscore before the Source Name, and uses the Source's password.
*
* @param {boolean} val Whether to use the defualt User Credentials or not [Default: true]
*/
mbo_Service.prototype.useDefaultUserCredentials = function( val ) {
if ( typeof val === 'undefined' ) {
val = true;
}
this._useDefaultUserCredentials = !!val;
};
/**
* Adds Site Ids to the current users accessible sites.
* @param {number|number[]} siteIds A single, or array of, Site ID(s) which the client can interact with.
*/
mbo_Service.prototype.addSiteIds = function( siteIds ) {
if ( this.userCredentials ) {
this.userCredentials.addSiteIds( siteIds );
}
this.sourceCredentials.addSiteIds( siteIds ); // Syncs User and Source Site Ids
};
/**
* Gets or Sets defaults passed to every request.
* If second argument is included, the key is set to that value.
* If the second parameter is not included, the current value of the key is returned.
*
* @param {string} name The key of the default parameter to get or set
* @param {string} [value] The value to set the key to.
* @return {string} If getting a value the value is returned. If setting a value, nothing is returned.
*/
mbo_Service.prototype.defaultParam = function( key, value ) {
if ( typeof value === 'undefined' ) { // Getter
return this.requestDefaults[ key ];
}
else { // Setter
this.requestDefaults[ key ] = value.toString();
}
};
mbo_Service.prototype._setUserCredentialsToDefault = function() {
var username = '_' + this.sourceCredentials.username,
password = this.sourceCredentials.password,
siteIds = this.sourceCredentials.siteIds;
if ( siteIds.length === 1 && siteIds[ 0 ] === -99 ) {
// For Test site only, Default username doesn't have '_' prefixed
username = 'Siteowner';
password = 'apitest1234';
}
this.userCredentials = new Credentials( {
username: username,
password: password
} );
this.userCredentials.addSiteIds( siteIds )
};
/**
* Defines a method to be added to the Service.
* @param {string} name The name of the SOAP method to be wrapped.
* @param {Object} signature The SOAP method's signature including the input parameters, and output object.
* @param {boolean} extractResults Whether the method should attempt to automatically extract the desired result or not.
* @return {function} Returns the wrapped SOAP method.
*
* @throws {SOAPError} If response code is not 200 Success.
*/
mbo_Service.prototype._defineMethod = function( name, signature, extractResults ) {
var self = this;
return function( args ) {
var params = { Request: {} };
// api versions less than 6.0 use SOAP credentials informtation
if ( self.apiVersion < 6.0 ) {
params.Request.SourceCredentials = self.sourceCredentials.toSOAP();
}
if ( self._useDefaultUserCredentials ) {
self._setUserCredentialsToDefault();
}
if ( self.userCredentials ) {
params.Request.UserCredentials = self.userCredentials.toSOAP()
}
for ( var dflt in self.requestDefaults ) { // Default arguments
params.Request[ dflt ] = self.requestDefaults[ dflt ];
}
for ( var arg in args ) { // Passed in arguments
params.Request[ arg ] = args[ arg ];
}
// use api key
var multikey = false;
if ( self.sourceCredentials.authentication.apiKey ) {
if ( self.sourceCredentials.siteIds.length === 1 ) {
// only one site id, add as header and continue normally
self.service.addHttpHeader( 'SiteId', self.sourceCredentials.siteIds[ 0 ] );
}
else {
multikey = true;
}
}
// Run the function
var method = ( self.service[ name + 'Async' ] )( params )
.spread( function( result, raw, header ) {
// Logging
if ( self.mboLogger ) {
self.mboLogger.log( params, name, result );
}
// Check for Errors
var res = result[ name + 'Result' ];
if ( res.ErrorCode !== 200 ) { // SOAP Fault occured
var fault = {
Status: res.Status,
ErrorCode: res.ErrorCode,
Message: res.Message
};
throw new SOAPError( fault.ErrorCode, '[ErrorCode ' + fault.ErrorCode + '] ' + fault.Message );
}
else { // Successful Request, No Errors, so extract results
return Promise.resolve( [ result, raw, header ] );
}
} )
.spread( function( result, raw, header ) {
if ( extractResults ) { // Extract Relevant info
if ( name.substr( 0, 3 ) === 'Get' ) { // Function is a Getter, Extract relevant results
return self._extractGetterResults( result[ name + 'Result' ] );
}
else { // Function performs an action with Side effects, Extract non-meta info
return self._extractActionResults( result[ name + 'Result' ] );
}
}
else { // Return raw result
return [ result, raw, header ];
}
} )
.catch( function( err ) {
if ( err instanceof Error ) { // Rethrow error
throw err;
}
else {
return self._defaultSoapErrorHandler( err );
}
} );
if ( multikey ) {
// TODO
// more than one site id used with api key, run method for each
return Promise.reject(
new TypeError (
'Can not use multiple site ids with api key authentication.'
)
);
}
else {
// no api key provided
return method;
}
};
};
/**
* Check if the SOAP call returned a SOAP Fualt.
* Triggers a 'SoapFault' event if found.
*
* @deprecated MBO Services respond with status codes instead of SOAP Faults.
* @param {object} result The object representation of the SOAP response.
* @return {boolean} Whether the response contained a SOAP Fault of not.
*/
mbo_Service.getSoapFault = function( result ) {
for ( var key in result ) {
if ( 'Status' in result[ key ] ) {
if ( result[ key ].ErrorCode === 200 ) { // No error
return false;
}
else { // Error
this.emit( 'SoapFault', fault );
return {
ErrorCode: result[ key ].ErrorCode,
Status: result[ key ].Status,
Message: result[ key ].Message
};
}
}
}
};
/* Default SOAP Error Handler. To be used if SOAP request returns a SOAPFault.
*
*/
mbo_Service.prototype._defaultSoapErrorHandler = function( err ) {
console.error( err );
return Promise.reject( err );
};
/**
* Attempts to exract the results from an API request.
* It does this by eliminating all metadata.
*
* @param {Object} result The Object representation of the SOAP response.
* @return {string|number|Array} Returns either an Array of results or,
* if only 1 non-metadata element existed in the
* response, returns the actual data.
*/
mbo_Service.prototype._extractGetterResults = function( result ) {
for ( var resultKey in result ) {
if ( mbo_Service.metaInfoKeys.indexOf( resultKey ) === -1 ) {
// Key is not meta info, meanining it contains the results we're interested in
var extracted = {};
for ( var key in result[ resultKey ] ) {
if ( mbo_Service.metaInfoKeys.indexOf( key ) === -1 ) {
// Again, key is not meta info, it must be the result we want
extracted[ key ] = result[ resultKey ][ key ];
}
}
var extractedKeys = Object.keys( extracted );
if ( extractedKeys.length === 1 ) { // Only one result to return, Return raw result
return Promise.resolve( extracted[ extractedKeys[ 0 ] ] );
}
else { // More than one result, return whole object
return Promise.resolve( extracted );
}
}
}
// Couldn't find a result
return Promise.reject(
new OperationalError ( '[ErrorCode 701] Could not extract results. Try using the service function instead.' )
);
};
/**
* Extracts the results from an API call with a side effect.
* @param {object} result The Object representation of a SOAP response.
* @return {Array} An array containing any non-metadata from the response.
*/
mbo_Service.prototype._extractActionResults = function( result ) {
var extracted = { ResultCount: result.ResultCount };
for ( var resultKey in result ) {
if ( mbo_Service.metaInfoKeys.indexOf( resultKey ) === -1 ) {
// Key is not meta info, meanining it contains a result we're interested in
extracted[ resultKey ] = result[ resultKey ];
}
}
return Promise.resolve( extracted );
};
mbo_Service.prototype.toString = function() {
return '[object mbo_Service] {' +
'service: ' + this.service + ', ' +
'sourceCredentials: ' + this.sourceCredentials + ', ' +
'userCredentials: ' + this.userCredentials + ', ' +
'ready: ' + this.ready + ', ' +
'requestDefaults: ' + this.requestDefaults + '}';
}
//------------ Event Methods -------------------
mbo_Service.prototype.on = function( event, listener ) {
this.emitter.on( event, listener );
};
mbo_Service.prototype.once = function( event, listener ) {
this.emitter.on( event, listener );
}
mbo_Service.prototype.emit = function( event ) {
this.emitter.emit( event );
};
module.exports = mbo_Service;