UNPKG

@developers-joyride/shortify

Version:

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

245 lines (244 loc) 8.82 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SqliteAdapter = void 0; const sqlite3_1 = __importDefault(require("sqlite3")); const events_1 = require("events"); class SqliteAdapter extends events_1.EventEmitter { constructor(config) { super(); this.db = null; this.retryCount = 0; this._isConnected = false; this.config = { maxRetries: 5, retryDelay: 5000, ...config, }; this.tableName = config.tableName || "urls"; } async connect() { try { if (this._isConnected && this.db) { return true; } return new Promise((resolve, reject) => { this.db = new sqlite3_1.default.Database(this.config.database, (err) => { 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) => { 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(); } } async createTables() { 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) => { 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) => { 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) => { if (err) console.warn("Failed to create originalUrl index:", err); }); resolve(); } }); }); } retryConnection() { 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() { if (this.db) { return new Promise((resolve) => { this.db.close((err) => { if (err) console.error("Error closing SQLite connection:", err); this.db = null; this._isConnected = false; this.emit("disconnected"); resolve(); }); }); } } isConnected() { return this._isConnected && this.db !== null; } async createUrl(url) { 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) { 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, row) => { 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) { 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, row) => { 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, clicks) { 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) { 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); } }); }); } } exports.SqliteAdapter = SqliteAdapter;