service.js (16328B)
1 var AWS = require('./core'); 2 var Api = require('./model/api'); 3 var regionConfig = require('./region_config'); 4 var inherit = AWS.util.inherit; 5 var clientCount = 0; 6 7 /** 8 * The service class representing an AWS service. 9 * 10 * @abstract 11 * 12 * @!attribute apiVersions 13 * @return [Array<String>] the list of API versions supported by this service. 14 * @readonly 15 */ 16 AWS.Service = inherit({ 17 /** 18 * Create a new service object with a configuration object 19 * 20 * @param config [map] a map of configuration options 21 */ 22 constructor: function Service(config) { 23 if (!this.loadServiceClass) { 24 throw AWS.util.error(new Error(), 25 'Service must be constructed with `new\' operator'); 26 } 27 var ServiceClass = this.loadServiceClass(config || {}); 28 if (ServiceClass) { 29 var originalConfig = AWS.util.copy(config); 30 var svc = new ServiceClass(config); 31 Object.defineProperty(svc, '_originalConfig', { 32 get: function() { return originalConfig; }, 33 enumerable: false, 34 configurable: true 35 }); 36 svc._clientId = ++clientCount; 37 return svc; 38 } 39 this.initialize(config); 40 }, 41 42 /** 43 * @api private 44 */ 45 initialize: function initialize(config) { 46 var svcConfig = AWS.config[this.serviceIdentifier]; 47 48 this.config = new AWS.Config(AWS.config); 49 if (svcConfig) this.config.update(svcConfig, true); 50 if (config) this.config.update(config, true); 51 52 this.validateService(); 53 if (!this.config.endpoint) regionConfig(this); 54 55 this.config.endpoint = this.endpointFromTemplate(this.config.endpoint); 56 this.setEndpoint(this.config.endpoint); 57 }, 58 59 /** 60 * @api private 61 */ 62 validateService: function validateService() { 63 }, 64 65 /** 66 * @api private 67 */ 68 loadServiceClass: function loadServiceClass(serviceConfig) { 69 var config = serviceConfig; 70 if (!AWS.util.isEmpty(this.api)) { 71 return null; 72 } else if (config.apiConfig) { 73 return AWS.Service.defineServiceApi(this.constructor, config.apiConfig); 74 } else if (!this.constructor.services) { 75 return null; 76 } else { 77 config = new AWS.Config(AWS.config); 78 config.update(serviceConfig, true); 79 var version = config.apiVersions[this.constructor.serviceIdentifier]; 80 version = version || config.apiVersion; 81 return this.getLatestServiceClass(version); 82 } 83 }, 84 85 /** 86 * @api private 87 */ 88 getLatestServiceClass: function getLatestServiceClass(version) { 89 version = this.getLatestServiceVersion(version); 90 if (this.constructor.services[version] === null) { 91 AWS.Service.defineServiceApi(this.constructor, version); 92 } 93 94 return this.constructor.services[version]; 95 }, 96 97 /** 98 * @api private 99 */ 100 getLatestServiceVersion: function getLatestServiceVersion(version) { 101 if (!this.constructor.services || this.constructor.services.length === 0) { 102 throw new Error('No services defined on ' + 103 this.constructor.serviceIdentifier); 104 } 105 106 if (!version) { 107 version = 'latest'; 108 } else if (AWS.util.isType(version, Date)) { 109 version = AWS.util.date.iso8601(version).split('T')[0]; 110 } 111 112 if (Object.hasOwnProperty(this.constructor.services, version)) { 113 return version; 114 } 115 116 var keys = Object.keys(this.constructor.services).sort(); 117 var selectedVersion = null; 118 for (var i = keys.length - 1; i >= 0; i--) { 119 // versions that end in "*" are not available on disk and can be 120 // skipped, so do not choose these as selectedVersions 121 if (keys[i][keys[i].length - 1] !== '*') { 122 selectedVersion = keys[i]; 123 } 124 if (keys[i].substr(0, 10) <= version) { 125 return selectedVersion; 126 } 127 } 128 129 throw new Error('Could not find ' + this.constructor.serviceIdentifier + 130 ' API to satisfy version constraint `' + version + '\''); 131 }, 132 133 /** 134 * @api private 135 */ 136 api: {}, 137 138 /** 139 * @api private 140 */ 141 defaultRetryCount: 3, 142 143 /** 144 * @api private 145 */ 146 customizeRequests: function customizeRequests(callback) { 147 if (!callback) { 148 this.customRequestHandler = null; 149 } else if (typeof callback === 'function') { 150 this.customRequestHandler = callback; 151 } else { 152 throw new Error('Invalid callback type \'' + typeof callback + '\' provided in customizeRequests'); 153 } 154 }, 155 156 /** 157 * Calls an operation on a service with the given input parameters. 158 * 159 * @param operation [String] the name of the operation to call on the service. 160 * @param params [map] a map of input options for the operation 161 * @callback callback function(err, data) 162 * If a callback is supplied, it is called when a response is returned 163 * from the service. 164 * @param err [Error] the error object returned from the request. 165 * Set to `null` if the request is successful. 166 * @param data [Object] the de-serialized data returned from 167 * the request. Set to `null` if a request error occurs. 168 */ 169 makeRequest: function makeRequest(operation, params, callback) { 170 if (typeof params === 'function') { 171 callback = params; 172 params = null; 173 } 174 175 params = params || {}; 176 if (this.config.params) { // copy only toplevel bound params 177 var rules = this.api.operations[operation]; 178 if (rules) { 179 params = AWS.util.copy(params); 180 AWS.util.each(this.config.params, function(key, value) { 181 if (rules.input.members[key]) { 182 if (params[key] === undefined || params[key] === null) { 183 params[key] = value; 184 } 185 } 186 }); 187 } 188 } 189 190 var request = new AWS.Request(this, operation, params); 191 this.addAllRequestListeners(request); 192 193 if (callback) request.send(callback); 194 return request; 195 }, 196 197 /** 198 * Calls an operation on a service with the given input parameters, without 199 * any authentication data. This method is useful for "public" API operations. 200 * 201 * @param operation [String] the name of the operation to call on the service. 202 * @param params [map] a map of input options for the operation 203 * @callback callback function(err, data) 204 * If a callback is supplied, it is called when a response is returned 205 * from the service. 206 * @param err [Error] the error object returned from the request. 207 * Set to `null` if the request is successful. 208 * @param data [Object] the de-serialized data returned from 209 * the request. Set to `null` if a request error occurs. 210 */ 211 makeUnauthenticatedRequest: function makeUnauthenticatedRequest(operation, params, callback) { 212 if (typeof params === 'function') { 213 callback = params; 214 params = {}; 215 } 216 217 var request = this.makeRequest(operation, params).toUnauthenticated(); 218 return callback ? request.send(callback) : request; 219 }, 220 221 /** 222 * Waits for a given state 223 * 224 * @param state [String] the state on the service to wait for 225 * @param params [map] a map of parameters to pass with each request 226 * @callback callback function(err, data) 227 * If a callback is supplied, it is called when a response is returned 228 * from the service. 229 * @param err [Error] the error object returned from the request. 230 * Set to `null` if the request is successful. 231 * @param data [Object] the de-serialized data returned from 232 * the request. Set to `null` if a request error occurs. 233 */ 234 waitFor: function waitFor(state, params, callback) { 235 var waiter = new AWS.ResourceWaiter(this, state); 236 return waiter.wait(params, callback); 237 }, 238 239 /** 240 * @api private 241 */ 242 addAllRequestListeners: function addAllRequestListeners(request) { 243 var list = [AWS.events, AWS.EventListeners.Core, this.serviceInterface(), 244 AWS.EventListeners.CorePost]; 245 for (var i = 0; i < list.length; i++) { 246 if (list[i]) request.addListeners(list[i]); 247 } 248 249 // disable parameter validation 250 if (!this.config.paramValidation) { 251 request.removeListener('validate', 252 AWS.EventListeners.Core.VALIDATE_PARAMETERS); 253 } 254 255 if (this.config.logger) { // add logging events 256 request.addListeners(AWS.EventListeners.Logger); 257 } 258 259 this.setupRequestListeners(request); 260 // call prototype's customRequestHandler 261 if (typeof this.constructor.prototype.customRequestHandler === 'function') { 262 this.constructor.prototype.customRequestHandler(request); 263 } 264 // call instance's customRequestHandler 265 if (Object.prototype.hasOwnProperty.call(this, 'customRequestHandler') && typeof this.customRequestHandler === 'function') { 266 this.customRequestHandler(request); 267 } 268 }, 269 270 /** 271 * Override this method to setup any custom request listeners for each 272 * new request to the service. 273 * 274 * @abstract 275 */ 276 setupRequestListeners: function setupRequestListeners() { 277 }, 278 279 /** 280 * Gets the signer class for a given request 281 * @api private 282 */ 283 getSignerClass: function getSignerClass() { 284 var version; 285 if (this.config.signatureVersion) { 286 version = this.config.signatureVersion; 287 } else { 288 version = this.api.signatureVersion; 289 } 290 return AWS.Signers.RequestSigner.getVersion(version); 291 }, 292 293 /** 294 * @api private 295 */ 296 serviceInterface: function serviceInterface() { 297 switch (this.api.protocol) { 298 case 'ec2': return AWS.EventListeners.Query; 299 case 'query': return AWS.EventListeners.Query; 300 case 'json': return AWS.EventListeners.Json; 301 case 'rest-json': return AWS.EventListeners.RestJson; 302 case 'rest-xml': return AWS.EventListeners.RestXml; 303 } 304 if (this.api.protocol) { 305 throw new Error('Invalid service `protocol\' ' + 306 this.api.protocol + ' in API config'); 307 } 308 }, 309 310 /** 311 * @api private 312 */ 313 successfulResponse: function successfulResponse(resp) { 314 return resp.httpResponse.statusCode < 300; 315 }, 316 317 /** 318 * How many times a failed request should be retried before giving up. 319 * the defaultRetryCount can be overriden by service classes. 320 * 321 * @api private 322 */ 323 numRetries: function numRetries() { 324 if (this.config.maxRetries !== undefined) { 325 return this.config.maxRetries; 326 } else { 327 return this.defaultRetryCount; 328 } 329 }, 330 331 /** 332 * @api private 333 */ 334 retryDelays: function retryDelays(retryCount) { 335 return AWS.util.calculateRetryDelay(retryCount, this.config.retryDelayOptions); 336 }, 337 338 /** 339 * @api private 340 */ 341 retryableError: function retryableError(error) { 342 if (this.networkingError(error)) return true; 343 if (this.expiredCredentialsError(error)) return true; 344 if (this.throttledError(error)) return true; 345 if (error.statusCode >= 500) return true; 346 return false; 347 }, 348 349 /** 350 * @api private 351 */ 352 networkingError: function networkingError(error) { 353 return error.code === 'NetworkingError'; 354 }, 355 356 /** 357 * @api private 358 */ 359 expiredCredentialsError: function expiredCredentialsError(error) { 360 // TODO : this only handles *one* of the expired credential codes 361 return (error.code === 'ExpiredTokenException'); 362 }, 363 364 /** 365 * @api private 366 */ 367 clockSkewError: function clockSkewError(error) { 368 switch (error.code) { 369 case 'RequestTimeTooSkewed': 370 case 'RequestExpired': 371 case 'InvalidSignatureException': 372 case 'SignatureDoesNotMatch': 373 case 'AuthFailure': 374 case 'RequestInTheFuture': 375 return true; 376 default: return false; 377 } 378 }, 379 380 /** 381 * @api private 382 */ 383 throttledError: function throttledError(error) { 384 // this logic varies between services 385 switch (error.code) { 386 case 'ProvisionedThroughputExceededException': 387 case 'Throttling': 388 case 'ThrottlingException': 389 case 'RequestLimitExceeded': 390 case 'RequestThrottled': 391 return true; 392 default: 393 return false; 394 } 395 }, 396 397 /** 398 * @api private 399 */ 400 endpointFromTemplate: function endpointFromTemplate(endpoint) { 401 if (typeof endpoint !== 'string') return endpoint; 402 403 var e = endpoint; 404 e = e.replace(/\{service\}/g, this.api.endpointPrefix); 405 e = e.replace(/\{region\}/g, this.config.region); 406 e = e.replace(/\{scheme\}/g, this.config.sslEnabled ? 'https' : 'http'); 407 return e; 408 }, 409 410 /** 411 * @api private 412 */ 413 setEndpoint: function setEndpoint(endpoint) { 414 this.endpoint = new AWS.Endpoint(endpoint, this.config); 415 }, 416 417 /** 418 * @api private 419 */ 420 paginationConfig: function paginationConfig(operation, throwException) { 421 var paginator = this.api.operations[operation].paginator; 422 if (!paginator) { 423 if (throwException) { 424 var e = new Error(); 425 throw AWS.util.error(e, 'No pagination configuration for ' + operation); 426 } 427 return null; 428 } 429 430 return paginator; 431 } 432 }); 433 434 AWS.util.update(AWS.Service, { 435 436 /** 437 * Adds one method for each operation described in the api configuration 438 * 439 * @api private 440 */ 441 defineMethods: function defineMethods(svc) { 442 AWS.util.each(svc.prototype.api.operations, function iterator(method) { 443 if (svc.prototype[method]) return; 444 var operation = svc.prototype.api.operations[method]; 445 if (operation.authtype === 'none') { 446 svc.prototype[method] = function (params, callback) { 447 return this.makeUnauthenticatedRequest(method, params, callback); 448 }; 449 } else { 450 svc.prototype[method] = function (params, callback) { 451 return this.makeRequest(method, params, callback); 452 }; 453 } 454 }); 455 }, 456 457 /** 458 * Defines a new Service class using a service identifier and list of versions 459 * including an optional set of features (functions) to apply to the class 460 * prototype. 461 * 462 * @param serviceIdentifier [String] the identifier for the service 463 * @param versions [Array<String>] a list of versions that work with this 464 * service 465 * @param features [Object] an object to attach to the prototype 466 * @return [Class<Service>] the service class defined by this function. 467 */ 468 defineService: function defineService(serviceIdentifier, versions, features) { 469 AWS.Service._serviceMap[serviceIdentifier] = true; 470 if (!Array.isArray(versions)) { 471 features = versions; 472 versions = []; 473 } 474 475 var svc = inherit(AWS.Service, features || {}); 476 477 if (typeof serviceIdentifier === 'string') { 478 AWS.Service.addVersions(svc, versions); 479 480 var identifier = svc.serviceIdentifier || serviceIdentifier; 481 svc.serviceIdentifier = identifier; 482 } else { // defineService called with an API 483 svc.prototype.api = serviceIdentifier; 484 AWS.Service.defineMethods(svc); 485 } 486 487 return svc; 488 }, 489 490 /** 491 * @api private 492 */ 493 addVersions: function addVersions(svc, versions) { 494 if (!Array.isArray(versions)) versions = [versions]; 495 496 svc.services = svc.services || {}; 497 for (var i = 0; i < versions.length; i++) { 498 if (svc.services[versions[i]] === undefined) { 499 svc.services[versions[i]] = null; 500 } 501 } 502 503 svc.apiVersions = Object.keys(svc.services).sort(); 504 }, 505 506 /** 507 * @api private 508 */ 509 defineServiceApi: function defineServiceApi(superclass, version, apiConfig) { 510 var svc = inherit(superclass, { 511 serviceIdentifier: superclass.serviceIdentifier 512 }); 513 514 function setApi(api) { 515 if (api.isApi) { 516 svc.prototype.api = api; 517 } else { 518 svc.prototype.api = new Api(api); 519 } 520 } 521 522 if (typeof version === 'string') { 523 if (apiConfig) { 524 setApi(apiConfig); 525 } else { 526 try { 527 setApi(AWS.apiLoader(superclass.serviceIdentifier, version)); 528 } catch (err) { 529 throw AWS.util.error(err, { 530 message: 'Could not find API configuration ' + 531 superclass.serviceIdentifier + '-' + version 532 }); 533 } 534 } 535 if (!Object.prototype.hasOwnProperty.call(superclass.services, version)) { 536 superclass.apiVersions = superclass.apiVersions.concat(version).sort(); 537 } 538 superclass.services[version] = svc; 539 } else { 540 setApi(version); 541 } 542 543 AWS.Service.defineMethods(svc); 544 return svc; 545 }, 546 547 /** 548 * @api private 549 */ 550 hasService: function(identifier) { 551 return Object.prototype.hasOwnProperty.call(AWS.Service._serviceMap, identifier); 552 }, 553 554 /** 555 * @api private 556 */ 557 _serviceMap: {} 558 }); 559 560 ```