UNPKG

@developers-joyride/shortify

Version:

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

160 lines (135 loc) 3.9 kB
import { IUrl, IDatabaseAdapter } from "../interfaces/database.interface"; import { URL } from "url"; import { generateId } from "../utils/id-generator"; export interface ShortifyOptions { baseUrl: string; urlLength?: number; customUrlId?: string; expiresInDays?: number; } export interface ShortifyResult { originalUrl: string; shortUrl: string; urlId: string; expiresAt?: Date; } export class ShortenerService { private defaultUrlLength: number = 8; private baseUrl: string; private dbAdapter: IDatabaseAdapter; constructor(baseUrl: string, dbAdapter: IDatabaseAdapter) { // Ensure the base URL ends with a slash this.baseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`; this.dbAdapter = dbAdapter; } /** * Validate if a URL is properly formatted */ private isValidUrl(urlString: string): boolean { try { const url = new URL(urlString); return url.protocol === "http:" || url.protocol === "https:"; } catch (error) { return false; } } /** * Generate a short URL ID */ private generateUrlId(length: number = this.defaultUrlLength): string { return generateId(length); } /** * Check if a URL ID already exists in the database * For performance, we use this to avoid duplicate URL IDs */ private async findExistingUrlId(urlId: string): Promise<IUrl | null> { return this.dbAdapter.findUrlByUrlId(urlId); } /** * Shorten a URL */ async shorten( originalUrl: string, options: Partial<ShortifyOptions> = {} ): Promise<ShortifyResult> { // Validate the original URL if (!this.isValidUrl(originalUrl)) { throw new Error("Invalid URL format"); } // Apply options with defaults const urlLength = options.urlLength || this.defaultUrlLength; const baseUrl = options.baseUrl || this.baseUrl; // Generate URL ID (either custom or generated) let urlId = options.customUrlId || this.generateUrlId(urlLength); // Check if the URL ID already exists and generate a new one if needed let existingUrl = await this.findExistingUrlId(urlId); while (existingUrl) { urlId = this.generateUrlId(urlLength); existingUrl = await this.findExistingUrlId(urlId); } // Calculate expiration date if provided let expiresAt: Date | undefined = undefined; if (options.expiresInDays) { expiresAt = new Date(); expiresAt.setDate(expiresAt.getDate() + options.expiresInDays); } // Create the short URL const shortUrl = `${baseUrl}${urlId}`; // Save the URL to database await this.dbAdapter.createUrl({ urlId, originalUrl, shortUrl, clicks: 0, expiresAt, }); return { originalUrl, shortUrl, urlId, expiresAt, }; } /** * Resolve a short URL to its original URL */ async resolve(urlId: string): Promise<string | null> { const url = await this.dbAdapter.findUrlByUrlId(urlId); if (!url) { return null; } // Check if URL has expired if (url.expiresAt && url.expiresAt < new Date()) { // URL has expired, delete it await this.dbAdapter.deleteUrl(urlId); return null; } // Increment click count await this.dbAdapter.updateUrlClicks(urlId, url.clicks + 1); return url.originalUrl; } /** * Get URL stats */ async getUrlStats(urlId: string): Promise<Partial<IUrl> | null> { const url = await this.dbAdapter.findUrlByUrlId(urlId); if (!url) { return null; } return { urlId: url.urlId, originalUrl: url.originalUrl, shortUrl: url.shortUrl, clicks: url.clicks, createdAt: url.createdAt, expiresAt: url.expiresAt, }; } /** * Delete a shortened URL */ async deleteUrl(urlId: string): Promise<boolean> { return this.dbAdapter.deleteUrl(urlId); } }