UNPKG

@unitio-code/url-shortener

Version:
168 lines (167 loc) 4.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.defaultStorage = exports.DEFAULT_SHORT_URL_OPTIONS = exports.RedisStorage = exports.MemoryStorage = void 0; exports.isValidUrl = isValidUrl; exports.encodeId = encodeId; exports.decodeShortUrl = decodeShortUrl; exports.storeUrlMapping = storeUrlMapping; exports.getOriginalUrl = getOriginalUrl; exports.hasUrlMapping = hasUrlMapping; exports.buildShortUrl = buildShortUrl; const CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; const BASE = CHARACTERS.length; /** * In-memory storage implementation */ class MemoryStorage { constructor() { this.storage = new Map(); } async get(id) { return this.storage.get(id); } async set(id, url, ttl) { this.storage.set(id, url); if (ttl) { setTimeout(() => this.storage.delete(id), ttl); } } async has(id) { return this.storage.has(id); } } exports.MemoryStorage = MemoryStorage; /** * Redis storage implementation (optional) */ class RedisStorage { constructor(client) { this.client = client; } async get(id) { const result = await this.client.get(`url:${id}`); return result || undefined; } async set(id, url, ttl) { const key = `url:${id}`; if (ttl) { await this.client.setex(key, ttl, url); } else { await this.client.set(key, url); } } async has(id) { const exists = await this.client.exists(`url:${id}`); return exists === 1; } } exports.RedisStorage = RedisStorage; /** * Validates if a string is a proper URL */ function isValidUrl(url) { try { new URL(url); return true; } catch (_a) { return false; } } /** * Generates a short URL code from a numeric ID using base62 encoding * @param id - The numeric ID to encode * @returns The base62 encoded string */ function encodeId(id) { let shortUrl = ""; let num = id; while (num > 0) { shortUrl = CHARACTERS[num % BASE] + shortUrl; num = Math.floor(num / BASE); } return shortUrl || CHARACTERS[0]; } /** * Decodes a short URL code back to its numeric ID * @param shortCode - The base62 encoded string * @returns The decoded numeric ID */ function decodeShortUrl(shortCode) { let id = 0; for (let i = 0; i < shortCode.length; i++) { const char = shortCode[i]; const charIndex = CHARACTERS.indexOf(char); if (charIndex === -1) { throw new Error(`Invalid character in short URL: ${char}`); } id = id * BASE + charIndex; } return id; } /** * Default options for building a short URL */ exports.DEFAULT_SHORT_URL_OPTIONS = { domain: "short.url", includeRedirectPath: true, redirectPathSegment: "r", includeProtocol: false, protocol: "https", pathSeparator: "/", }; /** * Default storage instance (in-memory) */ exports.defaultStorage = new MemoryStorage(); /** * Stores a URL mapping using the specified storage * @param id - The numeric ID * @param originalUrl - The original URL * @param storage - Storage instance to use * @param ttl - Time to live in seconds (optional) */ async function storeUrlMapping(id, originalUrl, storage = exports.defaultStorage, ttl) { await storage.set(id, originalUrl, ttl); } /** * Retrieves the original URL for a given ID * @param id - The numeric ID * @param storage - Storage instance to use * @returns The original URL if found, undefined otherwise */ async function getOriginalUrl(id, storage = exports.defaultStorage) { return await storage.get(id); } /** * Checks if a URL mapping exists * @param id - The numeric ID * @param storage - Storage instance to use * @returns True if the mapping exists, false otherwise */ async function hasUrlMapping(id, storage = exports.defaultStorage) { return await storage.has(id); } /** * Builds a complete short URL with domain and customizable options * @param id - The numeric ID to encode * @param options - Configuration options for the short URL * @returns The complete short URL */ function buildShortUrl(id, options) { const opts = typeof options === "string" ? { ...exports.DEFAULT_SHORT_URL_OPTIONS, domain: options } : { ...exports.DEFAULT_SHORT_URL_OPTIONS, ...options }; const shortCode = encodeId(id); let url = ""; if (opts.includeProtocol && opts.protocol) { url += `${opts.protocol}://`; } url += opts.domain; if (opts.includeRedirectPath && opts.redirectPathSegment) { url += `${opts.pathSeparator}${opts.redirectPathSegment}`; } url += `${opts.pathSeparator}${shortCode}`; return url; }