@the-node-forge/url-shortener
Version:
A URL shortener that generates and stores unique aliases for long URLs, with optional expiration and custom alias support.
130 lines (129 loc) • 4.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.URLShortener = void 0;
const parseExpiresIn_1 = require("../utils/parseExpiresIn");
/**
* URLShortener provides methods to shorten URLs and resolve aliases.
* All methods are asynchronous and include robust error handling.
*/
class URLShortener {
/**
* Create a new URLShortener instance.
* @param baseDomain - Domain used for returned shortened URLs (e.g., "https://sho.rt")
* @param store - Redis-backed implementation of the StoreAdapter interface
*/
constructor(baseDomain, store) {
this.baseDomain = baseDomain;
this.store = store;
}
/**
* Shortens a long URL into a shorter one with an alias.
* - Generates alias if not provided
* - Validates the URL
* - Checks for collisions
* - Supports optional expiration time and override flag
*
* @param longUrl - The full, original URL to shorten
* @param options - Optional config (alias, override, expiresIn)
* @returns A Promise that resolves to the full shortened URL string
*/
async shorten(longUrl, options) {
// Validate input URL
if (!this.isValidUrl(longUrl)) {
throw new Error('Invalid URL format.');
}
// If no alias provided, generate a random one
let alias = options?.alias ?? this.generateCode(URLShortener.DEFAULT_CODE_LENGTH);
// Check if alias already exists
if (await this.store.has(alias)) {
if (options?.alias && !options?.override) {
throw new Error('Alias is already in use.');
}
// If no alias was specified and there's a collision, generate a new one until unique
if (!options?.alias) {
do {
alias = this.generateCode(URLShortener.DEFAULT_CODE_LENGTH);
} while (await this.store.has(alias));
}
}
// Optional expiration timestamp
let expiresAt;
try {
expiresAt = options?.expiresIn
? Date.now() + (0, parseExpiresIn_1.parseExpiresIn)(options.expiresIn)
: undefined;
}
catch (error) {
console.error('Error parsing expiresIn:', error);
throw error;
}
// Store the shortened URL in Redis
try {
await this.store.set(alias, { url: longUrl, expiresAt }, options?.override ?? false);
}
catch (error) {
console.error('Error saving shortened URL:', error);
throw new Error('Could not save shortened URL.');
}
// Return full short URL
return `${this.baseDomain}/${alias}`;
}
/**
* Resolves a short alias back to its original long URL.
* - If alias doesn’t exist, returns null
* - If alias exists but expired, deletes it and returns null
*
* @param alias - The short code to resolve
* @returns The original long URL or null if not found/expired
*/
async resolve(alias) {
let entry;
try {
entry = await this.store.get(alias);
}
catch (error) {
console.error('Error retrieving URL:', error);
return null;
}
if (!entry)
return null;
// Handle expiration
if (entry.expiresAt && Date.now() > entry.expiresAt) {
try {
await this.store.delete(alias);
}
catch (error) {
console.error('Error deleting expired URL:', error);
}
return null;
}
return entry.url;
}
/**
* Helper method to validate a URL string using the built-in URL constructor.
*/
isValidUrl(url) {
try {
new URL(url);
return true;
}
catch {
return false;
}
}
/**
* Generates a random alphanumeric code of the specified length.
* Used for alias generation.
*/
generateCode(length) {
let code = '';
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * URLShortener.CHAR_SET.length);
code += URLShortener.CHAR_SET[randomIndex];
}
return code;
}
}
exports.URLShortener = URLShortener;
URLShortener.DEFAULT_CODE_LENGTH = 6;
URLShortener.CHAR_SET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';