File: /var/dev/farhangmoaser/web/routes/api/1.0/entry.js
/**
* Express endpoints for /entry address
* Version: 0.1
* Author: Babak Vandad
*
* Restful api for addresses:
* GET /entry
* PUT /entry
* POST /entry
* DELETE /entry
*/
'use strict';
var express = require('express');
var router = express.Router();
var fs = require('fs');
var path = require('path');
var consts = require(path.join(BASEDIR, 'consts'));
var authHelper = require(path.join(BASEDIR, 'helpers/auth'));
var LongTextModel = require(path.join(BASEDIR, 'models/longText'));
var DictionaryModel = require(path.join(BASEDIR, 'models/dictionary'));
var UserAccessModel = require(path.join(BASEDIR, 'models/userAccess'));
var SubscriptionModel = require(path.join(BASEDIR, 'models/subscription'));
var EntryModel = require(path.join(BASEDIR, 'models/entry'));
var db = require(path.join(BASEDIR, 'connectors/mysql'));
var authenticate = authHelper.authenticate;
var access = authHelper.access;
/**
* Fail a request by releasing database connection, rollback transaction
* and sending the proper error code and message with 4xx or 5xx status codes.
* @param {object} express response object
* @param {object} db connection
* @param {object} error object from costs.js
* @param {integer} override status code (if you want to send 200 instead of 4xx/5xx)
* @return {function} the function to fail the request.
*/
var fail = function(response, conn, error, status){
return function(){
if(conn && conn.release)
conn.release();
response.status(status ? status : error.status).json({error: error.code, message: error.message});
};
};
/**
* Sends data to the client, commits transaction and releases the db connection
* @param {object} express response object
* @param {object} db connection
* @param {object} data for the client
*/
var send = function(res, conn, data){
if(conn) conn.release();
if(data) res.json(data);
else res.end();
};
/**
* makes a searchable string (removes spaces)
* @param {string} str user input or given entry title
* @return {string} searchable string
*/
var simplifyString = function(str){
return str
.trim()
.toLowerCase()
.replace(/['!\- ()]/g, '')
.replace(/[ًٌٍَُِّْـ.]/g, '')
.replace(/[ ]/g, '')
.replace(/ي/g, 'ی')
.replace(/ى/g, 'ی')
.replace(/ك/g, 'ک')
.replace(/[àâä]/g, 'a')
.replace(/æ/g, 'ae')
.replace(/[îï]/g, 'i')
.replace(/[éèêë]/g, 'e')
.replace(/[ôö]/g, 'o')
.replace(/œ/g, 'oe')
.replace(/[ùûü]/g, 'u')
.replace(/ç/g, 'c')
.replace(/ñ/g, 'n')
.replace(/ÿ/g, 'y')
.replace()
;
};
/**
* get the list of entries based on constraints:
* return unique titles.
* user subscriptions are considered.
* source uuid
* title
* langFrom
* langTo
* order
* limit page size
* page page number (from 0)
*/
router.get('/distinct', authenticate, access('entry:get-distinct', 'نمایش مدخلهای یکتا از میان اشتراکهای کاربر'), async (req, res, next) => {
// return send(res, null, [req.user.fields(), req.user.hitData, req.user.subscriptions]);
let conn;
try {
conn = await db.getConnectionPromise();
}catch(err) { return fail(res, conn, consts.e.ERR_DB_ERROR)(); }
var query = {};
if(req.query.hasOwnProperty('langFrom')) query.langFrom = parseInt(req.query.langFrom);
if(req.query.hasOwnProperty('langTo')) query.langTo = parseInt(req.query.langTo);
if(req.query.hasOwnProperty('order')) query.order = parseInt(req.query.order);
let reqSources = [];
if(req.query.hasOwnProperty('source')) reqSources = req.query.source.split(',');
let subSources = req.user.subscriptions.map(subscription => subscription.book);
//admin is not checked for it's subscriptions
// if(req.user.field('role')==consts.v.ROLE_ADMIN){
if(reqSources.length)
query.source = reqSources;
/*}else{
//user's can look up in dictionaries present both in subscriptions and in request
if(reqSources.length)
query.source = reqSources.filter(source => subSources.indexOf(source)!=-1);
else
query.source = subSources;
if(!query.source.length)
return send(res, conn, {sum: 0, list: []});
}*/
if(req.query.hasOwnProperty('title'))
query['%%searchable'] = simplifyString(req.query.title)+'%';
var limit = req.query.hasOwnProperty('limit') ? Math.min(100, parseInt(req.query.limit)) : 15;
var page = req.query.hasOwnProperty('page') ? parseInt(req.query.page) : 0;
var offset = page*limit;
// list of records based on criteria
var listQuery = function(records) {
var response = [];
EntryModel.countDistinct(conn, query).then(
function(sum) {
send(res, conn, {sum: sum, list: records.map(record => {
delete record.data; /* data is only the id of long_text table. not useful for the user! */
return record;
})});
}, fail(res, conn, consts.e.ERR_DB_ERROR));
};
EntryModel.listDistinct(conn, query, limit, offset, req.query.orderBy).then(
listQuery, fail(res, conn, consts.e.ERR_DB_ERROR));
});
/**
* get the list of entries based on constraints:
* return unique titles. each entry contains it's defenitions.
* user subscriptions are considered.
* source uuid
* title
* langFrom
* langTo
* order
* limit page size
* page page number (from 0)
*/
router.get('/distinct/full', authenticate, access('entry:get-distinct', 'نمایش مدخلهای یکتا از میان اشتراکهای کاربر'), async (req, res, next) => {
let conn;
try {
conn = await db.getConnectionPromise();
}catch(err) { return fail(res, conn, consts.e.ERR_DB_ERROR)(); }
var query = {};
if(req.query.hasOwnProperty('langFrom')) query.langFrom = parseInt(req.query.langFrom);
if(req.query.hasOwnProperty('langTo')) query.langTo = parseInt(req.query.langTo);
if(req.query.hasOwnProperty('order')) query.order = parseInt(req.query.order);
let reqSources = [];
if(req.query.hasOwnProperty('source')) reqSources = req.query.source.split(',');
let subSources = [];
subSources = req.user.subscriptions.map(subscription => subscription.book);
//admin is not checked for it's subscriptions
// if(req.user.field('role')==consts.v.ROLE_ADMIN){
if(reqSources.length)
query.source = reqSources;
/*}else{
//user's can look up in dictionaries present both in subscriptions and in request
if(reqSources.length)
query.source = reqSources.filter(source => subSources.indexOf(source)!=-1);
else
query.source = subSources;
}*/
if(req.query.hasOwnProperty('title'))
query['searchable'] = simplifyString(req.query.title);
var limit = req.query.hasOwnProperty('limit') ? Math.min(100, parseInt(req.query.limit)) : -1;
var page = req.query.hasOwnProperty('page') ? parseInt(req.query.page) : 0;
var offset = page*limit;
try {
let records = await EntryModel.list(conn, query, -1, 0, req.query.orderBy, true);
if(req.user.field('role')!=consts.v.ROLE_ADMIN)
records = records.map(record => {
if(subSources.indexOf(record.source)==-1)
record.data = null;
return record;
});
send(res, conn, {sum: records.length, list: records});
}catch (e) {
fail(res, conn, consts.e.ERR_DB_ERROR)();
}
});
/**
* get the list of entries based on constraints:
* user subscriptions are not considered. (only editors and admins must be allowed.)
* source uuid
* title
* langFrom
* langTo
* order
* limit page size
* page page number (from 0)
* full load one full record not the list.
* neighbours get next and previous pages (parameter full must be set)
*/
router.get('/', authenticate, access('entry:get', 'نمایش مدخلها (بدون احتساب اشتراکهای کاربر)'), function(req, res, next){
db.getConnection(function(err, conn){
if(err) return fail(res, conn, err)();
var query = {};
if(req.query.hasOwnProperty('langFrom')) query.langFrom = parseInt(req.query.langFrom);
if(req.query.hasOwnProperty('langTo')) query.langTo = parseInt(req.query.langTo);
if(req.query.hasOwnProperty('order')) query.order = parseInt(req.query.order);
if(req.query.hasOwnProperty('source')) query.source = req.query.source;
if(req.query.full && req.query.full=='false') req.query.full=false;
if(req.query.neighbours && req.query.neighbours=='false') req.query.neighbours=false;
if(req.query.hasOwnProperty('title')){
if(req.query.full)
query.title = req.query.title;
else
query['%%searchable'] = simplifyString(req.query.title)+'%';
}
var limit = req.query.hasOwnProperty('limit') ? Math.min(100, parseInt(req.query.limit)) : 15;
var page = req.query.hasOwnProperty('page') ? parseInt(req.query.page) : 0;
var offset = page*limit;
// get next and previous records
var getNeighbours = function(record, orderBy){
var entryModel = new EntryModel(conn, record);
if(!orderBy) orderBy='a-z';
entryModel.getNeighbours(orderBy).then(
function(record){
send(res, conn, record.fields());
}, fail(res, conn, consts.e.ERR_DB_ERROR));
};
// load the full record:
// calculate maxOrder (override maxOrder field), data from longText, neighbours if set
// TODO: get multiple records with full data. not only one.
var loadFull = function(record){
EntryModel.count(conn, {
source: record.source,
langFrom: record.langFrom,
langTo: record.langTo,
title: record.title,
}).then(
function(sum){
record.maxOrder = sum;
var longTextModel = new LongTextModel(conn, {id: record.data});
longTextModel.load().then(
function(longText) {
record.data = JSON.parse(longText.field('content'));
if(req.query.neighbours)
getNeighbours(record, req.query.orderBy);
else
send(res, conn, record);
}, fail(res, conn, consts.e.ERR_DB_ERROR));
}, fail(res, conn, consts.e.ERR_DB_ERROR));
};
// list of records based on criteria
var listQuery = function(records) {
var response = [];
if(!req.query.full)
return EntryModel.count(conn, query).then(
function(sum) {
send(res, conn, {sum: sum, list: records.map(record => {
delete record.data; /* data is only the id of long_text table. not useful for the user! */
return record;
})});
}, fail(res, conn, consts.e.ERR_DB_ERROR));
if(!records.length) return send(res, conn, []);
var record = records[0];
loadFull(record);
};
EntryModel.list(conn, query, limit, offset, req.query.orderBy).then(
listQuery, fail(res, conn, consts.e.ERR_DB_ERROR));
});
});
/* add a new netry */
router.put('/', authenticate, access('entry:put', 'افزودن مدخل'), async (req, res, next) => {
if(!req.body.title || !req.body.source || !req.body.langFrom || !req.body.langTo)
return fail(res, null, consts.e.ERR_REQUIRED_FIELDS)();
let conn;
try { conn = await db.getConnectionPromise(); }
catch(err) { return fail(res, conn, consts.e.ERR_DB_ERROR)(); }
let dictionaryModel = new DictionaryModel(conn, {uuid: req.body.source});
let book;
try { book = await dictionaryModel.load(conn); }
catch(err) { return fail(res, conn, consts.e.ERR_DB_ERROR)(); }
let records;
try { records = await EntryModel.list(conn, {
title: req.body.title,
source: req.body.source,
langFrom: req.body.langFrom,
langTo: req.body.langTo
}) }
catch(err) { return fail(res, conn, consts.e.ERR_DB_ERROR)(); }
let newOrder = records ? records.length+1 : 1;
let longTextModel = new LongTextModel(conn, {content: JSON.stringify(req.body.data)});
try { await longTextModel.save(); }
catch(err) { return fail(res, conn, consts.e.ERR_DB_ERROR)(); }
let entryData = {
title: req.body.title,
searchable: simplifyString(req.body.title),
source: book.field('id'),
langFrom: req.body.langFrom,
langTo: req.body.langTo,
order: newOrder,
maxOrder: newOrder,
prefix: req.body.prefix,
postfix: req.body.postfix,
dataType: req.body.dataType
};
if(req.body.hasOwnProperty('pronunciation')){
try {
fs.renameSync(
path.join(BASEDIR, 'private/temp/'+req.body.pronunciation),
path.join(BASEDIR, 'private/pronunciation/'+req.body.pronunciation)
);
entryData.pronunciation = req.body.pronunciation;
}catch(err) {}
}
let entryModel = new EntryModel(conn, entryData);
entryModel.field('data', longTextModel.field('id'));
let record;
try {
record = await entryModel.save();
await record.setMaxOrder();
}
catch(err) { return fail(res, conn, consts.e.ERR_DB_ERROR)(); }
send(res, conn, record.fields());
});
/* update an existing entry */
router.post('/', authenticate, access('entry:post', 'ویرایش مدخل'), async (req, res, next) => {
if(!req.body.title || !req.body.source || !req.body.langFrom || !req.body.langTo || !req.body.order)
return fail(res, null, consts.e.ERR_REQUIRED_FIELDS)();
let conn;
try {
conn = await db.getConnectionPromise();
}catch(err) { return fail(res, conn, consts.e.ERR_DB_ERROR)(); }
req.body.reorder = Math.max(1, req.body.reorder);
// we load all the records regardless of the order. maybe we need to reorder them.
let records;
try {
records = await EntryModel.list(conn, {
title: req.body.title,
source: req.body.source,
langFrom: req.body.langFrom,
langTo: req.body.langTo,
});
}catch(e) {
return fail(res, conn, consts.e.ERR_DB_ERROR)();
}
let record;
let recordIndex;
// get the proper record and it's index
records.forEach(function(row, i){
if(row.order == req.body.order) {
record = row;
recordIndex = i;
}
});
// invalid record
if(!record)
return fail(res, conn, consts.e.ERR_MISSING_RECORD)();
req.body.reorder = Math.min(req.body.reorder, records.length);
// what if there is no longtext record yet? inconsitency
let longTextModel = new LongTextModel(conn, {id: record.data});
try {
await longTextModel.load();
// update longtext
longTextModel.field('content', JSON.stringify(req.body.data));
await longTextModel.save();
}catch(err) { return fail(res, conn, consts.e.ERR_DB_ERROR)(); }
let entryModel = new EntryModel(conn, {id: record.id});
try {
await entryModel.visit();
}catch(err) { return fail(res, conn, consts.e.ERR_DB_ERROR)(); }
let checkOrders = async () => {
if(record.maxOrder==records.length && (!req.body.reorder || req.body.reorder==record.order))
return;
records.splice(recordIndex, 1);
records.sort((a, b) => {
if(a.order > b.order) return 1;
if(a.order < b.order) return -1;
if(a.order == b.order) return 0;
});
records.splice(req.body.reorder-1, 0, record);
let reorders = [];
records.forEach((record, i) => {
if(record.order != i+1 || record.maxOrder!=records.length)
reorders.push({id: record.id, order: i+1, maxOrder: records.length});
});
req.body.order = req.body.reorder;
for(let i=0; i<reorders.length; i++)
await EntryModel.reorder(conn, reorders[i].id, reorders[i].order, reorders[i].maxOrder);
};
var updateObj = {};
if(req.body.hasOwnProperty('prefix') && req.body.prefix!=record.prefix)
updateObj.prefix = req.body.prefix;
if(req.body.hasOwnProperty('postfix') && req.body.postfix!=record.postfix)
updateObj.postfix = req.body.postfix;
if(req.body.hasOwnProperty('dataType') && req.body.dataType!=record.dataType)
updateObj.dataType = req.body.dataType;
if(req.body.hasOwnProperty('pronunciation') && req.body.pronunciation!=record.pronunciation){
try {
fs.unlinkSync(path.join(BASEDIR, 'private/pronunciation/'+record.pronunciation));
}catch(err) {}
if(req.body.pronunciation)
try {
fs.renameSync(
path.join(BASEDIR, 'private/temp/'+req.body.pronunciation),
path.join(BASEDIR, 'private/pronunciation/'+req.body.pronunciation)
);
updateObj.pronunciation = req.body.pronunciation;
}catch(err) {}
if(!req.body.pronunciation) updateObj.pronunciation='';
}
if(Object.keys(updateObj).length)
await entryModel.update(updateObj);
try {
await checkOrders();
send(res, conn, req.body);
}catch(err) { return fail(res, conn, consts.e.ERR_DB_ERROR)(); }
});
/* delete an entry */
router.delete('/', authenticate, access('entry:delete', 'حذف مدخل'), async (req, res, next) => {
if(!req.query.title || !req.query.source || !req.query.langFrom || !req.query.langTo || !req.query.order)
return fail(res, null, consts.e.ERR_REQUIRED_FIELDS)();
let conn;
try { conn = await db.getConnectionPromise(); }
catch(err) { return fail(res, conn, consts.e.ERR_DB_ERROR)(); }
let records;
try { records = await EntryModel.list(conn, {
title: req.query.title,
source: req.query.source,
langFrom: req.query.langFrom,
langTo: req.query.langTo,
}); }
catch(err) { return fail(res, conn, consts.e.ERR_DB_ERROR)(); }
if(!records.length)
return fail(res, conn, consts.e.ERR_MISSING_RECORD)();
let record;
let recordIndex;
records.forEach(function(row, i){
if(row.order == req.query.order) {
record = row;
recordIndex = i;
}
});
if(!record)
return fail(res, conn, consts.e.ERR_MISSING_RECORD)();
let entryModel = new EntryModel(conn, {id: record.id});
try { await entryModel.remove(); }
catch(err) { return fail(res, conn, consts.e.ERR_DB_ERROR)(); }
records.splice(recordIndex, 1);
records.sort(function(a, b){
if(a.order > b.order) return 1;
if(a.order < b.order) return -1;
if(a.order == b.order) return 0;
});
let reorders = [];
records.forEach(function(record, i){
reorders.push({id: record.id, order: i+1, maxOrder: records.length});
});
try {
for(let i=0; i<reorders.length; i++)
await EntryModel.reorder(conn, reorders[i].id, reorders[i].order, reorders[i].maxOrder);
} catch(err) {}
if(record.pronunciation)
try {
fs.unlinkSync(path.join(BASEDIR, 'private/pronunciation/'+record.pronunciation));
}catch(err) {}
send(res, conn, null);
});
module.exports = router;