@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.
288 lines • 9.75 kB
JavaScript
/**
* @module Cache
*/
import { MysqlAdapter, Transaction } from "kysely";
import {} from "../../../../cache/contracts/_module.js";
import {} from "../../../../execution-context/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
*/
export class DatabaseCacheTransaction {
kysely;
serde;
isMysql;
constructor(kysely, serde) {
this.kysely = kysely;
this.serde = serde;
this.isMysql =
this.kysely.getExecutor().adapter instanceof MysqlAdapter;
}
async find(_context, key) {
const cacheData = await this.kysely
.selectFrom("cache")
.where("cache.key", "=", key)
.select(["cache.expiration", "cache.value"])
.executeTakeFirst();
if (cacheData === undefined) {
return null;
}
return {
value: this.serde.deserialize(cacheData.value),
expiration: cacheData.expiration === null
? null
: new Date(Number(cacheData.expiration)),
};
}
async upsert(_context, key, value, expiration) {
let expirationAsMs;
if (expiration instanceof Date) {
expirationAsMs = expiration.getTime();
}
else {
expirationAsMs = expiration;
}
const serializedValue = this.serde.serialize(value);
await this.kysely
.insertInto("cache")
.values({
key,
value: serializedValue,
expiration: expirationAsMs,
})
.$if(!this.isMysql, (eb) => eb.onConflict((eb_) => eb_.column("key").doUpdateSet({
key,
value: serializedValue,
expiration: expirationAsMs,
})))
.$if(this.isMysql, (eb) => eb.onDuplicateKeyUpdate({
key,
value: serializedValue,
expiration: expirationAsMs,
}))
.execute();
}
}
/**
* To utilize the `KyselyCacheAdapter`, you must install the [`"kysely"`](https://www.npmjs.com/package/kysely) package and configure a `Kysely` class instance.
* The adapter have been tested with `sqlite`, `postgres` and `mysql` databases.
*
* IMPORT_PATH: `"@daiso-tech/core/cache/kysely-cache-adapter"`
* @group Adapters
*/
export class KyselyCacheAdapter {
isMysql;
serde;
kysely;
shouldRemoveExpiredKeys;
expiredKeysRemovalInterval;
timeoutId = null;
enableTransactions;
/**
* @example
* ```ts
* import { KyselyCacheAdapter } from "@daiso-tech/core/cache/kysely-cache-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 cacheAdapter = new KyselyCacheAdapter({
* kysely: new Kysely({
* dialect: new SqliteDialect({
* database: new Sqlite("local.db"),
* }),
* }),
* serde,
* });
* // You need initialize the adapter once before using it.
* await cacheAdapter.init();
* ```
*/
constructor(settings) {
const { kysely, serde, expiredKeysRemovalInterval = TimeSpan.fromMinutes(1), shouldRemoveExpiredKeys = true, enableTransactions = !(settings.kysely instanceof Transaction), } = settings;
this.enableTransactions = enableTransactions;
this.kysely = kysely;
this.serde = serde;
this.expiredKeysRemovalInterval = TimeSpan.fromTimeSpan(expiredKeysRemovalInterval);
this.shouldRemoveExpiredKeys = shouldRemoveExpiredKeys;
this.isMysql =
this.kysely.getExecutor().adapter instanceof MysqlAdapter;
}
async removeAllExpired() {
await this.kysely
.deleteFrom("cache")
.where("cache.expiration", "<=", Date.now())
.execute();
}
async init() {
// Should throw if the table already exists thats why the try catch is used.
try {
await this.kysely.schema
.createTable("cache")
.addColumn("key", "varchar(255)", (col) => col.primaryKey())
.addColumn("value", "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("cache_expiration")
.on("cache")
.columns(["expiration"])
.execute();
}
catch {
/* EMPTY */
}
if (this.shouldRemoveExpiredKeys && this.timeoutId === null) {
this.timeoutId = setInterval(() => {
void this.removeAllExpired();
}, this.expiredKeysRemovalInterval.toMilliseconds());
}
}
/**
* Removes all related cache tables and their rows.
* Note all cache data will be removed.
*/
async deInit() {
if (this.shouldRemoveExpiredKeys && this.timeoutId !== null) {
clearInterval(this.timeoutId);
}
// Should throw if the index does not exists thats why the try catch is used.
try {
await this.kysely.schema
.dropIndex("cache_expiration")
.on("cache")
.execute();
}
catch {
/* EMPTY */
}
// Should throw if the table does not exists thats why the try catch is used.
try {
await this.kysely.schema.dropTable("cache").execute();
}
catch {
/* EMPTY */
}
}
async find(_context, key) {
const cacheData = await this.kysely
.selectFrom("cache")
.where("cache.key", "=", key)
.select(["cache.expiration", "cache.value"])
.executeTakeFirst();
if (cacheData === undefined) {
return null;
}
return {
value: this.serde.deserialize(cacheData.value),
expiration: cacheData.expiration === null
? null
: new Date(Number(cacheData.expiration)),
};
}
_transaction(_context, trxFn) {
if (this.enableTransactions) {
return this.kysely.transaction().execute(async (trx) => {
return await trxFn(trx);
});
}
return trxFn(this.kysely);
}
async transaction(_context, trxFn) {
return await this._transaction(_context, (trx) => trxFn(new DatabaseCacheTransaction(trx, this.serde)));
}
async update(context, key, value) {
let row;
const serializedValue = this.serde.serialize(value);
if (this.isMysql) {
row = await this._transaction(context, async (trx) => {
const rows = await trx
.selectFrom("cache")
.where("cache.key", "=", key)
.select("cache.expiration")
.executeTakeFirst();
await trx
.updateTable("cache")
.where("cache.key", "=", key)
.set({
value: serializedValue,
})
.execute();
return rows;
});
}
else {
row = await this.kysely
.updateTable("cache")
.where("cache.key", "=", key)
.set({
value: serializedValue,
})
.returning("cache.expiration")
.executeTakeFirst();
}
if (row === undefined) {
return null;
}
return {
expiration: row.expiration === null
? null
: new Date(Number(row.expiration)),
};
}
async removeMany(context, keys) {
let rows;
if (keys.length === 0) {
return [];
}
if (this.isMysql) {
rows = await this._transaction(context, async (trx) => {
const rows_ = await trx
.selectFrom("cache")
.where("cache.key", "in", keys)
.select("cache.expiration")
.execute();
await trx
.deleteFrom("cache")
.where("cache.key", "in", keys)
.execute();
return rows_;
});
}
else {
rows = await this.kysely
.deleteFrom("cache")
.where("cache.key", "in", keys)
.returning("cache.expiration")
.execute();
}
return rows.map((row) => {
return {
expiration: row.expiration === null
? null
: new Date(Number(row.expiration)),
};
});
}
async removeAll(_context) {
await this.kysely.deleteFrom("cache").execute();
}
async removeByKeyPrefix(_context, prefix) {
await this.kysely
.deleteFrom("cache")
.where("cache.key", "like", `${prefix}%`)
.execute();
}
}
//# sourceMappingURL=kysely-cache-adapter.js.map