@developers-joyride/shortify
Version:
High performance URL shortener library with multi-database support (MongoDB, SQLite, PostgreSQL, MySQL)
245 lines (244 loc) • 8.82 kB
JavaScript
;
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;