@developers-joyride/shortify
Version:
High performance URL shortener library with multi-database support (MongoDB, SQLite, PostgreSQL, MySQL)
227 lines (188 loc) • 6.37 kB
text/typescript
import { Pool, PoolClient } from "pg";
import { EventEmitter } from "events";
import {
IDatabaseAdapter,
IUrl,
PostgresConfig,
} from "../interfaces/database.interface";
export class PostgresAdapter extends EventEmitter implements IDatabaseAdapter {
private pool: Pool | null = null;
private config: PostgresConfig;
private tableName: string;
private isConnectedFlag: boolean = false;
constructor(config: PostgresConfig) {
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 = new Pool({
host: this.config.host,
port: this.config.port,
database: this.config.database,
user: this.config.username,
password: this.config.password,
max: 20, // Maximum number of clients in the pool
idleTimeoutMillis: 30000, // Close idle clients after 30 seconds
connectionTimeoutMillis: 2000, // Return an error after 2 seconds if connection could not be established
});
// Test the connection
const client = await this.pool.connect();
client.release();
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 SERIAL PRIMARY KEY,
urlId VARCHAR(255) UNIQUE NOT NULL,
originalUrl TEXT NOT NULL,
shortUrl TEXT NOT NULL,
clicks INTEGER DEFAULT 0,
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expiresAt TIMESTAMP NULL
);
`;
const createIndexesSQL = `
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_urlId ON ${quotedTableName}(urlId);
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_originalUrl ON ${quotedTableName}(originalUrl);
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_createdAt ON ${quotedTableName}(createdAt);
`;
const client = await this.pool.connect();
try {
await client.query(createTableSQL);
await client.query(createIndexesSQL);
} finally {
client.release();
}
}
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 ($1, $2, $3, $4, $5)
RETURNING *
`;
const client = await this.pool.connect();
try {
const result = await client.query(sql, [
url.urlId,
url.originalUrl,
url.shortUrl,
url.clicks,
url.expiresAt ? url.expiresAt.toISOString() : null,
]);
const row = result.rows[0];
return {
urlId: row.urlid,
originalUrl: row.originalurl,
shortUrl: row.shorturl,
clicks: row.clicks,
createdAt: row.createdat,
expiresAt: row.expiresat ? new Date(row.expiresat) : undefined,
};
} finally {
client.release();
}
}
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 = $1 LIMIT 1`;
const client = await this.pool.connect();
try {
const result = await client.query(sql, [urlId]);
const row = result.rows[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,
};
} finally {
client.release();
}
}
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 = $1 LIMIT 1`;
const client = await this.pool.connect();
try {
const result = await client.query(sql, [originalUrl]);
const row = result.rows[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,
};
} finally {
client.release();
}
}
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 = $1 WHERE urlId = $2`;
const client = await this.pool.connect();
try {
await client.query(sql, [clicks, urlId]);
} finally {
client.release();
}
}
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 = $1`;
const client = await this.pool.connect();
try {
const result = await client.query(sql, [urlId]);
return (result.rowCount || 0) > 0;
} finally {
client.release();
}
}
}