@withjoy/limiter 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,16 +24,16 @@ Your client can use any subset of these Buckets, but when it does, the Best Prac
24
24
  However, for flexibility reasons, this is not strictly enforced.
25
25
 
26
26
  ## Why limitd-redis ?
27
- Although this package is available on npm, it's no longer actively maintained. We've observed bugs in the existing published version, but these issues have already been resolved in the [limitd-redis](https://github.com/auth0/limitd-redis) gitHub repository. To address this, we've imported the latest stable version(v7.8.1) from there github repository and using it locally in the limiter.
27
+ Although this package is available on npm, it's no longer actively maintained. We've observed bugs in the existing published version, but these issues have already been resolved in the [limitd-redis](https://github.com/auth0/limitd-redis) gitHub repository. To address this, we've imported the latest stable version(v7.8.1) from there github repository and using it locally in the `limiter`.
28
28
 
29
29
  In case we need to update in the future, we can replace the "limitd-redis" directory with the latest version of [limitd-redis](https://github.com/auth0/limitd-redis) from their GitHub repository.
30
30
 
31
- Note: We need to add dependency from limitd-redis to limiter package json before publish. We are using limitd-redis as directly not package inside limiter.
31
+ Note: We need to add dependency from `limitd-redis` to `limiter` package json before publish. We are using `limitd-redis` as directly not package inside `limiter`.
32
32
 
33
33
  ## Testing:
34
34
 
35
35
  `npm run test` will run offline tests using a mock.
36
- `npm run sanity-check` will run against a limitd service configured to run with the test buckets.
36
+ `npm run sanity-check` will run against a `limitd` service configured to run with the test buckets.
37
37
 
38
38
 
39
39
  ## Quick Test
package/limiter.js CHANGED
@@ -12,6 +12,7 @@ const defaultBuckets = {
12
12
  eventsByIpRate: { size: 240, per_hour: 240 },
13
13
  paymentAttempts: { size: 5000 },
14
14
  paymentAttemptsByReceiverId: { size: 900 },
15
+ paymentAttemptsGiftCards: { size: 3000 },
15
16
  // for development testing
16
17
  testPerMinute: { size: 3, per_minute: 1 },
17
18
  };
@@ -88,12 +89,36 @@ const limiterRetryConfig = z
88
89
  .strict()
89
90
  .default(LIMITER_RETRY_DEFAULTS);
90
91
 
92
+ // from https://github.com/auth0/limitd-redis#buckets
93
+ const limiterBucketConfig = z.object({
94
+ size: z.number(),
95
+ per_day: z.number().optional(),
96
+ per_hour: z.number().optional(),
97
+ per_minute: z.number().optional(),
98
+ per_second: z.number().optional(),
99
+ // and that's all we care about
100
+ // we need more? implement more of the `limitd-redis` schema
101
+ });
102
+
103
+ const limiterConfig = z
104
+ .object({
105
+ limitdUrl: z.string(),
106
+ // the rest are optional / have defaults
107
+ buckets: z.record(z.string(), limiterBucketConfig).optional(),
108
+ keyPrefix: z.string().default(defaultRedisKeyPrefix),
109
+ circuitbreaker: limiterCircuitBreakerConfig,
110
+ retry: limiterRetryConfig,
111
+ commandTimeout: z.number().default(30),
112
+ })
113
+ .strict();
114
+ ;
115
+
91
116
  class Limiter {
92
117
  // static defaultBuckets;
93
118
  // static pickDefaultBuckets(keys) { ... };
94
119
 
95
120
  constructor(config) {
96
- this._config = config;
121
+ this._config = limiterConfig.parse(config);
97
122
  this._limitdRedisConnectionPromise = this.connect();
98
123
  }
99
124
 
@@ -104,12 +129,12 @@ class Limiter {
104
129
  */
105
130
  connect() {
106
131
  return new Promise((resolve, reject) => {
107
- let buckets = this._config.buckets || {};
132
+ let buckets = this._config.buckets || {};
108
133
  buckets = {
109
134
  ...defaultBuckets,
110
135
  ...buckets,
111
136
  };
112
- const keyPrefix = this._config.keyPrefix || defaultRedisKeyPrefix;
137
+ const keyPrefix = this._config.keyPrefix;
113
138
  if (!connection) {
114
139
  connection = new LimitdRedis({
115
140
  uri: this._config.limitdUrl,
@@ -136,6 +161,9 @@ let buckets = this._config.buckets || {};
136
161
  close() {
137
162
  return new Promise(async (resolve, reject) => {
138
163
  const connectionPromise = await this._limitdRedisConnectionPromise;
164
+ if (!connectionPromise) {
165
+ return resolve(null);
166
+ }
139
167
  connectionPromise.close((err, resp) => {
140
168
  if (err) {
141
169
  this._console.error(err);
@@ -253,7 +281,11 @@ Limiter.pickDefaultBuckets = function pickDefaultBuckets(keyArray) {
253
281
  }
254
282
  return buckets;
255
283
  };
284
+
285
+ // schemas
256
286
  Limiter.limiterCircuitBreakerConfig = limiterCircuitBreakerConfig;
257
287
  Limiter.limiterRetryConfig = limiterRetryConfig;
288
+ Limiter.limiterBucketConfig = limiterBucketConfig;
289
+ Limiter.limiterConfig = limiterConfig;
258
290
 
259
291
  module.exports = Limiter;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withjoy/limiter",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "description": "Api Rate limiter",
5
5
  "main": "limiter.js",
6
6
  "scripts": {
@@ -54,7 +54,109 @@ const limiterWithMockService = async (succeeds = true, conforms = true) => {
54
54
  return limiterProcessor;
55
55
  };
56
56
 
57
- test("perfrom limitd-redis testing with test bucket", async (done) => {
57
+ describe("Limiter", () => {
58
+ let limiter;
59
+ beforeEach(() => {
60
+ jest.spyOn(Limiter.prototype, "connect").mockImplementation(async () => Promise.resolve());
61
+ });
62
+ afterEach(async () => {
63
+ await limiter.close();
64
+ jest.restoreAllMocks();
65
+ });
66
+
67
+ describe("constructor", () => {
68
+ it('accepts maximal valid configuration', () => {
69
+ limiter = new Limiter({
70
+ limitdUrl: 'localhost:6379',
71
+ buckets: {
72
+ ...TEST_BUCKETS,
73
+ perDay: { size: 11, per_day: 11 },
74
+ perMinute: { size: 21, per_minute: 21 },
75
+ perSecond: { size: 31, per_second: 31 },
76
+ },
77
+ keyPrefix: 'maximal:',
78
+ circuitbreaker: {
79
+ timeout: '41s',
80
+ maxFailures: 42,
81
+ cooldown: '43s',
82
+ maxCooldown: '44s',
83
+ name: 'maximal',
84
+ },
85
+ retry: {
86
+ retries: 51,
87
+ minTimeout: 52,
88
+ maxTimeout: 53,
89
+ },
90
+ commandTimeout: 61,
91
+ });
92
+ expect(limiter._config.limitdUrl).toBe('localhost:6379');
93
+ expect(limiter._config.buckets.perDay.size).toBe(11);
94
+ expect(limiter._config.keyPrefix).toBe('maximal:');
95
+ expect(limiter._config.circuitbreaker.timeout).toBe('41s');
96
+ expect(limiter._config.retry.retries).toBe(51);
97
+ expect(limiter._config.commandTimeout).toBe(61);
98
+ });
99
+
100
+ it("accepts minimum valid configuration", async () => {
101
+ limiter = new Limiter({
102
+ limitdUrl: 'localhost:6379',
103
+ });
104
+ expect(limiter._config.limitdUrl).toBe('localhost:6379');
105
+ expect(limiter._config.buckets).toBeUndefined();
106
+ expect(limiter._config.keyPrefix).toBe('limitdRedis:');
107
+ expect(limiter._config.circuitbreaker.timeout).toBe('0.50s');
108
+ expect(limiter._config.retry.retries).toBe(3);
109
+ expect(limiter._config.commandTimeout).toBe(30);
110
+ });
111
+
112
+ it("fails on invalid bucket configuration", () => {
113
+ expect(() => {
114
+ limiter = new Limiter({
115
+ limitdUrl: 'localhost:6379',
116
+ buckets: {
117
+ invalidBucket: { size: 'string' }, // Poisoning with an invalid size
118
+ },
119
+ });
120
+ }).toThrow(/"invalidBucket"/);
121
+ });
122
+
123
+ it("cannot fail on invalid circuitbreaker configuration", () => {
124
+ expect(() => {
125
+ limiter = new Limiter({
126
+ limitdUrl: 'localhost:6379',
127
+ circuitbreaker: {
128
+ timeout: 'still-valid',
129
+ },
130
+ });
131
+ }).not.toThrow();
132
+ });
133
+
134
+ it("fails on invalid retry configuration", () => {
135
+ expect(() => {
136
+ limiter = new Limiter({
137
+ limitdUrl: 'localhost:6379',
138
+ retry: {
139
+ retries: 'not-a-number', // Poisoning with an invalid number of retries
140
+ minTimeout: 52,
141
+ maxTimeout: 53,
142
+ },
143
+ });
144
+ }).toThrow(/"retries"/);
145
+ });
146
+
147
+ it("fails when limitdUrl is missing", () => {
148
+ expect(() => {
149
+ limiter = new Limiter({
150
+ buckets: {
151
+ emails: { size: 10 },
152
+ },
153
+ });
154
+ }).toThrow(/"limitdUrl"/);
155
+ });
156
+ });
157
+ });
158
+
159
+ test("perform limitd-redis testing with test bucket", async (done) => {
58
160
  const limiter = new Limiter({
59
161
  limitdUrl: "localhost:6379",
60
162
  buckets: {