link-shortner/dbConfig/index.js
2025-05-28 11:33:08 +05:30

286 lines
9.1 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Import necessary modules
const sqlite3 = require("sqlite3").verbose();
const { config } = require("dotenv");
const moment = require("moment");
const path = require("path");
const fs = require("fs");
// Load environment variables from .env file
config();
// Resolve the target directory path for the database folder
const targetPath = path.resolve(process.cwd(), "./database");
// Ensure the database directory exists; create it if it doesn't
if (!fs.existsSync(targetPath)) {
fs.mkdirSync(targetPath, { recursive: true });
console.log("✅ Created db folder at project root");
} else {
console.log(" db folder already exists");
}
// Determine SQLite DB file path from environment or use default
const dbPath = process.env.DB_PATH || path.join(targetPath, "links.db");
/**
* Initializes the SQLite database and creates the `urls` table and triggers if they don't exist.
* Ensures unique slugs per user or for anonymous users.
* @returns {Promise<sqlite3.Database>} - Resolves with the database instance.
*/
const initDB = async () => {
return new Promise(async (resolve, reject) => {
const db = new sqlite3.Database(dbPath, async (err) => {
if (err) {
console.error("Failed to open the database", err);
reject(err);
} else {
console.log("Database connected");
// Create the `urls` table with constraints
db.run(
`CREATE TABLE IF NOT EXISTS urls (
id INTEGER PRIMARY KEY AUTOINCREMENT,
long_url TEXT NOT NULL,
short_code TEXT NOT NULL,
expiry_date TEXT,
user_id TEXT DEFAULT '',
customized_shortner_slug TEXT,
UNIQUE(user_id, customized_shortner_slug)
)`,
function (err) {
if (err) {
console.error("Error creating table:", err);
reject(err);
} else {
// Create trigger to enforce slug uniqueness logic before insert
db.run(
`
CREATE TRIGGER IF NOT EXISTS enforce_custom_slug_uniqueness_insert
BEFORE INSERT ON urls
BEGIN
-- Prevent duplicate slugs for anonymous users
SELECT RAISE(ABORT, 'Slug must be unique for anonymous users')
WHERE NEW.user_id = '' AND NEW.customized_shortner_slug != '' AND EXISTS (
SELECT 1 FROM urls
WHERE user_id = '' AND customized_shortner_slug = NEW.customized_shortner_slug
);
-- Prevent duplicate slugs for the same logged-in user
SELECT RAISE(ABORT, 'Slug must be unique per user')
WHERE NEW.user_id != '' AND NEW.customized_shortner_slug != '' AND EXISTS (
SELECT 1 FROM urls
WHERE user_id = NEW.user_id AND customized_shortner_slug = NEW.customized_shortner_slug
);
END;
`,
function (err) {
if (err) reject(err);
resolve(db);
}
);
}
}
);
}
});
});
};
/**
* Inserts a new shortened URL record into the database.
* @param {string} longUrl - The original full URL.
* @param {string} shortCode - The generated short code.
* @param {string} expiryDate - The expiration date (format: 'YYYY-MM-DD HH:mm:ss').
* @param {string} user_id - The ID of the user (empty string for anonymous).
* @param {string} customized_shortner_slug - Optional custom slug provided by user.
* @returns {Promise<number>} - Resolves with the new record's ID.
*/
const insertUrl = async (
longUrl,
shortCode,
expiryDate,
user_id,
customized_shortner_slug
) => {
const db = await initDB();
return new Promise((resolve, reject) => {
const sql = `INSERT INTO urls (long_url, short_code, expiry_date, user_id,customized_shortner_slug) VALUES (?, ?, ?, ?, ?)`;
db.run(
sql,
[longUrl, shortCode, expiryDate, user_id, customized_shortner_slug],
function (err) {
if (err) {
if (err.code === "SQLITE_CONSTRAINT") {
reject(
new Error(
"The custom slug is already taken for this user. Please choose a different one."
)
);
}
reject(err);
} else {
resolve(this.lastID); // Return the ID of the inserted row
}
}
);
});
};
/**
* Looks up a short URL by its code or custom slug.
* If the URL has expired, deletes the record and rejects the promise.
* @param {string} shortCode - The short code or slug.
* @param {string} userId - The user ID to validate ownership (used during deletion).
* @returns {Promise<string>} - Resolves with the original long URL.
* @throws {Error} - If not found or expired.
*/
const findByShortCode = async (shortCode, userId = "") => {
const db = await initDB();
return new Promise((resolve, reject) => {
const sql = `SELECT * FROM urls WHERE short_code = ? or customized_shortner_slug = ?`;
db.get(sql, [shortCode, shortCode], (err, row) => {
if (err) {
reject(err);
} else {
if (row) {
const now = new Date();
const expiryDate = new Date(row.expiry_date);
// Delete and reject if the link has expired
if (expiryDate < now) {
if (row?.short_code === shortCode)
db.run("DELETE FROM urls WHERE short_code = ? and user_id = ?", [
shortCode,
userId,
]);
else if (row?.customized_shortner_slug === shortCode)
db.run(
"DELETE FROM urls WHERE customized_shortner_slug = ? and user_id = ?",
[shortCode, userId]
);
reject(new Error("This link has expired."));
} else {
resolve(row.long_url);
}
} else {
reject(new Error("Short code not found."));
}
}
});
});
};
/**
* Checks if a URL already exists with the same expiry date and user.
* Useful for avoiding duplicate entries.
* @param {string} longUrl - The long/original URL.
* @param {string} expiryDate - The expiry date to check against.
* @param {string} user_id - The user ID.
* @returns {Promise<Object|null>} - Resolves with the row if found, otherwise null.
*/
const findByLongUrl = async (
longUrl,
expiryDate,
userId,
customizedShortnerSlug
) => {
const db = await initDB();
return new Promise((resolve, reject) => {
const sql = `SELECT * FROM urls WHERE long_url = ? and expiry_date <= ? and user_id = ? and customized_shortner_slug = ?`;
db.get(
sql,
[longUrl, expiryDate, userId, customizedShortnerSlug],
(err, row) => {
if (err) {
reject(err);
} else {
resolve(row);
}
}
);
});
};
/**
* Retrieves all URL records from the database.
* @returns {Promise<Array>} - Resolves with an array of all records.
*/
const getAllData = async () => {
const db = await initDB();
return new Promise((resolve, reject) => {
const sql = `SELECT * FROM urls`;
db.all(sql, (err, row) => {
if (err) {
reject(err);
} else {
resolve(row);
}
});
});
};
/**
* Deletes all records that have expired (based on current time).
* @returns {Promise<string>} - Resolves with a message indicating how many records were deleted.
*/
const deleteAllExpired = async () => {
const db = await initDB();
return new Promise((resolve, reject) => {
const currentTime = moment().format("YYYY-MM-DD HH:mm:ss");
const sql = `DELETE FROM urls WHERE expiry_date < ?`;
db.run(sql, [currentTime], function (err) {
if (err) {
reject(err);
} else {
resolve(`${this.changes} expired URLs deleted.`);
}
});
});
};
/*
* Retrieves paginated URL records from the database.
* @param {number} page - The page number (starting from 1).
* @param {number} limit - Number of records per page.
* @returns {Promise<Object>} - Resolves with an object containing total count, current page, limit, and data.
*/
const getPaginatedData = async (page = 1, limit = 10, userId = "") => {
const db = await initDB();
const offset = (page - 1) * limit;
userId = String(userId).trim();
return new Promise((resolve, reject) => {
const sqlData = `SELECT * FROM urls where user_id = ? LIMIT ? OFFSET ?`;
const sqlCount = `SELECT COUNT(*) as count FROM urls where user_id = ? `;
db.all(sqlData, [userId, limit, offset], (err, rows) => {
if (err) {
reject(err);
} else {
db.get(sqlCount, [userId], (countErr, countRow) => {
if (countErr) {
reject(countErr);
} else {
resolve({
page,
limit,
total: countRow.count,
data: rows,
});
}
});
}
});
});
};
// Export all database utility functions
module.exports = {
initDB,
insertUrl,
findByShortCode,
findByLongUrl,
getAllData,
deleteAllExpired,
getPaginatedData,
};