'use strict';
const _target = new WeakMap();
const _content = new WeakMap();
const momentJS = require('moment');
const _ = require('lodash');
const tz = require('countryjs');
const parser = require('./lib/Parser').
getInstance();
const commons = require('./lib/Commons').
getInstance();
const ResultCalculator=require('./lib/ResultCalculator');
/**
*
* @param {String} operands
* @param {Array} values
* @return {{}}
*/
const selectOperands = (operands, values) => {
let rule = {};
switch (operands) {
case 'eql':
if (values.length === 1) {
// Simple equal.
rule = values[0];
}
else if (values.length > 1) {
// In a list.
rule = values;
}
else {
// Not valid at all( null).
rule = null;
}
break;
}
return rule;
};
const generalCompare = (obj, rules) => (commons.validator.ruleGeneralValidator(obj, rules.general) &&
commons.validator.ruleDateValidator(obj, rules.dateTime));
let Handler = null;
/**
* The JCompiler Main Object
*
* @description this object get the target ( search condition)
* and the content ( select rows ) and generate
* the query for waterlines and load data
*
* @typedef {Object} JSONTargetObject
* @typedef {Object} JSONContentObject
*
*
* @param {JSONTargetObject} target - the target search
* @param {JSONContentObject} content - the selected column */
class JCompiler {
/**
*
* @param {json} target
* @param {json} content
*/
constructor(target, content) {
Handler = this;
this.target = parser.rawTargetParser(target);
this.content = parser.rawContentParser(content);
}
/**
* @desc target getter
* @return {JSON}
*/
get target() {
return _target.get(Handler);
}
/**
* @desc target setter
* @param {JSON} value
*/
set target(value) {
_target.set(Handler, value);
}
/**
* @desc content getter
* @return {json}
*/
get content() {
return _content.get(Handler);
}
/**
* @desc content setter
* @param {json} value
*/
set content(value) {
_content.set(Handler, value);
}
/**
* @param {T} sails
* @param {string} prefix
* @return {Promise}
*/
loadPNS(sails, prefix = '') {
const functionBody = 'return sails' + prefix + '.devices';
let deviceRules = {general: {}};
let flightRules = null,
bookingRules = null;
let flightFilter,
bookingFilter,
deviceFilter;
const emptyRule = () => true;
const target = this.target;
const topKey = Object.keys(target);
const inDeepRules = [];
const deepLinkPreprocessor = [];
const extraTables = new Set();
flightFilter = bookingFilter = deviceFilter = emptyRule;
if (topKey.indexOf('device') !== -1) {
// add the device data models to function body
deviceRules = parser.loadConditions(target['device'],
new Function('rules', 'return rules.config.isDevice'));
if (Object.keys(deviceRules.inDeep).length) {
inDeepRules.push(...deviceRules.inDeep);
}
if (deviceRules.extraTables.length) {
deviceRules.extraTables.forEach(item => extraTables.add(item));
}
if (Object.keys(deviceRules.deepLink).length) {
deepLinkPreprocessor.push(...deviceRules.deepLink);
}
}
if (topKey.indexOf('flight') !== -1) {
flightRules = parser.loadConditions(target['flight'],
new Function('rules', 'return rules.config.isFlight'));
if (Object.keys(flightRules.inDeep).length) {
inDeepRules.push(...flightRules.inDeep);
}
if (flightRules.extraTables.length) {
flightRules.extraTables.forEach(item => extraTables.add(item));
}
if (Object.keys(flightRules.deepLink).length) {
deepLinkPreprocessor.push(...flightRules.deepLink);
}
}
if (topKey.indexOf('booking') !== -1) {
bookingRules = parser.loadConditions(target['booking'],
new Function('rules', 'return rules.config.isBooking'));
if (Object.keys(bookingRules.inDeep).length) {
inDeepRules.push(...bookingRules.inDeep);
}
if (bookingRules.extraTables.length) {
bookingRules.extraTables.forEach(item => extraTables.add(item));
}
if (Object.keys(bookingRules.deepLink).length) {
deepLinkPreprocessor.push(...bookingRules.deepLink);
}
}
const that = this;
const orchestra = async () => {
let res = [];
try {
const normalVerbs = Array.from(extraTables);
const extraInfo = (await Promise.all(normalVerbs.map(
tableName => new Function('sails', `return sails${prefix}.${tableName.toLowerCase()}`).call(that, sails).
find()
))).reduce((p, v, index) => {
p[normalVerbs[index]] = v;
return p;
}, {});
// update the rules.
deviceFilter = obj => commons.validator.ruleDateValidator(obj, deviceRules.dateTime || {});
if (flightRules) {
flightFilter = obj => generalCompare(obj, flightRules);
}
if (bookingRules) {
bookingFilter = obj => generalCompare(obj, bookingRules);
}
const devices = await (new Function('sails', functionBody).call(that,
sails).
find({where: deviceRules.general}).
populate(['anyFlights', 'anyBooking']));
let PNSs = devices.filter(device => {
let out;
if (!deviceFilter(device)) {
out = false;
}
else {
out = true;
if (flightFilter !== emptyRule) {
out = out && device.anyFlights.filter(flightFilter).length !== 0;
}
if (bookingFilter !== emptyRule) {
out = out && device.anyBooking.filter(bookingFilter).length !== 0;
}
}
return out;
});
deepLinkPreprocessor.forEach(rules => {
// check op1 & op2
const op1 = rules.config;
const op2 = Object.keys(rules.value).
filter((undefined, index) => index === 0).
// Skip all operands expect first one.
reduce((p, v) => {
if (_.isObject(rules.value[v]) && typeof rules.value[v].target === 'string') {
p = commons.commons.getVerbConfig(rules.value[v].target);
}
else {
p = rules.value[v];
}
return p;
}, {});
const operand = Object.keys(rules.value)[0]; // Skip all operands expect first one.
// Resolve the OPs
let OP2Value = null;
if (op2.target && (op2.target.hasOwnProperty('shouldPreProcessed') &&
typeof op2.target.shouldPreProcessed === 'boolean' && op2.target.shouldPreProcessed)) {
// do something
const [table, filed, ...join] = op2.target.maps;
OP2Value = {
targetTable: table.replace('T_', ''),
targetFiled: filed.replace('F_', ''),
map: {},
};
commons.commons.chunkArray(join, 3).
forEach((parts, index) => {
const [destinationField, table, field] = parts;
extraInfo[table.replace('T_', '')].forEach(row => {
if (!index) {
OP2Value.map[row[destinationField.replace('F_', '')]] = row[field.replace('F_', '')];
}
else {
const objectKeys = Object.keys(OP2Value.map);
for (let i = 0; i < objectKeys.length; i++) {
if (OP2Value.map[objectKeys[i]] === row[destinationField.replace('F_', '')]) {
OP2Value.map[objectKeys[i]] = row[field.replace('F_', '')];
}
}
}
});
});
}
else {
OP2Value = op2;
}
if (op1.calcRequired) {
//
const calcParam=(guideMap, pns)=>{
const postProcess=guideMap.filter(item=>item.startsWith('POST_'));
const [table, filed, resultsMode, ...otherCondition]=guideMap.
filter(item=>!item.startsWith('POST_')).
map(item=>item.replace('T_', '').replace('F_', '').replace('C_', ''));
const baseTable=table==='bookings'?pns.anyBooking : table==='flights'? pns.anyFlights :[];
const filedData=baseTable.map(item=>item[filed]);
const resultCalculator=new ResultCalculator(resultsMode);
filedData.forEach(value=>{
let res=false;
if (!otherCondition.length) {
res=true;
}
else {
// It's messy code , if u need fix it.
const [cmd, ...params]=otherCondition[0].replace('C_', '').split('_');
if (cmd==='last') {
res=(momentJS().add(params[0], params[1]).isAfter(momentJS(value)));
}
}
resultCalculator.collectConditionResults(res, value);
});
let res=resultCalculator.Results;
if (postProcess.length) {
// just accept first post process;
/**
* messy code is here !
* its just hard coded unfortunately.
*
*/
if (postProcess[0]==='POST_avrage_per_month') {
// get first date,
// get last date
// calc how many month available.
// calculate the avrage.
const first=_.min(filedData.map(item=>new Date(item)));
const last=_.max(filedData.map(item=>new Date(item)));
const months=momentJS(last).diff(momentJS(first), 'months', true)+1;
if (months) {
res=res/months;
}
}
}
return res;
};
PNSs=PNSs.filter(pns=>{
let res=false;
const calculatedOp1Value=calcParam(op1.maps, pns);
switch (operand) {
case 'eql':
res= calculatedOp1Value == OP2Value;
break;
case 'greaterThan':
res= calculatedOp1Value >= OP2Value;
break;
case 'lessThan':
res = calculatedOp1Value <= OP2Value;
break;
}
return res;
});
}
else if (op1.shouldPreProcessed) {
/* todo I disable this part because right now I dent have any deep link in device.
if( op1.isDevice){
// do device processing
}*/
/* Two first is table and base filed from the baseSelector and the rest
is join table , each join have 3 part , so split the join tables to 3 base chunks.
*/
let secondParts = selectOperands(operand, OP2Value);
const [targetTable, baseFiled, ...joinedTables] = rules.config.maps;
commons.commons.chunkArray(joinedTables, 3).
reverse().
reduce((p, v) => {
const [destinationField, table, searchField] = v.
map(item => item.replace('T_', '').
replace('F_', ''));
if (extraInfo[table]) {
const filtering = part => {
let res = false;
if (typeof secondParts === 'string' ||
typeof secondParts === 'number' ||
typeof secondParts === 'boolean') {
res = part[searchField] == secondParts; // eslint-disable-line eqeqeq
}
else {
res = false;
}
return res;
};
secondParts = extraInfo[table].
filter(filtering).
// Run the rules.
reduce((p, v, index, array) => { // Format the results.
p.push(v[destinationField]);
if (array.length === index + 1) {// Collapse the results.
p = Array.from(new Set(p));
if (p.length === 1) {
p = p[0];
}
}
return p;
}, []);
}
}, []);
// Filter PNS based the new rules and update it.
const myCompare = (val1, val2) => {
let res = false;
const typeOfVal2 = typeof val2;
if (Array.isArray(val2)) {
res = val2.indexOf(val1) !== -1;
}
else if (['string', 'number', 'boolean'].indexOf(typeOfVal2) !== -1) {
res = val1 == val2;// eslint-disable-line eqeqeq
}
return res;
};
PNSs = PNSs.filter(pns => {
let baseSelector = pns;
let byPassed = false;
switch (targetTable.replace('T_', '')) {
case 'flights':
baseSelector = baseSelector.anyFlights;
break;
case 'bookings':
baseSelector = baseSelector.anyBooking;
}
for (let i = 0; i < baseSelector.length && !byPassed; i++) {
byPassed = myCompare(baseSelector[i][baseFiled.replace('F_', '')], secondParts);
}
return byPassed;
});
}// End of OP1 deep processing.
else { // OP2 processing
const filter = (pns => {
const currentOp1Value = parser.extractValue(op1.maps[0], op1.maps[1], pns);
const currentOp2Value = parser.extractValue(OP2Value.targetTable, OP2Value.targetFiled, pns);
return commons.validator.logicalConfirmDeepMap(currentOp1Value, currentOp2Value, operand, OP2Value.map);
});
PNSs = PNSs.filter(filter);
}
});// End of deep link processing.
inDeepRules.forEach(rule => {
PNSs = PNSs.filter(pns => {
const currentOP1Value = parser.extractValue(rule.maps[0], rule.maps[1], pns);
const operand = Object.keys(rule.rules)[0];
const currentOP2Value = parser.extractValue(rule.rules[operand][0], rule.rules[operand][1], pns);
return commons.validator.logicalConfirmDeepMap(currentOP1Value, currentOP2Value, operand);
});
});
res = PNSs.map(PNS => {
const tmp = {};
tmp.pnsID = PNS.push_token;
tmp.lang = PNS.device_locale || 'en-us';
let params;
if (PNS.device_locale !== undefined && PNS.device_locale !== null &&
Handler.content[PNS.device_locale.toLowerCase()] !==
undefined) {
params = Handler.content[PNS.device_locale.toLowerCase()];
}
else if (Handler.content['en-us'] !== undefined) {
params = Handler.content['en-us'];
}
else {
params = Handler.content[Object.keys(Handler.content)[0]];
}
tmp.params = {};
Object.keys(params).
map(key => params[key]).
reduce((p, v) => {
v.forEach(param => {
if (p.filter(key => key.name === param.name).length !== 1) {
p.push(param);
}
});
return p;
}, []).
forEach(param => {
let val = '';
switch (param.target.maps[0]) {
case 'devices':
val = PNS[param.target.maps[1]];
try {
if (val !== undefined && val !== null && commons.validator.isValidDate(val)) {
tz.timezones(PNS.country);
if (PNS.latitude !== null && PNS.latitude !== undefined) {
const tzwhere = require('tzwhere');
tzwhere.init();
const min = tzwhere.tzOffsetAt(PNS.latitude,
PNS.longitude) / 60000;
val = momentJS(val).
utcOffset(min).
format('DD-MMM-YYYY hh:mm A');
}
else if (tz !== undefined && tz !== null &&
tz.length >= 0) {
val = momentJS(val).
utcOffset(tz[0].replace('UTC', '')).
format('DD-MMM-YYYY hh:mm A');
}
else {
val = momentJS(val).
format('DD-MMM-YYYY hh:mm A') +
' (in UTC timezone) ';
}
}
}
catch (err) {
console.log(err);// eslint-disable-line no-console
}
break;
case 'flights':
val = PNS.anyFlights.reduce((p, v, i, arr) => p + (i != 0 ? ',' : '') + v[param.target.maps[1]],
'');
break;
case 'bookings':
val = PNS.anyBooking.reduce((p, v, i, arr) => p + (i != 0 ? ',' : '') + v[param.target.maps[1]],
'');
break;
default:
val = 'NotFound';
break;
}
tmp.params[param.name] = val;
});
return tmp;
});
}
catch (err) {
res = err;
}
return res;
};
return new Promise((resolve, reject) => {
orchestra().
then(res => {
if (res instanceof Error) {
reject(res);
}
else {
resolve(res);
}
}).
catch(err => {
reject(err);
});
});
}
}
module.exports = JCompiler;