UNPKG

@keyv/mysql

Version:

MySQL/MariaDB storage adapter for Keyv

303 lines (299 loc) 10.2 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { KeyvMysql: () => KeyvMysql, default: () => index_default }); module.exports = __toCommonJS(index_exports); var import_node_events = __toESM(require("events"), 1); var import_mysql22 = __toESM(require("mysql2"), 1); // src/pool.ts var import_mysql2 = __toESM(require("mysql2"), 1); var mysqlPool; var globalUri; var parseConnectionString = (connectionString) => { connectionString = connectionString.replace(/#/g, "%23"); const url = new URL(connectionString); const poolOptions = { user: decodeURIComponent(url.username), password: decodeURIComponent(url.password) || void 0, host: url.hostname, port: url.port ? Number.parseInt(url.port, 10) : void 0, database: decodeURIComponent(url.pathname.slice(1)) // Remove the leading '/' }; for (const key of Object.keys(poolOptions)) { if (poolOptions[key] === void 0) { delete poolOptions[key]; } } return poolOptions; }; var pool = (uri, options = {}) => { if (globalUri !== uri) { mysqlPool = void 0; globalUri = uri; } const connectObject = parseConnectionString(uri); const poolOptions = { ...connectObject, ...options }; mysqlPool ??= import_mysql2.default.createPool(poolOptions); return mysqlPool.promise(); }; var endPool = () => { if (mysqlPool) { mysqlPool.end(); } globalUri = void 0; }; // src/index.ts var keyvMysqlKeys = /* @__PURE__ */ new Set([ "adapter", "compression", "connect", "dialect", "keySize", "table", "ttl", "uri" ]); var KeyvMysql = class extends import_node_events.default { /** * Indicates whether TTL (Time To Live) support is enabled. * Set to true when intervalExpiration is configured. */ ttlSupport = false; /** * Configuration options for the MySQL adapter. */ opts; /** * Optional namespace for key prefixing. */ namespace; /** * Query function for executing SQL statements against the MySQL database. */ query; /** * Creates a new KeyvMysql instance. * @param keyvOptions - Configuration options or connection URI string */ constructor(keyvOptions) { super(); let options = { dialect: "mysql", uri: "mysql://localhost" }; if (typeof keyvOptions === "string") { options.uri = keyvOptions; } else { options = { ...options, ...keyvOptions }; } if (options.intervalExpiration !== void 0 && options.intervalExpiration > 0) { this.ttlSupport = true; } const mysqlOptions = Object.fromEntries( Object.entries(options).filter(([k]) => !keyvMysqlKeys.has(k)) ); delete mysqlOptions.namespace; delete mysqlOptions.serialize; delete mysqlOptions.deserialize; const connection = async () => { const conn = pool(options.uri, mysqlOptions); return async (sql) => { const data = await conn.query(sql); return data[0]; }; }; this.opts = { table: "keyv", keySize: 255, ...options }; const createTable = `CREATE TABLE IF NOT EXISTS ${this.opts.table}(id VARCHAR(${Number(this.opts.keySize)}) PRIMARY KEY, value TEXT)`; const connected = connection().then(async (query) => { await query(createTable); if (this.opts.intervalExpiration !== void 0 && this.opts.intervalExpiration > 0) { await query("SET GLOBAL event_scheduler = ON;"); await query("DROP EVENT IF EXISTS keyv_delete_expired_keys;"); await query(`CREATE EVENT IF NOT EXISTS keyv_delete_expired_keys ON SCHEDULE EVERY ${this.opts.intervalExpiration} SECOND DO DELETE FROM ${this.opts.table} WHERE CAST(value->'$.expires' AS UNSIGNED) BETWEEN 1 AND UNIX_TIMESTAMP(NOW(3)) * 1000;`); } return query; }).catch((error) => this.emit("error", error)); this.query = async (sqlString) => { const query = await connected; return query(sqlString); }; } /** * Retrieves a value from the store by key. * @param key - The key to retrieve * @returns The stored value or undefined if not found */ async get(key) { const sql = `SELECT * FROM ${this.opts.table} WHERE id = ?`; const select = import_mysql22.default.format(sql, [key]); const rows = await this.query(select); const row = rows[0]; return row?.value; } /** * Retrieves multiple values from the store by their keys. * @param keys - Array of keys to retrieve * @returns Array of stored values in the same order as the input keys, with undefined for missing keys */ async getMany(keys) { const sql = `SELECT * FROM ${this.opts.table} WHERE id IN (?)`; const select = import_mysql22.default.format(sql, [keys]); const rows = await this.query(select); const results = []; for (const key of keys) { const rowIndex = rows.findIndex((row) => row.id === key); results.push( rowIndex === -1 ? void 0 : rows[rowIndex].value ); } return results; } /** * Sets a value in the store for the given key. * If the key already exists, it will be updated. * @param key - The key to set * @param value - The value to store * @returns Promise that resolves when the operation completes */ // biome-ignore lint/suspicious/noExplicitAny: type format async set(key, value) { const sql = `INSERT INTO ${this.opts.table} (id, value) VALUES(?, ?) ON DUPLICATE KEY UPDATE value=?;`; const insert = [key, value, value]; const upsert = import_mysql22.default.format(sql, insert); return this.query(upsert); } /** * Deletes a key-value pair from the store. * @param key - The key to delete * @returns True if the key existed and was deleted, false if the key did not exist */ async delete(key) { const sql = `SELECT * FROM ${this.opts.table} WHERE id = ?`; const select = import_mysql22.default.format(sql, [key]); const delSql = `DELETE FROM ${this.opts.table} WHERE id = ?`; const del = import_mysql22.default.format(delSql, [key]); const rows = await this.query(select); const row = rows[0]; if (row === void 0) { return false; } await this.query(del); return true; } /** * Deletes multiple key-value pairs from the store. * @param key - Array of keys to delete * @returns True if at least one key was deleted, false if no keys were found */ async deleteMany(key) { const sql = `DELETE FROM ${this.opts.table} WHERE id IN (?)`; const del = import_mysql22.default.format(sql, [key]); const result = await this.query(del); return result.affectedRows !== 0; } /** * Clears all entries from the store. * If a namespace is set, only entries within that namespace are cleared. * @returns Promise that resolves when the operation completes */ async clear() { const sql = `DELETE FROM ${this.opts.table} WHERE id LIKE ?`; const del = import_mysql22.default.format(sql, [ this.namespace ? `${this.namespace}:%` : "%" ]); await this.query(del); } /** * Returns an async iterator for iterating over all key-value pairs in the store. * @param namespace - Optional namespace to filter results * @yields Arrays containing [key, value] pairs */ async *iterator(namespace) { const limit = Number.parseInt(this.opts.iterationLimit, 10) || 10; async function* iterate(offset, options, query) { const sql = `SELECT * FROM ${options.table} WHERE id LIKE ? LIMIT ? OFFSET ?`; const select = import_mysql22.default.format(sql, [ // biome-ignore lint/style/useTemplate: need to fix `${namespace ? namespace + ":" : ""}%`, limit, offset ]); const entries = await query(select); if (entries.length === 0) { return; } for (const entry of entries) { offset += 1; yield [entry.id, entry.value]; } yield* iterate(offset, options, query); } yield* iterate(0, this.opts, this.query); } /** * Checks if a key exists in the store. * @param key - The key to check * @returns True if the key exists, false otherwise */ async has(key) { const sql = `SELECT EXISTS ( SELECT * FROM ${this.opts.table} WHERE id = ? )`; const exists = import_mysql22.default.format(sql, [key]); const rows = await this.query(exists); return Object.values(rows[0])[0] === 1; } /** * Disconnects from the MySQL database and closes the connection pool. * @returns Promise that resolves when disconnected */ async disconnect() { endPool(); } }; var index_default = KeyvMysql; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { KeyvMysql }); /* v8 ignore next -- @preserve */