File: //var/dev/farhangmoaser/web/models/user.js
"use strict";
var md5 = require('md5');
var path = require('path');
var redis = require(path.join(BASEDIR, 'connectors/redis.js'));
var conf = require(path.join(BASEDIR, 'config'));
var SubscriptionModel = require('./subscription');
var redclient;
redis.connect(function(client) {
redclient = client;
});
class UserModel {
/**
* Constructor
* @param {Object} data values for table fields.
* At least on of `key` or `id` must be set.
*/
constructor(conn, data) {
this.data = data;
this.conn = conn;
this.subscriptions = [];
this.cacheKey = '';
this.hitData = {
hitCount: 0,
lastVisit: (new Date()).getTime(),
lastReset: (new Date()).getTime(),
};
if(data.hasOwnProperty('id'))
this.cacheKey = 'userdata-id:'+data.id;
// else if(data.hasOwnProperty('email')) this.cacheKey = 'userdata-email:'+data.email;
}
field(key, value) {
if(arguments.length==0) return;
if(arguments.length==1)
return this.data[key];
if(value===undefined)
delete this.data[key];
else
this.data[key] = value;
}
fields(){
return this.data;
}
generateActivationKey() {
let key = '';
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (var i=0; i<32; i++)
key += possible.charAt(Math.floor(Math.random() * possible.length));
this.field('activationKey', key);
}
save(updateVisit) {
updateVisit = typeof updateVisit !== 'undefined' ? updateVisit : false;
if(this.data.hasOwnProperty('id')) return this.update(updateVisit);
else return this.add();
}
add() {
var self = this;
return new Promise(function(resolve, reject){
self.conn.query(`INSERT INTO user SET ?`, self.data, function(err, result) {
if (err) return reject(err.message);
self.data.id = result.insertId;
resolve(self);
});
});
}
update(updateVisit) {
var self = this;
return new Promise(function(resolve, reject){
var visited = updateVisit ? ', visited=CURRENT_TIMESTAMP' : '';
self.data.gender = self.data.gender ? self.data.gender : 0;
self.data.hitCount = self.data.hitCount ? self.data.hitCount : 0;
self.data.hitLimit = self.data.hitLimit ? self.data.hitLimit : 0;
self.data.hitPeriod = self.data.hitPeriod ? self.data.hitPeriod : 0;
self.conn.query(`UPDATE user SET ? ${visited} WHERE ?`, [self.data, {id: self.data.id}], function(err, rows, fields){
if (err) return reject(err.message);
resolve(self);
});
});
}
load(nocache=false) {
if(nocache)
return this.loadFromDB();
else
return new Promise((resolve, reject) =>
this.loadFromCache().then(
resolve,
error => this.loadFromDB().then(resolve, reject))
);
}
loadFromDB() {
return new Promise((resolve, reject) => {
this.loadUserData().then(
(user) => {
if(!user) return resolve(false);
this.loadSubscriptions().then(
(user) => {
this.loadSubscriptions().then(
() => {
/* you may want to log the error(s) but we can cotinue without saving to cache*/
this.saveToCache().then(resolve, error => resolve(this));
}, error => reject(error));
}, error => reject(error));
}, error => reject(error));
}, error => reject(error));
}
loadUserData() {
return new Promise((resolve, reject) => {
let params = {};
if(this.data.hasOwnProperty('id')) params.id = this.data.id;
else if(this.data.hasOwnProperty('email')) params.email = this.data.email;
else if(this.data.hasOwnProperty('activationKey')) params.activationKey = this.data.activationKey;
else return reject('At least on of the id and email fields must be provided.');
this.conn.query(`SELECT * FROM user WHERE ?`, params, (err, rows, fields) => {
if (err) return reject(err.message);
if(!rows.length) return resolve(false);
this.data = rows[0];
this.cacheKey = 'userdata-id:'+this.data.id;
if(!redclient) return resolve(this);
redclient.get(this.cacheKey, (err, raw) => {
if(err) return resolve(this);
try {
let cacheData = JSON.parse(raw);
if((new Date()).getTime() - cacheData.timestamp > conf.cacheLifeSpan.userData*1000)
this.hitData = {
hitCount: 0,
lastVisit: (new Date()).getTime(),
lastReset: (new Date()).getTime(),
};
else
// we've loaded user data from db and we are going to load the subscriptions from db too. so only the hit data is relevent.
this.hitData = cacheData.hitData;
}catch(e) {
//preventing process termination. we can continue without reading cache data.
}
resolve(this);
});
});
});
}
loadSubscriptions() {
return new Promise((resolve, reject) => {
if(!this.data.id) return reject('not a valid user data');
SubscriptionModel.getActiveSubscriptionsOf(this.conn, this.data.id).then(
subscriptions => {
this.subscriptions = subscriptions;
resolve(this);
},
errmsg => reject(errmsg)
);
});
}
loadFromCache() {
return new Promise((resolve, reject) => {
if(!redclient || !this.cacheKey) reject('redis client inaccessible.');
redclient.get(this.cacheKey, (error, raw) => {
if(error || !raw) return reject('redis read error.');
try {
let cacheData = JSON.parse(raw);
if((new Date()).getTime() - cacheData.timestamp > conf.cacheLifeSpan.userData*1000)
return reject('cache outdated.');
this.data = cacheData.data;
this.subscriptions = cacheData.subscriptions;
this.hitData = cacheData.hitData;
resolve(this);
}catch(e) {
reject('redis parse error.');
}
});
});
}
saveToCache() {
return new Promise((resolve, reject) => {
if(redclient) {
return redclient.set(
this.cacheKey,
JSON.stringify({
data: this.data,
subscriptions: this.subscriptions,
timestamp: (new Date()).getTime(),
hitData: this.hitData,
}),
(err) => {
if(err) return reject('cache error')
resolve(this);
});
}
reject('redis not available.');
});
}
resetHitCounter() {
this.hitData = {
hitCount: 0,
lastVisit: (new Date()).getTime(),
lastReset: (new Date()).getTime()
};
return this.saveToCache();
}
delete() {
var self = this;
return new Promise(function(resolve, reject){
if(!self.data.hasOwnProperty('id'))
reject('user id is required');
self.conn.query(`DELETE FROM user WHERE ?`, {id: self.data.id}, function(err, rows, fields){
if(err) return reject(err.message);
resolve(self);
});
});
}
/**
* Increments the hit counter.
* IMPORTANT: Hence the hit limitations is a business rule we do not consider it in the model.
* @return {Promise} Result of the operation
*/
touch() {
return new Promise((resolve, reject) => {
if(!this.cacheKey || !redclient) return reject('cache inaccessible.');
this.hitData.hitCount++;
this.hitData.lastVisit = (new Date()).getTime();
this.saveToCache().then(
() => resolve(this),
err => reject(err)
);
});
}
static list(conn, query, limit, offset, orderBy) {
if(!orderBy) orderBy = 'newest';
var orderByStr = '';
switch(orderBy){
case 'newest':
orderByStr = 'ORDER BY `updated` DESC, id DESC';
break;
case 'oldest':
orderByStr = 'ORDER BY `updated` ASC, id ASC';
break;
case 'displayname:a-z':
orderByStr = 'ORDER BY `sortable` ASC, `id` ASC';
break;
case 'displayname:z-a':
orderByStr = 'ORDER BY `sortable` DESC, `id` DESC';
break;
case 'email:a-z':
orderByStr = 'ORDER BY `email` ASC, `id` ASC';
break;
case 'email:z-a':
orderByStr = 'ORDER BY `email` DESC, `id` DESC';
break;
}
limit = limit || 100;
offset = offset || 0;
return new Promise(function(resolve, reject){
var params = [offset, limit];
var where = '';
if(Object.keys(query).length) {
where = 'WHERE !!';
params.unshift(query);
}
var template = conn.format(`SELECT * FROM \`user\` ${where} ${orderByStr} limit ?, ?`, params);
conn.query(template, query, function(err, rows, fields){
if (err) return reject(err.message);
resolve(rows);
});
});
}
static count(conn, query) {
return new Promise(function(resolve, reject){
var where = '';
if(Object.keys(query).length)
where = 'WHERE !!';
var template = conn.format(`SELECT COUNT(*) AS sum FROM \`user\` ${where}`, query);
conn.query(template, query, function(err, rows, fields){
if (err) return reject(err.message);
resolve(rows[0].sum);
});
});
}
static authenticate(conn, email, password) {
return new Promise(function(resolve, reject){
if(!email || !password) return reject('Email and password required.');
email = email.toLowerCase();
conn.query(`SELECT * FROM user WHERE ?`, {email: email}, function(err, rows, fields){
if(err) return reject(err.message);
if(!rows.length)
return reject('Unknown email address.');
var row = rows[0];
if(md5(password) != row.password)
return reject('Incorrect password.');
(new UserModel(conn, {id: row.id})).load().then(resolve, reject);
});
});
}
}
module.exports = UserModel;