@developers-joyride/shortify
Version:
High performance URL shortener library with multi-database support (MongoDB, SQLite, PostgreSQL, MySQL)
180 lines (155 loc) • 5.66 kB
text/typescript
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;
}
}