UNPKG

@developers-joyride/shortify

Version:

High performance URL shortener library with multi-database support (MongoDB, SQLite, PostgreSQL, MySQL)

180 lines (155 loc) 5.66 kB
import mysql from "mysql2/promise"; import { EventEmitter } from "events"; import { IDatabaseAdapter, IUrl, MysqlConfig, } from "../interfaces/database.interface"; export class MysqlAdapter extends EventEmitter implements IDatabaseAdapter { private pool: mysql.Pool | null = null; private config: MysqlConfig; private tableName: string; private isConnectedFlag: boolean = false; constructor(config: MysqlConfig) { super(); this.config = { maxRetries: 5, retryDelay: 5000, ...config, }; this.tableName = config.tableName || "urls"; } async connect(): Promise<boolean> { try { if (this.isConnectedFlag && this.pool) { return true; } this.pool = mysql.createPool({ host: this.config.host, port: this.config.port, user: this.config.username, password: this.config.password, database: this.config.database, waitForConnections: true, connectionLimit: 10, queueLimit: 0, }); await this.createTables(); this.isConnectedFlag = true; this.emit("connected"); return true; } catch (error) { this.emit("error", error); return false; } } async createTables(): Promise<void> { if (!this.pool) throw new Error("Database not connected"); // Quote table name to preserve case const quotedTableName = `\`${this.tableName}\``; const createTableSQL = ` CREATE TABLE IF NOT EXISTS ${quotedTableName} ( id INT AUTO_INCREMENT PRIMARY KEY, urlId VARCHAR(255) UNIQUE NOT NULL, originalUrl TEXT NOT NULL, shortUrl TEXT NOT NULL, clicks INT DEFAULT 0, createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, expiresAt DATETIME NULL ); `; // MySQL does not support CREATE INDEX IF NOT EXISTS, so we need to check manually const createIndex1 = `CREATE INDEX idx_${this.tableName}_urlId ON ${quotedTableName}(urlId)`; const createIndex2 = `CREATE INDEX idx_${this.tableName}_originalUrl ON ${quotedTableName}(originalUrl)`; await this.pool.query(createTableSQL); const [rows] = await this.pool.query( `SHOW INDEX FROM ${quotedTableName} WHERE Key_name = "idx_${this.tableName}_urlId"` ); if ((rows as any[]).length === 0) { await this.pool.query(createIndex1); } const [rows2] = await this.pool.query( `SHOW INDEX FROM ${quotedTableName} WHERE Key_name = "idx_${this.tableName}_originalUrl"` ); if ((rows2 as any[]).length === 0) { await this.pool.query(createIndex2); } } async disconnect(): Promise<void> { if (this.pool) { await this.pool.end(); this.pool = null; this.isConnectedFlag = false; this.emit("disconnected"); } } isConnected(): boolean { return this.isConnectedFlag && this.pool !== null; } async createUrl(url: Omit<IUrl, "createdAt">): Promise<IUrl> { if (!this.pool) throw new Error("Database not connected"); // Quote table name to preserve case const quotedTableName = `\`${this.tableName}\``; const sql = `INSERT INTO ${quotedTableName} (urlId, originalUrl, shortUrl, clicks, expiresAt) VALUES (?, ?, ?, ?, ?)`; const [result] = await this.pool.query(sql, [ url.urlId, url.originalUrl, url.shortUrl, url.clicks, url.expiresAt ? url.expiresAt.toISOString().slice(0, 19).replace("T", " ") : null, ]); // MySQL does not return the row, so fetch it return (await this.findUrlByUrlId(url.urlId))!; } async findUrlByUrlId(urlId: string): Promise<IUrl | null> { if (!this.pool) throw new Error("Database not connected"); // Quote table name to preserve case const quotedTableName = `\`${this.tableName}\``; const sql = `SELECT * FROM ${quotedTableName} WHERE urlId = ? LIMIT 1`; const [rows] = await this.pool.query(sql, [urlId]); const row = (rows as any[])[0]; if (!row) return null; return { urlId: row.urlId, originalUrl: row.originalUrl, shortUrl: row.shortUrl, clicks: row.clicks, createdAt: row.createdAt, expiresAt: row.expiresAt ? new Date(row.expiresAt) : undefined, }; } async findUrlByOriginalUrl(originalUrl: string): Promise<IUrl | null> { if (!this.pool) throw new Error("Database not connected"); // Quote table name to preserve case const quotedTableName = `\`${this.tableName}\``; const sql = `SELECT * FROM ${quotedTableName} WHERE originalUrl = ? LIMIT 1`; const [rows] = await this.pool.query(sql, [originalUrl]); const row = (rows as any[])[0]; if (!row) return null; return { urlId: row.urlId, originalUrl: row.originalUrl, shortUrl: row.shortUrl, clicks: row.clicks, createdAt: row.createdAt, expiresAt: row.expiresAt ? new Date(row.expiresAt) : undefined, }; } async updateUrlClicks(urlId: string, clicks: number): Promise<void> { if (!this.pool) throw new Error("Database not connected"); // Quote table name to preserve case const quotedTableName = `\`${this.tableName}\``; const sql = `UPDATE ${quotedTableName} SET clicks = ? WHERE urlId = ?`; await this.pool.query(sql, [clicks, urlId]); } async deleteUrl(urlId: string): Promise<boolean> { if (!this.pool) throw new Error("Database not connected"); // Quote table name to preserve case const quotedTableName = `\`${this.tableName}\``; const sql = `DELETE FROM ${quotedTableName} WHERE urlId = ?`; const [result]: any = await this.pool.query(sql, [urlId]); return result.affectedRows > 0; } }