volos-quota-apigee
Advanced tools
Comparing version 0.9.3 to 0.10.0
@@ -33,10 +33,24 @@ /**************************************************************************** | ||
var assert = require('assert'); | ||
var url = require('url'); | ||
var debug = require('debug')('apigee'); | ||
var http = require('http'); | ||
var https = require('https'); | ||
var Quota = require('volos-quota-common'); | ||
var querystring = require('querystring'); | ||
var semver = require('semver'); | ||
var superagent = require('superagent'); | ||
var util = require('util'); | ||
var Quota = require('volos-quota-common'); | ||
var debug = require('debug')('apigee'); | ||
var url = require('url'); | ||
var apigeeAccess; | ||
var hasApigeeAccess; | ||
try { | ||
// Older versions of Apigee won't have this, so be prepared to work around. | ||
apigeeAccess = require('apigee-access'); | ||
apigeeAccess.getQuota(); | ||
hasApigeeAccess = true; | ||
} catch (e) { | ||
debug('Operating without access to apigee-access'); | ||
} | ||
var create = function(options) { | ||
@@ -48,43 +62,57 @@ return new Quota(ApigeeQuotaSpi, options); | ||
var ApigeeQuotaSpi = function(options) { | ||
if (!options.uri) { | ||
throw new Error('uri parameter must be specified'); | ||
// Allow users to override use of apigee-access | ||
if (options.apigeeMode === 'local') { | ||
debug('Using apigee-access no matter what'); | ||
this.useApigeeAccess = true; | ||
} else if (options.apigeeMode === 'remote') { | ||
debug('Using remote apigee proxy no matter what'); | ||
this.useApigeeAccess = false; | ||
} else { | ||
this.useApigeeAccess = hasApigeeAccess; | ||
} | ||
if (!options.key) { | ||
throw new Error('key parameter must be specified'); | ||
} | ||
assert(options.timeUnit); | ||
assert(options.interval); | ||
if (options.startTime) { | ||
throw new Error('Quotas with a fixed starting time are not supported'); | ||
this.options = options; | ||
if (this.useApigeeAccess) { | ||
this.apigeeQuota = apigeeAccess.getQuota(); | ||
} else { | ||
if (!options.uri) { | ||
throw new Error('uri parameter must be specified'); | ||
} | ||
if (!options.key) { | ||
throw new Error('key parameter must be specified'); | ||
} | ||
this.uri = options.uri; | ||
this.key = options.key; | ||
} | ||
}; | ||
this.uri = options.uri; | ||
this.key = options.key; | ||
this.options = options; | ||
ApigeeQuotaSpi.prototype.getImplementationName = function(cb) { | ||
var self = this; | ||
selectImplementation(self, function(err, impl) { | ||
if (err) { | ||
cb(err); | ||
} else { | ||
cb(undefined, impl.getImplementationName()); | ||
} | ||
}); | ||
}; | ||
ApigeeQuotaSpi.prototype.apply = function(options, cb) { | ||
var allow = options.allow || this.options.allow; | ||
var r = { | ||
identifier: options.identifier, | ||
weight: options.weight, | ||
interval: this.options.interval, | ||
allow: allow, | ||
unit: this.options.timeUnit | ||
}; | ||
makeRequest(this, 'POST', '/quotas/distributed', querystring.stringify(r), function(err, resp) { | ||
var self = this; | ||
selectImplementation(self, function(err, impl) { | ||
if (err) { | ||
cb(err); | ||
} else { | ||
var ret = { | ||
allowed: Number(resp.allowed), | ||
used: Number(resp.used) + Number(resp.exceeded), // note: this is exceeded for this req, not overall | ||
isAllowed: !resp.failed, | ||
expiryTime: Number(resp.expiry_time), | ||
timestamp: Number(resp.ts) | ||
}; | ||
cb(undefined, ret); | ||
impl.apply(options, function(err, result) { | ||
if (err) { | ||
debug('Quota error: %j', err); | ||
} else { | ||
debug('Quota result: %j', result); | ||
} | ||
cb(err, result); | ||
}); | ||
} | ||
@@ -94,82 +122,168 @@ }); | ||
function makeRequest(self, verb, uriPath, body, cb) { | ||
var finalUri = self.uri + uriPath; | ||
if (debug.enabled) { | ||
debug(util.format('API call to %s: %s', finalUri, body)); | ||
// Given the state of apigee-access, or the "version" of the remote proxy, | ||
// select an implementation. This happens on the first call so that we can | ||
// "start" the module if the proxy is down -- but this doesn't complete | ||
// until we can get one successful HTTP call through | ||
function selectImplementation(self, cb) { | ||
if (self.quotaImpl) { | ||
cb(undefined, self.quotaImpl); | ||
return; | ||
} | ||
var r = url.parse(finalUri); | ||
r.headers = { | ||
'x-DNA-Api-Key': self.key | ||
}; | ||
r.method = verb; | ||
if (body) { | ||
r.headers['Content-Type'] = 'application/x-www-form-urlencoded'; | ||
} | ||
var impl; | ||
if (self.apigeeQuota) { | ||
self.quotaImpl = new ApigeeAccessQuota(self); | ||
cb(undefined, self.quotaImpl); | ||
var req; | ||
if (r.protocol === 'http:') { | ||
req = http.request(r, function(resp) { | ||
requestComplete(resp, cb); | ||
} else { | ||
superagent.agent(). | ||
get(self.options.uri + '/v2/version'). | ||
set('x-DNA-Api-Key', self.options.key). | ||
end(function(err, resp) { | ||
if (err) { | ||
cb(err); | ||
} else { | ||
if (resp.notFound || !semver.satisfies(resp.text, '>=1.1.0')) { | ||
if (self.options.startTime) { | ||
cb(new Error('Quotas with a fixed starting time are not supported')); | ||
} else { | ||
self.quotaImpl = new ApigeeOldRemoteQuota(self); | ||
cb(undefined, self.quotaImpl); | ||
} | ||
} else if (resp.ok) { | ||
self.quotaImpl = new ApigeeRemoteQuota(self); | ||
cb(undefined, self.quotaImpl); | ||
} else { | ||
cb(new Error(util.format('HTTP error getting proxy version: %d', resp.statusCode))); | ||
} | ||
} | ||
}); | ||
} else if (r.protocol === 'https:') { | ||
req = https.request(r, function(resp) { | ||
requestComplete(resp, cb); | ||
}); | ||
} else { | ||
cb(new Error('Unsupported protocol ' + r.protocol)); | ||
return; | ||
} | ||
} | ||
req.on('error', function(err) { | ||
cb(err); | ||
}); | ||
if (body) { | ||
req.end(body); | ||
function makeNewQuotaRequest(self, opts, allow) { | ||
var r ={ | ||
identifier: opts.identifier, | ||
weight: opts.weight, | ||
interval: self.options.interval, | ||
allow: allow, | ||
timeUnit: self.options.timeUnit | ||
}; | ||
if (self.options.startTime) { | ||
// This type of quota just computes fixed buckets of 24 hours, 60 minutes, | ||
// etc. from a fixed start time. No fancy calendar math. | ||
r.startTime = self.options.startTime; | ||
r.quotaType = 'fixedStart'; | ||
} else { | ||
req.end(); | ||
// This is the quota type that works most like others in Volos. | ||
// The window starts when the first message for a given ID arrives. | ||
r.quotaType = 'flexi'; | ||
} | ||
return r; | ||
} | ||
function readResponse(resp, data) { | ||
var d; | ||
do { | ||
d = resp.read(); | ||
if (d) { | ||
data += d; | ||
} | ||
} while (d); | ||
return data; | ||
function ApigeeAccessQuota(quota) { | ||
debug('Using apigee-access for native quota'); | ||
this.quota = quota; | ||
} | ||
function requestComplete(resp, cb) { | ||
resp.on('error', function(err) { | ||
cb(err); | ||
}); | ||
ApigeeAccessQuota.prototype.getImplementationName = function() { | ||
return 'Local'; | ||
}; | ||
var respData = ''; | ||
resp.on('readable', function() { | ||
respData = readResponse(resp, respData); | ||
}); | ||
ApigeeAccessQuota.prototype.apply = function(opts, cb) { | ||
var allow = opts.allow || this.quota.options.allow; | ||
var r = makeNewQuotaRequest(this.quota, opts, allow); | ||
resp.on('end', function() { | ||
if (debug.enabled) { | ||
debug(util.format('API response %d: %s', resp.statusCode, respData)); | ||
} | ||
if (resp.statusCode !== 200) { | ||
var err = new Error('Error on HTTP request'); | ||
err.statusCode = resp.statusCode; | ||
err.message = respData; | ||
// Result is almost the same as a volos result | ||
debug('Local quota request: %j', r); | ||
this.quota.apigeeQuota.apply(r, function(err, result) { | ||
result.expiryTime = result.expiryTime - result.timestamp; | ||
if (err) { | ||
cb(err); | ||
} else { | ||
var ret; | ||
try { | ||
ret = querystring.parse(respData); | ||
} catch (e) { | ||
// The response might not be a query string -- not everything returns it | ||
cb(e); | ||
} | ||
cb(undefined, ret); | ||
debug('Quota result: %j', result); | ||
cb(undefined, result); | ||
} | ||
}); | ||
} | ||
}; | ||
function ApigeeRemoteQuota(quota) { | ||
debug('Using a remote quota'); | ||
this.quota = quota; | ||
} | ||
ApigeeRemoteQuota.prototype.getImplementationName = function() { | ||
return 'Remote'; | ||
}; | ||
ApigeeRemoteQuota.prototype.apply = function(opts, cb) { | ||
var allow = opts.allow || this.quota.options.allow; | ||
var r = makeNewQuotaRequest(this.quota, opts, allow); | ||
debug('Remote quota request: %j', r); | ||
superagent.agent(). | ||
post(this.quota.options.uri + '/v2/quotas/apply'). | ||
set('x-DNA-Api-Key', this.quota.options.key). | ||
type('json'). | ||
send(r). | ||
end(function(err, resp) { | ||
if (err) { | ||
cb(err); | ||
} else if (resp.ok) { | ||
// result from apigee is almost what the module expects | ||
var result = resp.body; | ||
result.expiryTime = result.expiryTime - result.timestamp; | ||
cb(undefined, resp.body); | ||
} else { | ||
cb(new Error(util.format('Error updating remote quota: %d %s', | ||
resp.statusCode, resp.text))); | ||
} | ||
}); | ||
}; | ||
function ApigeeOldRemoteQuota(quota) { | ||
debug('Using a remote quota with the old protocol'); | ||
this.quota = quota; | ||
} | ||
ApigeeOldRemoteQuota.prototype.getImplementationName = function() { | ||
return 'OldRemote'; | ||
}; | ||
ApigeeOldRemoteQuota.prototype.apply = function(opts, cb) { | ||
var allow = opts.allow || this.quota.options.allow; | ||
var r = { | ||
identifier: opts.identifier, | ||
weight: opts.weight, | ||
interval: this.quota.options.interval, | ||
allow: allow, | ||
unit: this.quota.options.timeUnit | ||
}; | ||
debug('Old remote quota request: %j', r); | ||
superagent.agent(). | ||
post(this.quota.options.uri + '/quotas/distributed'). | ||
set('x-DNA-Api-Key', this.quota.options.key). | ||
type('form'). | ||
send(r). | ||
end(function(err, resp) { | ||
if (err) { | ||
cb(err); | ||
} else if (resp.ok) { | ||
debug('result: %s', resp.text); | ||
var result = { | ||
allowed: resp.body.allowed, | ||
used: resp.body.used, | ||
isAllowed: !resp.body.failed, | ||
expiryTime: resp.body.expiry_time - resp.body.ts | ||
}; | ||
cb(undefined, result); | ||
} else { | ||
cb(new Error(util.format('Error updating remote quota: %d %s', | ||
resp.statusCode, resp.text))); | ||
} | ||
}); | ||
}; |
{ | ||
"name": "volos-quota-apigee", | ||
"version": "0.9.3", | ||
"version": "0.10.0", | ||
"main": "lib/apigeequota.js", | ||
@@ -13,4 +13,7 @@ "license": "MIT", | ||
"dependencies": { | ||
"apigee-access": ">=1.2.x", | ||
"debug": "1.0.x", | ||
"volos-quota-common": "0.9.x" | ||
"semver": "3.x.x", | ||
"superagent": "0.x.x", | ||
"volos-quota-common": "0.10.x" | ||
}, | ||
@@ -17,0 +20,0 @@ "devDependencies": {}, |
@@ -34,3 +34,5 @@ # volos-quota-apigee | ||
* uri: The full URI of the Apigee adapter that you deployed in the last step. For instance, if the organization | ||
name is "foo" then this might be "https://foo-test.apigee.net/adapterproxy". | ||
name is "foo" then this might be "https://foo-test.apigee.net/apigee-remote-proxy". This parameter is | ||
not used when the app is always deployed to Apigee Edge, or when "apigeeMode" is set to "local". | ||
See below for more. | ||
* key: An API consumer key for a valid "application" that is part of the same organization where the adapter | ||
@@ -44,2 +46,20 @@ was installed. | ||
The following parameters are optional: | ||
* apigeeMode (optional): By default, this module will use the OAuth service built in to Apigee Edge when it | ||
is deployed there, and use an HTTP-based API to the "uri" specified above when it is not. This parameter | ||
overrides that default. | ||
"apigeeMode" supports two values: | ||
* remote: When set to "remote," this module will use the "uri" specified in the options to communicate | ||
with Apigee Edge, even if it is deployed to Apigee Edge. This allows you to use the OAuth services of | ||
another organization, for instance. | ||
* local: When set to "local," this module wil use the "apigee-access" module to access Apigee Edge | ||
functionality that is built in to the runtime. If this is set and the module is running outside | ||
Apigee Edge, then all calls will fail. | ||
By default, the module will use Apigee Edge functionality when available, and fall back to API | ||
calls when it is not. | ||
## Example | ||
@@ -46,0 +66,0 @@ |
@@ -35,4 +35,6 @@ /**************************************************************************** | ||
this.timeout(150000); | ||
var implementationName; | ||
this.timeout(120000); | ||
function id(_id) { | ||
@@ -55,3 +57,3 @@ return 'test:' + random + ":" + _id; | ||
before(function() { | ||
before(function(done) { | ||
var options = extend(config, { | ||
@@ -63,2 +65,7 @@ timeUnit: 'minute', | ||
pm = Spi.create(options); | ||
pm.quota.getImplementationName(function(err, implName) { | ||
implementationName = implName; | ||
done(err); | ||
}); | ||
}); | ||
@@ -68,2 +75,4 @@ | ||
// Verify that quota is reset within the expiration time, | ||
// plus a fudge factor for distributed quota sync | ||
it('Minute', function(done) { | ||
@@ -74,7 +83,4 @@ var hit = { identifier: id('TimeOne'), weight: 1 }; | ||
checkResult(result, 2, 1, true); | ||
var expiry = Date.now() + 60000; | ||
result.expiryTime.should.be.approximately(expiry, 10000); | ||
result.expiryTime.should.be.approximately(60000, 10000); | ||
var offset = result.expiryTime - expiry; | ||
setTimeout(function() { | ||
@@ -85,3 +91,3 @@ pm.apply(hit, function(err, result) { | ||
// Ensure quota is reset after a minute | ||
// Ensure quota is reset within a minute | ||
setTimeout(function() { | ||
@@ -93,6 +99,6 @@ pm.apply(hit, function(err, result) { | ||
}); | ||
}, 120000 + offset); | ||
}, 40001); | ||
}); | ||
}, 5000); | ||
}, 40001); | ||
}); | ||
@@ -103,2 +109,3 @@ }); | ||
describe('Basic', function() { | ||
// Just do some basic counting | ||
it('Minute', function(done) { | ||
@@ -111,26 +118,145 @@ pm.apply({ | ||
checkResult(result, 2, 1, true); | ||
result.expiryTime.should.be.approximately(Date.now() + 60000, 10000); | ||
result.expiryTime.should.be.approximately(60000, 10000); | ||
setTimeout(function() { | ||
pm.apply({ | ||
identifier: id('One'), | ||
weight: 1 | ||
}, function(err, result) { | ||
assert(!err); | ||
checkResult(result, 2, 2, true); | ||
pm.apply({ | ||
identifier: id('One'), | ||
identifier: id('Two'), | ||
weight: 1 | ||
}, function(err, result) { | ||
assert(!err); | ||
checkResult(result, 2, 2, true); | ||
pm.apply({ | ||
identifier: id('Two'), | ||
weight: 1 | ||
}, function(err, result) { | ||
assert(!err); | ||
checkResult(result, 2, 1, true); | ||
done(); | ||
}); | ||
checkResult(result, 2, 1, true); | ||
done(); | ||
}); | ||
}, 5000); | ||
}); | ||
}); | ||
}); | ||
it('Quota weight', function(done) { | ||
// Ensure that weighting works | ||
// Apigee won't keep counting once the quota is exceeded, so set a | ||
// larger "allow" value for this | ||
pm.apply({ | ||
identifier: id('WeightOne'), | ||
weight: 1, | ||
allow: 10 | ||
}, function(err, result) { | ||
assert(!err); | ||
checkResult(result, 10, 1, true); | ||
pm.apply({ | ||
identifier: id('WeightOne'), | ||
weight: 3, | ||
allow: 10 | ||
}, function(err, result) { | ||
assert(!err); | ||
checkResult(result, 10, 4, true); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
}); | ||
describe('Calendar', function() { | ||
// For each of these, verify that the parameters work and that we can | ||
// calculate the right expiration time. Do not wait for expiration | ||
// as we can't seem to get consistent results there yet. | ||
it('Minute', function(done) { | ||
if (implementationName === 'OldRemote') { | ||
console.log('Skipping calendar tests because old implementation doesn\'t support them'); | ||
done(); | ||
} | ||
this.timeout(2000); | ||
var startTime = Date.now() - 59000; // start almost a minute ago | ||
var options = extend(config, { | ||
timeUnit: 'minute', | ||
interval: 1, | ||
allow: 1, | ||
startTime: startTime | ||
}); | ||
var pm = Spi.create(options); | ||
var hit = { identifier: id('TimeTwo'), weight: 1 }; | ||
pm.apply(hit, function(err, result) { | ||
assert(!err); | ||
checkResult(result, 1, 1, true); | ||
result.expiryTime.should.be.approximately(1000, 500); | ||
done(); | ||
}); | ||
}); | ||
it('Hour', function(done) { | ||
if (implementationName === 'OldRemote') { | ||
done(); | ||
} | ||
this.timeout(2000); | ||
var startTime = Date.now() - (60000 * 60 - 1000); // start almost an hour ago | ||
var options = extend(config, { | ||
timeUnit: 'hour', | ||
interval: 1, | ||
allow: 1, | ||
startTime: startTime | ||
}); | ||
var pm = Spi.create(options); | ||
var hit = { identifier: id('TimeThree'), weight: 1 }; | ||
pm.apply(hit, function(err, result) { | ||
assert(!err); | ||
checkResult(result, 1, 1, true); | ||
result.expiryTime.should.be.approximately(1000, 500); | ||
done(); | ||
}); | ||
}); | ||
it('Day', function(done) { | ||
if (implementationName === 'OldRemote') { | ||
done(); | ||
} | ||
this.timeout(2000); | ||
var startTime = Date.now() - (60000 * 60 * 24 - 1000); // start almost a day ago | ||
var options = extend(config, { | ||
timeUnit: 'day', | ||
interval: 1, | ||
allow: 1, | ||
startTime: startTime | ||
}); | ||
var pm = Spi.create(options); | ||
var hit = { identifier: id('TimeFour'), weight: 1 }; | ||
pm.apply(hit, function(err, result) { | ||
assert(!err); | ||
checkResult(result, 1, 1, true); | ||
result.expiryTime.should.be.approximately(1000, 500); | ||
done(); | ||
}); | ||
}); | ||
it('Week', function(done) { | ||
if (implementationName === 'OldRemote') { | ||
done(); | ||
} | ||
this.timeout(2000); | ||
var startTime = Date.now() - (60000 * 60 * 24 * 7 - 1000); // start almost a week ago | ||
var options = extend(config, { | ||
timeUnit: 'week', | ||
interval: 1, | ||
allow: 1, | ||
startTime: startTime | ||
}); | ||
var pm = Spi.create(options); | ||
var hit = { identifier: id('TimeFive'), weight: 1 }; | ||
pm.apply(hit, function(err, result) { | ||
assert(!err); | ||
checkResult(result, 1, 1, true); | ||
result.expiryTime.should.be.approximately(1000, 500); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
}); |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
20693
475
81
5
3
+ Addedapigee-access@>=1.2.x
+ Addedsemver@3.x.x
+ Addedsuperagent@0.x.x
+ Addedapigee-access@1.4.0(transitive)
+ Addedasync@0.9.2(transitive)
+ Addedcombined-stream@0.0.7(transitive)
+ Addedcomponent-emitter@1.1.2(transitive)
+ Addedcookiejar@2.0.1(transitive)
+ Addedcore-util-is@1.0.3(transitive)
+ Addeddebug@2.6.9(transitive)
+ Addeddelayed-stream@0.0.5(transitive)
+ Addedextend@1.2.1(transitive)
+ Addedform-data@0.1.3(transitive)
+ Addedformidable@1.0.14(transitive)
+ Addedinherits@2.0.4(transitive)
+ Addedisarray@0.0.1(transitive)
+ Addedmethods@1.0.1(transitive)
+ Addedmime@1.2.11(transitive)
+ Addedqs@1.2.0(transitive)
+ Addedreadable-stream@1.0.27-1(transitive)
+ Addedreduce-component@1.0.1(transitive)
+ Addedsemver@3.0.1(transitive)
+ Addedstring_decoder@0.10.31(transitive)
+ Addedsuperagent@0.21.0(transitive)
+ Addedvolos-quota-common@0.10.2(transitive)
- Removedvolos-quota-common@0.9.11(transitive)
Updatedvolos-quota-common@0.10.x