1 /** 2 * @author Brian Carlsen 3 * @version 1.0.0 4 * 5 * Serves as a base class for interacting with the actual 6 * MINDBODY services: Client, Class, Sale, Staff, and Site. 7 */ 8 9 var EventEmitter = require( 'events' ).EventEmitter, 10 soap = require( 'soap' ), 11 Promise = require( 'bluebird' ), 12 OperationalError = Promise.OperationalError; 13 14 var Credentials = require( '../classes/mbo_Credentials' ), 15 SOAPError = require( '../classes/SOAPError' ), 16 mboLogger = require( '../../logger/mbo_Logger' ); 17 18 //******************************// 19 20 /** 21 * This class is not meant to be instantiated, and only serves as a base class for the actual Service classes. 22 * 23 * Creates a new MBO Service. 24 * Retrieves the WSDL of the given service and creates two methods for each SOAP method in the WSDL. 25 * For each SOAP method a function is created: 26 * 1) Bearing the same name that either extracts the result of the same name, and 27 * 2) Bearing the name with 'Response' post-fixed, which returns an array of the 28 * i) Raw response converted to a JS object 29 * ii) Raw XML response 30 * iii) Raw XML SOAP header info 31 * Each of these functions returns an A+ Promise. 32 * 33 * Each method of the service methods accepts an Object as a parameter as well. 34 * Each element of the object will be included in the Request section of the SOAP Request. 35 * 36 * Emits an 'initialized' event once all methods have been defined. 37 * A 'ready' event is triggered by the ServiceFactory once User Credentails have been set. 38 * 39 * @constructor 40 * @param {string|boolean} service The full name of the service to be implemented. E.g 'SaleService' 41 Set to False if no service is desired. 42 * @param {string} sourceName Your MINDBODY developer Source Name, included in all service calls. 43 * @param {string} sourcePassword Your MINDBODY developer Source Password, included in all service calls. 44 * @param {int[]} [siteIds] An array containg site Ids to add. 45 * @param {number} apiVersion The MINDBODY API version you wish to use. 46 * Defaults to the most recent stable release. 47 * 48 * @return {mbo_Service} An absract service to interact with the MINDBODY API service. 49 */ 50 function mbo_Service( service, sourceName, sourcePassword, siteIds, apiVersion = 5.0 ) { 51 var self = this; 52 self.name = service; 53 self.mboLogger = undefined; 54 self.emitter = new EventEmitter(); 55 56 self.ready = false; 57 self.on( 'ready', function() { self.ready = true; } ); 58 59 self.sourceCredentials = new Credentials( sourceName, sourcePassword, siteIds, 'source' ); 60 self.userCredentials = undefined; 61 self._useDefaultUserCredentials = false; 62 63 // Request Defaults 64 self.requestDefaults = { 65 XMLDetail: 'Full', 66 PageSize: '1000' 67 }; 68 69 self.apiVersion = apiVersion; 70 self.apiVersionString = ( function( version ) { 71 switch( verison ) { 72 case 5.0: return '0_5'; break; 73 case 5.1: return '0_5_1'; break; 74 }; 75 } )( apiVersion ) 76 77 if( service === false ) { 78 self.emit( 'initialized' ); 79 return self; 80 } 81 82 // Setup SOAP Client 83 soap.createClientAsync( 'https://api.mindbodyonline.com/' + self.apiVersionString + '/' + service + '.asmx?wsdl' ) 84 .then( function( client ) { 85 Promise.promisifyAll( client ); 86 self.service = client; 87 88 // Add Service Functions to the Class 89 var description = client.describe(); 90 for ( var service in description ) { 91 for ( var port in description[ service ] ) { 92 for ( var fcn in description[ service ][ port ] ) { 93 if ( self[ fcn ] === undefined ) { // Only create function if name is not taken. 94 // Defines the standard funtion, with extracted results 95 self[ fcn ] = self._defineMethod( fcn, description[ service ][ port ][ fcn ], true ); 96 } 97 98 if ( self[ fcn + 'Response' ] === undefined ) { // Only create function if name is not taken. 99 // Defines the full function, returning the entire reponse, without extracting results 100 self[ fcn + 'Response' ] = self._defineMethod( fcn, description[ service ][ port ][ fcn ], false ); 101 } 102 } 103 } 104 } 105 106 self.emit( 'initialized' ); 107 } ) 108 .catch( function( err ) { 109 throw err; 110 } ); 111 } 112 113 // Array of meta info keys common to all responses 114 mbo_Service.metaInfoKeys = [ 115 'Status', 116 'ErrorCode', 117 'Message', 118 'XMLDetail', 119 'ResultCount', 120 'CurrentPageIndex', 121 'TotalPageCount', 122 'targetNSAlias', 123 'targetNamespace' 124 ]; 125 126 /** 127 * Enables or disables the logging of calls to the API. 128 * 129 * @param {string|boolean} type The type of Logger to use -- 'local' or 'remote' -- 130 * or false to disable. 131 * @param {string} 132 * 133 * @throws {Error} 134 */ 135 mbo_Service.prototype.log = function( type, path, host, port ) { 136 try { 137 this.mboLogger = ( type === false ) ? undefined : new mbo_Logger( type ); 138 this.mboLogger.setPath( path ); 139 140 // remote only 141 if( type === 'remote' ) { 142 this.mboLogger.setHost( host, port ); 143 } 144 } 145 catch( err ) { 146 throw err; 147 } 148 } 149 150 /** 151 * Sets the User Credentials to use for any call, and sets them to be used. 152 * Not all calls require user credentials. 153 * 154 * @param {string} username The username of the MINDOBDY client you're interacting with. 155 * @param {string} password The password of the MINDBODY client you're interacting with. 156 * @param {number|number[]} siteIds A single, or array of, Site ID(s) which the client can interact with. 157 */ 158 mbo_Service.prototype.setUserCredentials = function( username, password, siteIds ) { 159 if ( this._useDefaultUserCredentials ) { 160 this._useDefaultUserCredentials = false; 161 } 162 163 this.userCredentials = new Credentials( username, password ); 164 165 if ( siteIds ) { 166 this.addSiteIds( siteIds ); 167 } 168 }; 169 170 /** 171 * Sets the default credentials to the default value. 172 * This appends an underscore before the Source Name, and uses the Source's password. 173 */ 174 mbo_Service.prototype.useDefaultUserCredentials = function( val ) { 175 if ( typeof val === 'undefined' ) { 176 val = true; 177 } 178 179 this._useDefaultUserCredentials = !!val; 180 }; 181 182 /** 183 * Adds Site Ids to the current users accessible sites. 184 * @param {number|number[]} siteIds A single, or array of, Site ID(s) which the client can interact with. 185 */ 186 mbo_Service.prototype.addSiteIds = function( siteIds ) { 187 if ( this.userCredentials ) { 188 this.userCredentials.addSiteIds( siteIds ); 189 } 190 191 this.sourceCredentials.addSiteIds( siteIds ); // Syncs User and Source Site Ids 192 }; 193 194 /** 195 * Gets or Sets defaults passed to every request. 196 * If second argument is included, the key is set to that value. 197 * If the second parameter is not included, the current value of the key is returned. 198 * 199 * @param {string} name The key of the default parameter to get or set 200 * @param {string} [value] The value to set the key to. 201 * @return {string} If getting a value the value is returned. If setting a value, nothing is returned. 202 */ 203 mbo_Service.prototype.defaultParam = function( key, value ) { 204 if ( typeof value === 'undefined' ) { // Getter 205 return this.requestDefaults[ key ]; 206 } 207 else { // Setter 208 this.requestDefaults[ key ] = value.toString(); 209 } 210 }; 211 212 mbo_Service.prototype._setUserCredentialsToDefault = function() { 213 var username = '_' + this.sourceCredentials.username, 214 password = this.sourceCredentials.password, 215 siteIds = this.sourceCredentials.siteIds; 216 217 if ( siteIds.length === 1 && siteIds[ 0 ] === -99 ) { 218 // For Test site only, Default username doesn't have '_' prefixed 219 username = 'Siteowner'; 220 password = 'apitest1234'; 221 } 222 223 this.userCredentials = new Credentials( username, password ); 224 this.userCredentials.addSiteIds( siteIds ) 225 }; 226 227 /** 228 * Defines a method to be added to the Service. 229 * @param {string} name The name of the SOAP method to be wrapped. 230 * @param {Object} signature The SOAP method's signature including the input parameters, and output object. 231 * @param {boolean} extractResults Whether the method should attempt to automatically extract the desired result or not. 232 * @return {function} Returns the wrapped SOAP method. 233 * 234 * @throws {SOAPError} If response code is not 200 Success. 235 */ 236 mbo_Service.prototype._defineMethod = function( name, signature, extractResults ) { 237 var self = this; 238 return function( args ) { 239 var params = { 240 Request: { 241 SourceCredentials: self.sourceCredentials.toSOAP() 242 } 243 }; 244 245 if ( self._useDefaultUserCredentials ) { 246 self._setUserCredentialsToDefault(); 247 } 248 249 if ( self.userCredentials ) { 250 params.Request.UserCredentials = self.userCredentials.toSOAP() 251 } 252 253 for ( var dflt in self.requestDefaults ) { // Default arguments 254 params.Request[ dflt ] = self.requestDefaults[ dflt ]; 255 } 256 257 for ( var arg in args ) { // Passed in arguments 258 params.Request[ arg ] = args[ arg ]; 259 } 260 261 // Run the function 262 return ( self.service[ name + 'Async' ] )( params ) 263 .spread( function( result, raw, header ) { 264 265 // Logging 266 if ( self.mboLogger ) { 267 self.mboLogger.log( self.name, params, name, result ); 268 } 269 270 // Check for Errors 271 var res = result[ name + 'Result' ]; 272 if ( res.ErrorCode !== 200 ) { // SOAP Fault occured 273 274 var fault = { 275 Status: res.Status, 276 ErrorCode: res.ErrorCode, 277 Message: res.Message 278 }; 279 280 throw new SOAPError( '[ErrorCode ' + fault.ErrorCode + '] ' + fault.Message ); 281 } 282 else { // Successful Request, No Errors, so extract results 283 return Promise.resolve( [ result, raw, header ] ); 284 } 285 } ) 286 .spread( function( result, raw, header ) { 287 if ( extractResults ) { // Extract Relevant info 288 if ( name.substr( 0, 3 ) === 'Get' ) { // Function is a Getter, Extract relevant results 289 return self._extractGetterResults( result[ name + 'Result' ] ); 290 } 291 else { // Function performs an action with Side effects, Extract non-meta info 292 return self._extractActionResults( result[ name + 'Result' ] ); 293 } 294 } 295 else { // Return raw result 296 return [ result, raw, header ]; 297 } 298 } ) 299 .catch( function( err ) { 300 if ( err instanceof Error ) { // Rethrow error 301 throw err; 302 } 303 else { 304 return self._defaultSoapErrorHandler( err ); 305 } 306 } ); 307 }; 308 }; 309 310 /** 311 * Check if the SOAP call returned a SOAP Fualt. 312 * Triggers a 'SoapFault' event if found. 313 * 314 * @deprecated MBO Services respond with status codes instead of SOAP Faults. 315 * @param {object} result The object representation of the SOAP response. 316 * @return {boolean} Whether the response contained a SOAP Fault of not. 317 */ 318 mbo_Service.getSoapFault = function( result ) { 319 for ( var key in result ) { 320 if ( 'Status' in result[ key ] ) { 321 if ( result[ key ].ErrorCode === 200 ) { // No error 322 return false; 323 } 324 else { // Error 325 this.emit( 'SoapFault', fault ); 326 327 return { 328 ErrorCode: result[ key ].ErrorCode, 329 Status: result[ key ].Status, 330 Message: result[ key ].Message 331 }; 332 } 333 } 334 } 335 }; 336 337 /* Default SOAP Error Handler. To be used if SOAP request returns a SOAPFault. 338 * 339 */ 340 mbo_Service.prototype._defaultSoapErrorHandler = function( err ) { 341 console.error( err ); 342 return Promise.reject( err ); 343 }; 344 345 /** 346 * Attempts to exract the results from an API request. 347 * It does this by eliminating all metadata. 348 * 349 * @param {Object} result The Object representation of the SOAP response. 350 * @return {string|number|Array} Returns either an Array of results or, 351 * if only 1 non-metadata element existed in the 352 * response, returns the actual data. 353 */ 354 mbo_Service.prototype._extractGetterResults = function( result ) { 355 for ( var resultKey in result ) { 356 if ( mbo_Service.metaInfoKeys.indexOf( resultKey ) === -1 ) { 357 // Key is not meta info, meanining it contains the results we're interested in 358 359 var extracted = {}; 360 for ( var key in result[ resultKey ] ) { 361 if ( mbo_Service.metaInfoKeys.indexOf( key ) === -1 ) { 362 // Again, key is not meta info, it must be the result we want 363 364 extracted[ key ] = result[ resultKey ][ key ]; 365 } 366 } 367 368 var extractedKeys = Object.keys( extracted ); 369 if ( extractedKeys.length === 1 ) { // Only one result to return, Return raw result 370 return Promise.resolve( extracted[ extractedKeys[ 0 ] ] ); 371 } 372 else { // More than one result, return whole object 373 return Promise.resolve( extracted ); 374 } 375 } 376 } 377 378 // Couldn't find a result 379 return Promise.reject( 380 new OperationalError ( '[ErrorCode 701] Could not extract results. Try using the service function instead.' ) 381 ); 382 }; 383 384 /** 385 * Extracts the results from an API call with a side effect. 386 * @param {object} result The Object representation of a SOAP response. 387 * @return {Array} An array containing any non-metadata from the response. 388 */ 389 mbo_Service.prototype._extractActionResults = function( result ) { 390 var extracted = { ResultCount: result.ResultCount }; 391 392 for ( var resultKey in result ) { 393 if ( mbo_Service.metaInfoKeys.indexOf( resultKey ) === -1 ) { 394 // Key is not meta info, meanining it contains a result we're interested in 395 extracted[ resultKey ] = result[ resultKey ]; 396 } 397 } 398 399 return Promise.resolve( extracted ); 400 }; 401 402 mbo_Service.prototype.toString = function() { 403 return '[object mbo_Service] {' + 404 'service: ' + this.service + ', ' + 405 'sourceCredentials: ' + this.sourceCredentials + ', ' + 406 'userCredentials: ' + this.userCredentials + ', ' + 407 'ready: ' + this.ready + ', ' + 408 'requestDefaults: ' + this.requestDefaults + '}'; 409 } 410 411 //------------ Event Methods ------------------- 412 413 mbo_Service.prototype.on = function( event, listener ) { 414 this.emitter.on( event, listener ); 415 }; 416 417 mbo_Service.prototype.once = function( event, listener ) { 418 this.emitter.on( event, listener ); 419 } 420 421 mbo_Service.prototype.emit = function( event ) { 422 this.emitter.emit( event ); 423 }; 424 425 module.exports = mbo_Service;