145 lines
4.4 KiB
JavaScript
145 lines
4.4 KiB
JavaScript
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<string>} - 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<string>} - 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,
|
||
};
|