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