UNPKG

@developers-joyride/shortify

Version:

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

276 lines (240 loc) 7.7 kB
import sqlite3 from "sqlite3"; import { EventEmitter } from "events"; import { IDatabaseAdapter, IUrl, SqliteConfig, } from "../interfaces/database.interface"; export class SqliteAdapter extends EventEmitter implements IDatabaseAdapter { private db: sqlite3.Database | null = null; private config: SqliteConfig; private tableName: string; private retryCount: number = 0; private _isConnected: boolean = false; constructor(config: SqliteConfig) { super(); this.config = { maxRetries: 5, retryDelay: 5000, ...config, }; this.tableName = config.tableName || "urls"; } async connect(): Promise<boolean> { try { if (this._isConnected && this.db) { return true; } return new Promise((resolve, reject) => { this.db = new sqlite3.Database( this.config.database, (err: Error | null) => { if (err) { console.error("SQLite connection error:", err); this.emit("error", err); reject(err); return; } this._isConnected = true; this.retryCount = 0; this.emit("connected"); // Create tables if they don't exist this.createTables() .then(() => resolve(true)) .catch(reject); } ); // Handle connection events this.db!.on("error", (err: Error) => { console.error("SQLite database error:", err); this._isConnected = false; this.emit("error", err); this.retryConnection(); }); }); } catch (error) { console.error("Failed to connect to SQLite:", error); this.emit("error", error); return this.retryConnection(); } } private async createTables(): Promise<void> { if (!this.db) 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 INTEGER PRIMARY KEY AUTOINCREMENT, urlId TEXT UNIQUE NOT NULL, originalUrl TEXT NOT NULL, shortUrl TEXT NOT NULL, clicks INTEGER DEFAULT 0, createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, expiresAt DATETIME NULL ) `; return new Promise((resolve, reject) => { this.db!.run(createTableSQL, (err: Error | null) => { if (err) { reject(err); } else { // Create indexes for performance this.db!.run( `CREATE INDEX IF NOT EXISTS idx_${this.tableName}_urlId ON ${quotedTableName}(urlId)`, (err: Error | null) => { if (err) console.warn("Failed to create urlId index:", err); } ); this.db!.run( `CREATE INDEX IF NOT EXISTS idx_${this.tableName}_originalUrl ON ${quotedTableName}(originalUrl)`, (err: Error | null) => { if (err) console.warn("Failed to create originalUrl index:", err); } ); resolve(); } }); }); } private retryConnection(): boolean { if (this.retryCount < this.config.maxRetries!) { this.retryCount++; const delay = this.config.retryDelay! * Math.pow(2, this.retryCount - 1); console.log( `Retrying SQLite connection in ${delay}ms (attempt ${this.retryCount}/${this.config.maxRetries})` ); setTimeout(() => { this.connect(); }, delay); return true; } else { this.emit("maxRetriesReached"); return false; } } async disconnect(): Promise<void> { if (this.db) { return new Promise((resolve) => { this.db!.close((err: Error | null) => { if (err) console.error("Error closing SQLite connection:", err); this.db = null; this._isConnected = false; this.emit("disconnected"); resolve(); }); }); } } isConnected(): boolean { return this._isConnected && this.db !== null; } async createUrl(url: Omit<IUrl, "createdAt">): Promise<IUrl> { if (!this.db) 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 (?, ?, ?, ?, ?) `; return new Promise((resolve, reject) => { this.db!.run( sql, [ url.urlId, url.originalUrl, url.shortUrl, url.clicks, url.expiresAt ? url.expiresAt.toISOString() : null, ], function (err) { if (err) { reject(err); } else { resolve({ ...url, createdAt: new Date(), }); } } ); }); } async findUrlByUrlId(urlId: string): Promise<IUrl | null> { if (!this.db) throw new Error("Database not connected"); // Quote table name to preserve case const quotedTableName = `"${this.tableName}"`; const sql = `SELECT * FROM ${quotedTableName} WHERE urlId = ?`; return new Promise((resolve, reject) => { this.db!.get(sql, [urlId], (err: Error | null, row: any) => { if (err) { reject(err); } else if (!row) { resolve(null); } else { resolve({ urlId: row.urlId, originalUrl: row.originalUrl, shortUrl: row.shortUrl, clicks: row.clicks, createdAt: new Date(row.createdAt), expiresAt: row.expiresAt ? new Date(row.expiresAt) : undefined, }); } }); }); } async findUrlByOriginalUrl(originalUrl: string): Promise<IUrl | null> { if (!this.db) throw new Error("Database not connected"); // Quote table name to preserve case const quotedTableName = `"${this.tableName}"`; const sql = `SELECT * FROM ${quotedTableName} WHERE originalUrl = ?`; return new Promise((resolve, reject) => { this.db!.get(sql, [originalUrl], (err: Error | null, row: any) => { if (err) { reject(err); } else if (!row) { resolve(null); } else { resolve({ urlId: row.urlId, originalUrl: row.originalUrl, shortUrl: row.shortUrl, clicks: row.clicks, createdAt: new Date(row.createdAt), expiresAt: row.expiresAt ? new Date(row.expiresAt) : undefined, }); } }); }); } async updateUrlClicks(urlId: string, clicks: number): Promise<void> { if (!this.db) throw new Error("Database not connected"); // Quote table name to preserve case const quotedTableName = `"${this.tableName}"`; const sql = `UPDATE ${quotedTableName} SET clicks = ? WHERE urlId = ?`; return new Promise((resolve, reject) => { this.db!.run(sql, [clicks, urlId], (err) => { if (err) { reject(err); } else { resolve(); } }); }); } async deleteUrl(urlId: string): Promise<boolean> { if (!this.db) throw new Error("Database not connected"); // Quote table name to preserve case const quotedTableName = `"${this.tableName}"`; const sql = `DELETE FROM ${quotedTableName} WHERE urlId = ?`; return new Promise((resolve, reject) => { this.db!.run(sql, [urlId], function (err) { if (err) { reject(err); } else { resolve(this.changes > 0); } }); }); } }