/**
* @fileOverview Entry point for getting shortened url for given URL using counter based base62 encoding
*
* @author Anish Lushte
* @author Darshit Vora
*
* @requires NPM:mongoose
*/
const mongoose = require('mongoose');
const shortUrl = require('../models/ShortURL');
const counter = require('../models/Counter');
const { toBase62 } = require('./base62');
/**
* URL Shortener class object
* Initialize URL Shortener Object
* @example
* const URLShortener = require('node-short-url');
*
* const options = {
* characters: "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
* minHashCount: "4",
* domain: "click.com"
* }
* const shortUrl = new URLShortener(mongodb, errorCallback, options);
* @class
* @constructor
* @param {object} mongodb connection url
* @param {function} error callback
* @param {object} url shortener configuration object
*/
const URLShortener = function(mongodb, onConnectionError, options = {}) {
if (!mongodb) throw new Error('Mongo URL not specified');
const { characters, minHashCount, domain } = options;
if (!domain) throw new Error('Please provide domain, Generated hash will be appended to given domain.');
if (mongoose.connection.readyState === 0) {
_connectMongo(mongodb)
.catch(err => (onConnectionError(err)));
this.symbols = characters || '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
this.len = this.symbols.length;
this.minHashCount = +minHashCount || 4;
this.domain = domain;
this.CounterModel = counter();
_createDefaultRecord(this.CounterModel);
this.ShortURLModel = shortUrl();
}
}
function _createDefaultRecord(model) {
model.countDocuments((err, result) => {
if (err) return console.error(err);
if (!result) {
const record = new model({ _id: 0 });
record.save();
}
});
}
function _getRecordId(model) {
return new Promise((resolve, reject) => {
const updateQuery = { $inc: { last_index: 1 } };
model.findOneAndUpdate({ _id: 0 }, updateQuery, { new: true }, (err, result) => {
if (err) return reject(err);
return resolve(result);
});
});
}
function _connectMongo(url) {
return new Promise((resolve, reject) => {
mongoose.connect(url, {
useNewUrlParser: true,
useUnifiedTopology: true,
useFindAndModify: false,
}, (err) => {
if (err) return reject(err);
return resolve();
});
});
}
function _find(model, query) {
return new Promise((resolve, reject) => {
model.findOne(query, '_id URL expires_at', (err, result) => {
if (err) return reject(err);
return resolve(result);
});
});
}
function _incrementHits(model, id) {
model.updateOne({
_id: id,
}, {
$inc: {
hits: 1,
},
}, (err) => {
if (err) console.error(err);
});
}
/**
* Takes hash string parameters
* Retrieve actual url from hash
* @example
*
* const shortUrl = new URLShortner('mongodb://192.168.0.161/shortdb', (err) => console.log(err), { domain: 'https:click.com' });
*
* shortUrl.retrieve('0004')
* .then(res => console.log('res' , res))
* .catch(err => console.log(err));
*
* @memberof URLShortener
* @param {String} hash shortened url hash
*
* @returns {String} returns long url of hash
*/
URLShortener.prototype.retrieve = function(hash) {
const _id = hash.trim();
if (!_id) {
return Promise.reject({ message: 'Please specify hash value.' });
}
return _find(this.ShortURLModel, { _id })
.then(result => {
if (!result) {
return Promise.reject({ statusCode: 404, message: 'No record found.' });
}
if (result && result.expires_at < new Date()) {
return Promise.reject({ statusCode: 409, message: 'URL has expired.' });
}
_incrementHits(this.ShortURLModel, result._id);
return Promise.resolve(result);
})
.catch(err => Promise.reject(err));
};
/**
* Takes 2 string parameters url and expiry date.
* Generates and returns shortURL
* @example
*
* const shortUrl = new URLShortner('mongodb://192.168.0.161/shortdb', (err) => console.log(err), { domain: 'https:click.com' });
*
* shortUrl.shortenUrl('https://client.example.com/user/1', new Date("2025-02-01"))
* .then(res => console.log('res' , res))
* .catch(err => console.log(err));
*
* @memberof URLShortener
* @param {String} url URL to be shortened
* @param {Date} expiry Date when url will expire
*
* @returns {object} returns object with shortened url
*/
URLShortener.prototype.shortenUrl = function (url, expiry) {
if (!url || typeof url !== 'string') {
return Promise.reject({ message: 'Please specify URL to be shortened' });
}
return _getRecordId(this.CounterModel)
.then(data => {
let hash = toBase62(data.last_index, this.symbols, this.len);
const shortUrl = `${this.domain}/${hash}`;
if (hash.length < this.minHashCount) {
hash = hash.padStart(this.minHashCount, '0');
}
const record = new this.ShortURLModel({
_id: hash,
URL: url,
expires_at: expiry,
});
return new Promise((resolve, reject) => {
record.save(function (err, result) {
if (err) return reject(err);
return resolve({ url: shortUrl });
});
});
})
.catch(err => Promise.reject(err));
};
module.exports = URLShortener;