@thangnv-dev/rate-limiter-mikroorm-node 0.0.1
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/dist/errors.d.ts +8 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +25 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations/migration-20260221143000-rate-limiter.d.ts +6 -0
- package/dist/migrations/migration-20260221143000-rate-limiter.d.ts.map +1 -0
- package/dist/migrations/migration-20260221143000-rate-limiter.js +137 -0
- package/dist/migrations/migration-20260221143000-rate-limiter.js.map +1 -0
- package/dist/rate-limiter.d.ts +11 -0
- package/dist/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limiter.js +172 -0
- package/dist/rate-limiter.js.map +1 -0
- package/package.json +49 -0
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { StandardError } from '@thangnv-dev/error-common';
|
|
2
|
+
export declare class RateLimiterQueryError extends StandardError {
|
|
3
|
+
constructor(operation: string, cause?: unknown);
|
|
4
|
+
}
|
|
5
|
+
export declare class RateLimiterInvalidResultError extends StandardError {
|
|
6
|
+
constructor(field: string, cause?: unknown);
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AAIzD,qBAAa,qBAAsB,SAAQ,aAAa;gBAC1C,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAS/C;AAED,qBAAa,6BAA8B,SAAQ,aAAa;gBAClD,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAS3C"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { StandardError } from '@thangnv-dev/error-common';
|
|
2
|
+
const RATE_LIMITER_IMPL_PACKAGE = '@thangnv-dev/rate-limiter-mikroorm-node';
|
|
3
|
+
export class RateLimiterQueryError extends StandardError {
|
|
4
|
+
constructor(operation, cause) {
|
|
5
|
+
super({
|
|
6
|
+
package: RATE_LIMITER_IMPL_PACKAGE,
|
|
7
|
+
code: 'RATE_LIMITER_QUERY_FAILED',
|
|
8
|
+
cause,
|
|
9
|
+
message: `${operation} query failed`,
|
|
10
|
+
});
|
|
11
|
+
this.name = 'RateLimiterQueryError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export class RateLimiterInvalidResultError extends StandardError {
|
|
15
|
+
constructor(field, cause) {
|
|
16
|
+
super({
|
|
17
|
+
package: RATE_LIMITER_IMPL_PACKAGE,
|
|
18
|
+
code: 'RATE_LIMITER_INVALID_RESULT',
|
|
19
|
+
cause,
|
|
20
|
+
message: `invalid rate-limiter result field: ${field}`,
|
|
21
|
+
});
|
|
22
|
+
this.name = 'RateLimiterInvalidResultError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AAEzD,MAAM,yBAAyB,GAAG,yCAAyC,CAAA;AAE3E,MAAM,OAAO,qBAAsB,SAAQ,aAAa;IACtD,YAAY,SAAiB,EAAE,KAAe;QAC5C,KAAK,CAAC;YACJ,OAAO,EAAE,yBAAyB;YAClC,IAAI,EAAE,2BAA2B;YACjC,KAAK;YACL,OAAO,EAAE,GAAG,SAAS,eAAe;SACrC,CAAC,CAAA;QACF,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAA;IACrC,CAAC;CACF;AAED,MAAM,OAAO,6BAA8B,SAAQ,aAAa;IAC9D,YAAY,KAAa,EAAE,KAAe;QACxC,KAAK,CAAC;YACJ,OAAO,EAAE,yBAAyB;YAClC,IAAI,EAAE,6BAA6B;YACnC,KAAK;YACL,OAAO,EAAE,sCAAsC,KAAK,EAAE;SACvD,CAAC,CAAA;QACF,IAAI,CAAC,IAAI,GAAG,+BAA+B,CAAA;IAC7C,CAAC;CACF"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { RateLimiterInvalidResultError, RateLimiterQueryError } from './errors.js';
|
|
2
|
+
export { KnexRateLimiter } from './rate-limiter.js';
|
|
3
|
+
export { Migration20260221143000RateLimiter } from './migrations/migration-20260221143000-rate-limiter.js';
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,6BAA6B,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAClF,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,kCAAkC,EAAE,MAAM,uDAAuD,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { RateLimiterInvalidResultError, RateLimiterQueryError } from './errors.js';
|
|
2
|
+
export { KnexRateLimiter } from './rate-limiter.js';
|
|
3
|
+
export { Migration20260221143000RateLimiter } from './migrations/migration-20260221143000-rate-limiter.js';
|
|
4
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,6BAA6B,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAClF,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,kCAAkC,EAAE,MAAM,uDAAuD,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migration-20260221143000-rate-limiter.d.ts","sourceRoot":"","sources":["../../src/migrations/migration-20260221143000-rate-limiter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AAEjD,qBAAa,kCAAmC,SAAQ,SAAS;IAChD,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAmInB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAQrC"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
export class Migration20260221143000RateLimiter extends Migration {
|
|
3
|
+
async up() {
|
|
4
|
+
this.addSql('create schema if not exists rate_limiter;');
|
|
5
|
+
this.addSql(`
|
|
6
|
+
create table if not exists rate_limiter.buckets (
|
|
7
|
+
bucket_key text primary key,
|
|
8
|
+
capacity bigint not null check (capacity > 0),
|
|
9
|
+
refill_tokens bigint not null check (refill_tokens > 0),
|
|
10
|
+
refill_interval_ms bigint not null check (refill_interval_ms > 0),
|
|
11
|
+
tokens numeric not null check (tokens >= 0),
|
|
12
|
+
updated_at timestamptz not null default clock_timestamp(),
|
|
13
|
+
created_at timestamptz not null default clock_timestamp()
|
|
14
|
+
);
|
|
15
|
+
`);
|
|
16
|
+
this.addSql(`
|
|
17
|
+
create or replace function rate_limiter.take_tokens(
|
|
18
|
+
p_bucket_key varchar,
|
|
19
|
+
p_capacity bigint,
|
|
20
|
+
p_refill_tokens bigint,
|
|
21
|
+
p_refill_interval_ms bigint,
|
|
22
|
+
p_tokens bigint default 1
|
|
23
|
+
)
|
|
24
|
+
returns table (
|
|
25
|
+
allowed boolean,
|
|
26
|
+
remaining bigint,
|
|
27
|
+
retry_after_ms bigint,
|
|
28
|
+
rate_limit bigint
|
|
29
|
+
)
|
|
30
|
+
language plpgsql
|
|
31
|
+
as $$
|
|
32
|
+
declare
|
|
33
|
+
current_bucket rate_limiter.buckets%rowtype;
|
|
34
|
+
now_ts timestamptz := clock_timestamp();
|
|
35
|
+
elapsed_ms numeric;
|
|
36
|
+
refill_amount numeric;
|
|
37
|
+
refilled_tokens numeric;
|
|
38
|
+
requested_tokens numeric := p_tokens::numeric;
|
|
39
|
+
remaining_tokens numeric;
|
|
40
|
+
is_allowed boolean;
|
|
41
|
+
retry_after bigint;
|
|
42
|
+
begin
|
|
43
|
+
if p_bucket_key is null or btrim(p_bucket_key) = '' then
|
|
44
|
+
raise exception 'bucket_key must not be empty';
|
|
45
|
+
end if;
|
|
46
|
+
if p_capacity <= 0 then
|
|
47
|
+
raise exception 'capacity must be greater than 0';
|
|
48
|
+
end if;
|
|
49
|
+
if p_refill_tokens <= 0 then
|
|
50
|
+
raise exception 'refill_tokens must be greater than 0';
|
|
51
|
+
end if;
|
|
52
|
+
if p_refill_interval_ms <= 0 then
|
|
53
|
+
raise exception 'refill_interval_ms must be greater than 0';
|
|
54
|
+
end if;
|
|
55
|
+
if p_tokens <= 0 then
|
|
56
|
+
raise exception 'tokens must be greater than 0';
|
|
57
|
+
end if;
|
|
58
|
+
|
|
59
|
+
insert into rate_limiter.buckets (
|
|
60
|
+
bucket_key,
|
|
61
|
+
capacity,
|
|
62
|
+
refill_tokens,
|
|
63
|
+
refill_interval_ms,
|
|
64
|
+
tokens,
|
|
65
|
+
updated_at,
|
|
66
|
+
created_at
|
|
67
|
+
)
|
|
68
|
+
values (
|
|
69
|
+
p_bucket_key,
|
|
70
|
+
p_capacity,
|
|
71
|
+
p_refill_tokens,
|
|
72
|
+
p_refill_interval_ms,
|
|
73
|
+
p_capacity::numeric,
|
|
74
|
+
now_ts,
|
|
75
|
+
now_ts
|
|
76
|
+
)
|
|
77
|
+
on conflict (bucket_key)
|
|
78
|
+
do update set
|
|
79
|
+
capacity = excluded.capacity,
|
|
80
|
+
refill_tokens = excluded.refill_tokens,
|
|
81
|
+
refill_interval_ms = excluded.refill_interval_ms
|
|
82
|
+
returning * into current_bucket;
|
|
83
|
+
|
|
84
|
+
elapsed_ms := greatest(0, extract(epoch from (now_ts - current_bucket.updated_at)) * 1000);
|
|
85
|
+
refill_amount := (elapsed_ms * current_bucket.refill_tokens::numeric) / current_bucket.refill_interval_ms::numeric;
|
|
86
|
+
refilled_tokens := least(current_bucket.capacity::numeric, current_bucket.tokens + refill_amount);
|
|
87
|
+
|
|
88
|
+
if refilled_tokens >= requested_tokens then
|
|
89
|
+
is_allowed := true;
|
|
90
|
+
remaining_tokens := greatest(0, refilled_tokens - requested_tokens);
|
|
91
|
+
retry_after := 0;
|
|
92
|
+
else
|
|
93
|
+
is_allowed := false;
|
|
94
|
+
remaining_tokens := greatest(0, refilled_tokens);
|
|
95
|
+
retry_after := ceil(
|
|
96
|
+
((requested_tokens - refilled_tokens) * current_bucket.refill_interval_ms::numeric)
|
|
97
|
+
/ current_bucket.refill_tokens::numeric
|
|
98
|
+
)::bigint;
|
|
99
|
+
end if;
|
|
100
|
+
|
|
101
|
+
update rate_limiter.buckets
|
|
102
|
+
set
|
|
103
|
+
tokens = remaining_tokens,
|
|
104
|
+
updated_at = now_ts,
|
|
105
|
+
capacity = p_capacity,
|
|
106
|
+
refill_tokens = p_refill_tokens,
|
|
107
|
+
refill_interval_ms = p_refill_interval_ms
|
|
108
|
+
where bucket_key = p_bucket_key;
|
|
109
|
+
|
|
110
|
+
return query
|
|
111
|
+
select
|
|
112
|
+
is_allowed,
|
|
113
|
+
floor(remaining_tokens)::bigint,
|
|
114
|
+
retry_after,
|
|
115
|
+
p_capacity;
|
|
116
|
+
end;
|
|
117
|
+
$$;
|
|
118
|
+
`);
|
|
119
|
+
this.addSql(`
|
|
120
|
+
create or replace function rate_limiter.reset_bucket(
|
|
121
|
+
p_bucket_key varchar
|
|
122
|
+
)
|
|
123
|
+
returns void
|
|
124
|
+
language sql
|
|
125
|
+
as $$
|
|
126
|
+
delete from rate_limiter.buckets where bucket_key = p_bucket_key;
|
|
127
|
+
$$;
|
|
128
|
+
`);
|
|
129
|
+
}
|
|
130
|
+
async down() {
|
|
131
|
+
this.addSql('drop function if exists rate_limiter.reset_bucket(varchar);');
|
|
132
|
+
this.addSql('drop function if exists rate_limiter.take_tokens(varchar, bigint, bigint, bigint, bigint);');
|
|
133
|
+
this.addSql('drop table if exists rate_limiter.buckets;');
|
|
134
|
+
this.addSql('drop schema if exists rate_limiter;');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=migration-20260221143000-rate-limiter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migration-20260221143000-rate-limiter.js","sourceRoot":"","sources":["../../src/migrations/migration-20260221143000-rate-limiter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AAEjD,MAAM,OAAO,kCAAmC,SAAQ,SAAS;IACtD,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CAAC,2CAA2C,CAAC,CAAA;QAExD,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;KAUX,CAAC,CAAA;QAEF,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAsGX,CAAC,CAAA;QAEF,IAAI,CAAC,MAAM,CAAC;;;;;;;;;KASX,CAAC,CAAA;IACJ,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,6DAA6D,CAAC,CAAA;QAC1E,IAAI,CAAC,MAAM,CACT,4FAA4F,CAC7F,CAAA;QACD,IAAI,CAAC,MAAM,CAAC,4CAA4C,CAAC,CAAA;QACzD,IAAI,CAAC,MAAM,CAAC,qCAAqC,CAAC,CAAA;IACpD,CAAC;CACF"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import 'temporal-polyfill/global';
|
|
2
|
+
import { type RateLimiter, type RateLimiterTakeInput, type RateLimiterTakeResult } from '@thangnv-dev/rate-limiter-node';
|
|
3
|
+
import type { SqlEntityManager } from '@mikro-orm/knex';
|
|
4
|
+
export declare class KnexRateLimiter implements RateLimiter {
|
|
5
|
+
private readonly em;
|
|
6
|
+
constructor(em: SqlEntityManager);
|
|
7
|
+
take(input: RateLimiterTakeInput): Promise<RateLimiterTakeResult>;
|
|
8
|
+
reset(key: string): Promise<void>;
|
|
9
|
+
private queryRows;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=rate-limiter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../src/rate-limiter.ts"],"names":[],"mappings":"AAAA,OAAO,0BAA0B,CAAA;AACjC,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC3B,MAAM,gCAAgC,CAAA;AACvC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAoJvD,qBAAa,eAAgB,YAAW,WAAW;IACjD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAkB;gBAEzB,EAAE,EAAE,gBAAgB;IAI1B,IAAI,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,qBAAqB,CAAC;IA+CjE,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAKzB,SAAS;CAYxB"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import 'temporal-polyfill/global';
|
|
2
|
+
import { RATE_LIMITER_DEFAULT_TOKENS, } from '@thangnv-dev/rate-limiter-node';
|
|
3
|
+
import { RateLimiterInvalidResultError, RateLimiterQueryError } from './errors.js';
|
|
4
|
+
function isRecord(value) {
|
|
5
|
+
return typeof value === 'object' && value !== null;
|
|
6
|
+
}
|
|
7
|
+
function readField(record, keys) {
|
|
8
|
+
for (const key of keys) {
|
|
9
|
+
if (Object.prototype.hasOwnProperty.call(record, key)) {
|
|
10
|
+
return record[key];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
function requireNonEmptyString(fieldName, value) {
|
|
16
|
+
if (typeof value !== 'string') {
|
|
17
|
+
throw new TypeError(`${fieldName} must be a string`);
|
|
18
|
+
}
|
|
19
|
+
const trimmed = value.trim();
|
|
20
|
+
if (!trimmed) {
|
|
21
|
+
throw new TypeError(`${fieldName} must not be empty`);
|
|
22
|
+
}
|
|
23
|
+
return trimmed;
|
|
24
|
+
}
|
|
25
|
+
function requirePositiveBigInt(fieldName, value) {
|
|
26
|
+
if (typeof value !== 'bigint') {
|
|
27
|
+
throw new TypeError(`${fieldName} must be a bigint`);
|
|
28
|
+
}
|
|
29
|
+
if (value <= 0n) {
|
|
30
|
+
throw new TypeError(`${fieldName} must be greater than 0`);
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
function durationToPositiveMillisecondBigInt(fieldName, value) {
|
|
35
|
+
if (!(value instanceof Temporal.Duration)) {
|
|
36
|
+
throw new TypeError(`${fieldName} must be a Temporal.Duration`);
|
|
37
|
+
}
|
|
38
|
+
let milliseconds;
|
|
39
|
+
try {
|
|
40
|
+
milliseconds = value.total({ unit: 'millisecond' });
|
|
41
|
+
}
|
|
42
|
+
catch (cause) {
|
|
43
|
+
throw new TypeError(`${fieldName} must use fixed-length time units`, { cause });
|
|
44
|
+
}
|
|
45
|
+
if (!Number.isFinite(milliseconds)) {
|
|
46
|
+
throw new TypeError(`${fieldName} must be finite`);
|
|
47
|
+
}
|
|
48
|
+
if (milliseconds <= 0) {
|
|
49
|
+
throw new TypeError(`${fieldName} must be greater than 0`);
|
|
50
|
+
}
|
|
51
|
+
return BigInt(Math.ceil(milliseconds));
|
|
52
|
+
}
|
|
53
|
+
function toDurationFromMilliseconds(value) {
|
|
54
|
+
if (value < 0n) {
|
|
55
|
+
throw new RateLimiterInvalidResultError('retry_after_ms');
|
|
56
|
+
}
|
|
57
|
+
if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
58
|
+
throw new RateLimiterInvalidResultError('retry_after_ms');
|
|
59
|
+
}
|
|
60
|
+
return Temporal.Duration.from({ milliseconds: Number(value) });
|
|
61
|
+
}
|
|
62
|
+
function toPgBigInt(value) {
|
|
63
|
+
return value.toString();
|
|
64
|
+
}
|
|
65
|
+
function parseBooleanField(fieldName, value) {
|
|
66
|
+
if (typeof value === 'boolean') {
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
if (typeof value === 'string') {
|
|
70
|
+
const normalized = value.trim().toLowerCase();
|
|
71
|
+
if (normalized === 'true' || normalized === 't' || normalized === '1') {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
if (normalized === 'false' || normalized === 'f' || normalized === '0') {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (typeof value === 'number') {
|
|
79
|
+
if (value === 1) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
if (value === 0) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
throw new RateLimiterInvalidResultError(fieldName);
|
|
87
|
+
}
|
|
88
|
+
function parseBigIntField(fieldName, value) {
|
|
89
|
+
if (typeof value === 'bigint') {
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
if (typeof value === 'string') {
|
|
93
|
+
try {
|
|
94
|
+
return BigInt(value);
|
|
95
|
+
}
|
|
96
|
+
catch (cause) {
|
|
97
|
+
throw new RateLimiterInvalidResultError(fieldName, cause);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (typeof value === 'number' && Number.isInteger(value)) {
|
|
101
|
+
return BigInt(value);
|
|
102
|
+
}
|
|
103
|
+
throw new RateLimiterInvalidResultError(fieldName);
|
|
104
|
+
}
|
|
105
|
+
function extractRows(rawResult) {
|
|
106
|
+
if (isRecord(rawResult)) {
|
|
107
|
+
const rows = rawResult.rows;
|
|
108
|
+
if (Array.isArray(rows)) {
|
|
109
|
+
return rows;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (Array.isArray(rawResult)) {
|
|
113
|
+
if (rawResult.length === 0 || !Array.isArray(rawResult[0])) {
|
|
114
|
+
return rawResult;
|
|
115
|
+
}
|
|
116
|
+
const [rows] = rawResult;
|
|
117
|
+
if (Array.isArray(rows)) {
|
|
118
|
+
return rows;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
export class KnexRateLimiter {
|
|
124
|
+
em;
|
|
125
|
+
constructor(em) {
|
|
126
|
+
this.em = em;
|
|
127
|
+
}
|
|
128
|
+
async take(input) {
|
|
129
|
+
const key = requireNonEmptyString('key', input.key);
|
|
130
|
+
const capacity = requirePositiveBigInt('capacity', input.capacity);
|
|
131
|
+
const refillTokens = input.refillTokens == null
|
|
132
|
+
? capacity
|
|
133
|
+
: requirePositiveBigInt('refillTokens', input.refillTokens);
|
|
134
|
+
const refillIntervalMs = durationToPositiveMillisecondBigInt('refillInterval', input.refillInterval);
|
|
135
|
+
const tokens = input.tokens == null ? RATE_LIMITER_DEFAULT_TOKENS : requirePositiveBigInt('tokens', input.tokens);
|
|
136
|
+
const rows = await this.queryRows('select * from rate_limiter.take_tokens(?, ?::bigint, ?::bigint, ?::bigint, ?::bigint)', [
|
|
137
|
+
key,
|
|
138
|
+
toPgBigInt(capacity),
|
|
139
|
+
toPgBigInt(refillTokens),
|
|
140
|
+
toPgBigInt(refillIntervalMs),
|
|
141
|
+
toPgBigInt(tokens),
|
|
142
|
+
], 'take');
|
|
143
|
+
const row = rows[0];
|
|
144
|
+
if (!isRecord(row)) {
|
|
145
|
+
throw new RateLimiterInvalidResultError('row');
|
|
146
|
+
}
|
|
147
|
+
const allowed = parseBooleanField('allowed', readField(row, ['allowed']));
|
|
148
|
+
const remaining = parseBigIntField('remaining', readField(row, ['remaining']));
|
|
149
|
+
const retryAfterMs = parseBigIntField('retry_after_ms', readField(row, ['retry_after_ms', 'retryAfterMs']));
|
|
150
|
+
const limit = parseBigIntField('rate_limit', readField(row, ['rate_limit', 'rateLimit', 'limit']));
|
|
151
|
+
return {
|
|
152
|
+
allowed,
|
|
153
|
+
remaining,
|
|
154
|
+
limit,
|
|
155
|
+
retryAfter: toDurationFromMilliseconds(retryAfterMs),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
async reset(key) {
|
|
159
|
+
const normalizedKey = requireNonEmptyString('key', key);
|
|
160
|
+
await this.queryRows('select rate_limiter.reset_bucket(?) as reset_bucket', [normalizedKey], 'reset');
|
|
161
|
+
}
|
|
162
|
+
async queryRows(sql, params, operation) {
|
|
163
|
+
try {
|
|
164
|
+
const rawResult = await this.em.execute(sql, params, 'all');
|
|
165
|
+
return extractRows(rawResult);
|
|
166
|
+
}
|
|
167
|
+
catch (cause) {
|
|
168
|
+
throw new RateLimiterQueryError(operation, cause);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=rate-limiter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limiter.js","sourceRoot":"","sources":["../src/rate-limiter.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thangnv-dev/rate-limiter-mikroorm-node",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc -p tsconfig.build.json",
|
|
22
|
+
"lint": "eslint .",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"test": "vitest run"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@thangnv-dev/error-common": "workspace:^",
|
|
28
|
+
"@thangnv-dev/rate-limiter-node": "workspace:^"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@mikro-orm/core": "^6.6.7",
|
|
32
|
+
"@mikro-orm/knex": "^6.6.7",
|
|
33
|
+
"@mikro-orm/migrations": "^6.6.7",
|
|
34
|
+
"@mikro-orm/postgresql": "^6.6.7",
|
|
35
|
+
"@thangnv-dev/mikroorm-nest": "workspace:^",
|
|
36
|
+
"@typescript-eslint/eslint-plugin": "^8.56.0",
|
|
37
|
+
"@typescript-eslint/parser": "^8.56.0",
|
|
38
|
+
"eslint": "^10.0.0",
|
|
39
|
+
"pg": "^8.18.0",
|
|
40
|
+
"temporal-polyfill": "^0.3.0",
|
|
41
|
+
"typescript": "^5.9.3",
|
|
42
|
+
"vitest": "^4.0.18"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@mikro-orm/knex": "^6.6.6",
|
|
46
|
+
"@mikro-orm/migrations": "^6.6.6",
|
|
47
|
+
"temporal-polyfill": "*"
|
|
48
|
+
}
|
|
49
|
+
}
|