UNPKG

@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
/** * @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