@daiso-tech/core
Version:
The library offers flexible, framework-agnostic solutions for modern web applications, built on adaptable components that integrate seamlessly with popular frameworks like Next Js.
186 lines • 6.23 kB
JavaScript
/**
* @module RateLimiter
*/
import { MysqlAdapter } from "kysely";
import {} from "../../../../rate-limiter/contracts/_module.js";
import {} from "../../../../serde/contracts/_module.js";
import {} from "../../../../time-span/contracts/_module.js";
import { TimeSpan } from "../../../../time-span/implementations/_module.js";
import {} from "../../../../utilities/_module.js";
/**
* @internal
*/
async function find(kysely, serde, key) {
const row = await kysely
.selectFrom("rateLimiter")
.select(["rateLimiter.state", "rateLimiter.expiration"])
.where("rateLimiter.key", "=", key)
.executeTakeFirst();
if (row === undefined) {
return null;
}
return {
state: serde.deserialize(row.state),
expiration: new Date(Number(row.expiration)),
};
}
/**
* @internal
*/
class KyselyRateLimiterStorageAdapterTransaction {
kysely;
serde;
isMysql;
constructor(kysely, serde) {
this.kysely = kysely;
this.serde = serde;
this.isMysql =
this.kysely.getExecutor().adapter instanceof MysqlAdapter;
}
async upsert(key, state, expiration) {
const expirationAsMs = expiration.getTime();
const serializedState = this.serde.serialize(state);
await this.kysely
.insertInto("rateLimiter")
.values({
key,
state: serializedState,
expiration: expirationAsMs,
})
.$if(!this.isMysql, (eb) => eb.onConflict((eb) => eb.column("key").doUpdateSet({
key,
state: serializedState,
expiration: expirationAsMs,
})))
.$if(this.isMysql, (eb) => eb.onDuplicateKeyUpdate({
key,
state: serializedState,
expiration: expirationAsMs,
}))
.execute();
}
async find(key) {
return await find(this.kysely, this.serde, key);
}
}
/**
* IMPORT_PATH: `"@daiso-tech/core/rate-limiter/kysely-rate-limiter-storage-adapter"`
* @group Adapters
*/
export class KyselyRateLimiterStorageAdapter {
kysely;
serde;
expiredKeysRemovalInterval;
shouldRemoveExpiredKeys;
intervalId = null;
/**
* @example
* ```ts
* import { KyselyRateLimiterStorageAdapter } from "@daiso-tech/core/rate-limiter/kysely-rate-limiter-storage-adapter";
* import { Serde } from "@daiso-tech/core/serde";
* import { SuperJsonSerdeAdapter } from "@daiso-tech/core/serde/super-json-serde-adapter"
* import Sqlite from "better-sqlite3";
* import { Kysely, SqliteDialect } from "kysely";
*
* const serde = new Serde(new SuperJsonSerdeAdapter());
* const rateLimiterStorageAdapter = new KyselyRateLimiterStorageAdapter({
* kysely: new Kysely({
* dialect: new SqliteDialect({
* database: new Sqlite("local.db"),
* }),
* }),
* serde
* });
* // You need initialize the adapter once before using it.
* await rateLimiterStorageAdapter.init();
* ```
*/
constructor(settings) {
const { kysely, serde, expiredKeysRemovalInterval = TimeSpan.fromMinutes(1), shouldRemoveExpiredKeys = true, } = settings;
this.expiredKeysRemovalInterval = TimeSpan.fromTimeSpan(expiredKeysRemovalInterval);
this.shouldRemoveExpiredKeys = shouldRemoveExpiredKeys;
this.kysely = kysely;
this.serde = serde;
}
/**
* Removes all related rate limiter tables and their rows.
* Note all rate limiter data will be removed.
*/
async deInit() {
if (this.shouldRemoveExpiredKeys && this.intervalId !== null) {
clearInterval(this.intervalId);
}
// Should throw if the index does not exists thats why the try catch is used.
try {
await this.kysely.schema
.dropIndex("rateLimiter_expiration")
.on("rateLimiter")
.execute();
}
catch {
/* EMPTY */
}
// Should throw if the table does not exists thats why the try catch is used.
try {
await this.kysely.schema.dropTable("rateLimiter").execute();
}
catch {
/* EMPTY */
}
}
/**
* Creates all related tables and indexes.
* Note the `init` method needs to be called once before using the adapter.
*/
async init() {
// Should throw if the table already exists thats why the try catch is used.
try {
await this.kysely.schema
.createTable("rateLimiter")
.addColumn("key", "varchar(255)", (col) => col.primaryKey().notNull())
.addColumn("state", "varchar(255)", (col) => col.notNull())
.addColumn("expiration", "bigint")
.execute();
}
catch {
/* EMPTY */
}
// Should throw if the index already exists thats why the try catch is used.
try {
await this.kysely.schema
.createIndex("rateLimiter_expiration")
.on("rateLimiter")
.column("expiration")
.execute();
}
catch {
/* EMPTY */
}
if (this.shouldRemoveExpiredKeys) {
this.intervalId = setInterval(() => {
void this.removeAllExpired();
}, this.expiredKeysRemovalInterval.toMilliseconds());
}
}
async removeAllExpired() {
await this.kysely
.deleteFrom("rateLimiter")
.where("rateLimiter.expiration", "<=", Date.now())
.execute();
}
async transaction(fn) {
return await this.kysely.transaction().execute(async (trx) => {
return await fn(new KyselyRateLimiterStorageAdapterTransaction(trx, this.serde));
});
}
async find(key) {
return await find(this.kysely, this.serde, key);
}
async remove(key) {
await this.kysely
.deleteFrom("rateLimiter")
.where("rateLimiter.key", "=", key)
.executeTakeFirst();
}
}
//# sourceMappingURL=kysely-rate-limiter-storage-adapter.js.map