const moment = require("moment"); const db = require("../dbConfig"); const crypto = require("crypto"); const cron = require("node-cron"); /** * Checks if a given string is a valid URL format. * @param {string} url - The input string to validate. * @returns {boolean} - Returns true if valid URL format, otherwise false. */ function isValidUrl(url) { try { new URL(url); return true; } catch (e) { return false; } } /** * Generates a deterministic short code based on long URL, expiry date, and user ID. * Uses MD5 hashing, encodes the result in base64, and slices to desired length. * @param {string} longUrl - Original input URL. * @param {number} codeLength - Desired length of output short code. * @param {string} expiryDate - Expiry date used in code generation. * @param {string} user_id - User identifier to namespace the code. * @returns {string} - Base64-based truncated hash used as short code. */ const generateShortCode = ( longUrl, codeLength, expiryDate, userId, customizedShortnerSlug ) => { const dataToHash = longUrl + "_" + expiryDate + "_" + userId + "_" + customizedShortnerSlug; return crypto .createHash("md5") .update(dataToHash) .digest("base64") .substring(0, codeLength); }; /** * Creates and stores a new short code mapping for a given URL. * Validates input values and optionally supports user-scoped or custom slugs. * @param {object} params - Input parameters for short code creation. * @param {string} params.longUrl - Original full URL. * @param {number} params.expiryInDays - Days until expiry (0–30 allowed). * @param {number} params.codeLength - Short code length (8–30 allowed). * @param {string} [params.user_id=""] - Optional user ID for namespace isolation. * @param {string} [params.customized_shortner_slug=""] - Optional custom alias. * @returns {Promise} - Returns existing or newly created code/slug. * @throws {Error} - If input validation fails or database errors occur. */ const createShortUrl = async ({ longUrl = "", expiryInDays = 30, codeLength = 10, userId = "", customizedShortnerSlug = "", }) => { if (!String(longUrl).trim() || !isValidUrl(longUrl)) { throw new Error("Long url required."); } if (!Number.isInteger(codeLength)) { throw new Error("Code length must be valid integer value."); } else if (codeLength < 8 || codeLength > 30) { throw new Error("Code length must be between 8 to 30 characters."); } if (!Number.isInteger(expiryInDays)) { throw new Error("Expiry in days must be valid integer value."); } else if (expiryInDays < 0 || expiryInDays > 30) { throw new Error("Expiry in days must be between 0 to 30 days."); } const expiryDate = moment() .add(expiryInDays, "days") .endOf("day") .format("YYYY-MM-DD HH:mm:ss"); // Avoid duplicates: return existing code if it already exists const existing = await db.findByLongUrl( longUrl, expiryDate, userId, customizedShortnerSlug ); if (existing) return existing.customized_shortner_slug || existing.short_code; const shortCode = generateShortCode( longUrl, codeLength, expiryDate, userId, customizedShortnerSlug ); const res = await db.insertUrl( longUrl, shortCode, expiryDate, String(userId).trim(), String(customizedShortnerSlug).trim() ); return customizedShortnerSlug || shortCode; }; /** * Retrieves the original long URL using a short code or custom slug. * @param {string} shortCode - Code or slug used to look up the original URL. * @param {string} [userId=""] - Optional user ID for user-specific resolution. * @returns {Promise} - Returns the original URL if found and valid. * @throws {Error} - If lookup fails or URL is expired. */ const getOriginalUrl = async (shortCode, userId = "") => { try { const longUrl = await db.findByShortCode(shortCode, userId); return longUrl; } catch (error) { throw error; } }; /** * Starts a cron job that runs daily at midnight. * Cleans up expired short URL entries from the database. */ const startScheduler = () => { console.log("Starting expiration cleanup scheduler..."); cron.schedule("0 0 * * *", () => { console.log("[Scheduler] Running deletion job..."); db.deleteAllExpired().then(console.log).catch(console.error); }); }; module.exports = { createShortUrl, getOriginalUrl, startScheduler, getAllData: db.getAllData, getPaginatedData: db.getPaginatedData, };