1liner
Version:
Query JSON with one line of code.
408 lines (353 loc) • 12.8 kB
JavaScript
/**
* 1liner - Copyright (C) 2020, Callum Rimmer <callum@deadtrendy.co.uk>
*/
/**
* Lodash helpers
*/
const get = require('lodash.get');
const isArray = require('lodash.isarray');
const flatten = require('lodash.flatten');
const round = require('lodash.round');
const min = require('lodash.min');
const max = require('lodash.max');
const mean = require('lodash.mean');
const sumBy = require('lodash.sumby');
const isUndefined = require('lodash.isundefined');
const unique = require('lodash.uniqby');
const isNumber = require('lodash.isnumber');
/**
* Define equators
*/
const equators = [
'=',
'!=',
'>',
'>=',
'<',
'<=',
];
/**
* Define array operators
*/
const operators = [
'count',
'map',
'filter',
'sum',
'mean',
'min',
'max',
'range',
'unique',
'exists',
'default',
'age',
'date',
'regex',
];
/**
* Private functions
*/
function __getMultipleQueries(segment = '', method = '', singleQuery) {
const queries = segment.split(',')
.map(s => {
return s.replace(`${method}([`, '').replace('])', '').trim();
})
.map(q => {
return singleQuery(q);
});
if (queries.filter(q => !isNumber(q)).length > 0) {
throw new Error(`Query error: Only numbers can be returned for multiple query statements ${segment}`);
}
return queries;
}
function __getEquator(item = '') {
let selected;
equators.forEach(eq => { if(item.includes(eq)) selected = eq; });
if (!selected) throw new Error(`Equator error: no equator exists for ${item}`);
return selected;
}
function __checkOperator(element = '') {
let check = false;
operators.forEach(o => { if(element.includes(o + '(')) check = true; });
if (!check && element.includes('(')) throw new Error(`Operator error: no operator exists for ${element}`);
return check;
}
function __getOperatorValue(element = '', segment){
const opValue = element.split('(').pop().split(')')[0];
if (!opValue) {
if (element.includes('count(') || element.includes('max(') || element.includes('min(') || element.includes('mean(')) return null;
if (opValue === '') throw new Error(`Operator error: No value in ${element} (in ${segment})`);
throw new Error(`Operator error : No value in ${element} (in ${segment})`);
}
return opValue;
}
function cleanStringQuotes(str = '') {
return str.replace(/"/g, '').replace(/'/g, '');
}
function __recursive(obj = {}, el = '', nextEl, segment, source) {
if(!nextEl) return obj;
const newObj = isArray(obj) ? obj : obj[el];
// Check element is array and next element is an operator
if(__checkOperator(nextEl)) {
if (!isArray(obj[el]) &&
!isArray(obj) &&
nextEl !=='exists()' &&
!nextEl.includes('default(') &&
!nextEl.includes('age(') &&
!nextEl.includes('date(') &&
!nextEl.includes('regex(')
) {
throw new Error(`Path error: no object exists at ${el} (in ${segment}`);
}
} else {
if (!obj) throw new Error(`Path error: no object exists at ${el} (in ${segment}`);
if (!obj[el]) throw new Error(`Path error: ${el} (in ${segment})`);
if (isArray(obj[el])) throw new Error(`Array error: ${el}. (in ${segment}) should be followed by operator i.e. count(), filter() as it is an array`);
return obj[el];
}
if (nextEl.includes('filter(')) {
const item = __getOperatorValue(nextEl, segment);
const equator = __getEquator(item);
const key = item.split(equator)[0];
const value = cleanStringQuotes(item.split(equator)[1]);
if (equator === '!=') {
return newObj.filter(o => o[key] != value);
} else if (equator === '>') {
return newObj.filter(o => o[key] > value);
} else if (equator === '>=') {
return newObj.filter(o => o[key] >= value);
} else if (equator === '<') {
return newObj.filter(o => o[key] < value);
} else if (equator === '<=') {
return newObj.filter(o => o[key] <= value);
} else {
return newObj.filter(o => {
if(typeof o[key] === "boolean") return o[key].toString() == value;
return o[key] == cleanStringQuotes(value);
});
}
}
if (nextEl.includes('unique(')) {
const key =__getOperatorValue(nextEl, segment);
return unique(newObj, key);
}
if (nextEl.includes('map(')) {
const key =__getOperatorValue(nextEl, segment);
return flatten(newObj.map((o) => {
if (!o) throw new Error(`Path error: no ${key} in parent of query ${segment}`)
return o[key];
}));
}
if (nextEl.includes('sum(')) {
const key =__getOperatorValue(nextEl, segment);
return round(sumBy(newObj, key), 2);
}
if (nextEl.includes('count(')) {
return newObj.length;
}
if (nextEl.includes('exists(')) {
return (newObj === '' || newObj === 0 || newObj === null || newObj === undefined).toString();
}
if (nextEl.includes('date(') || nextEl.includes('age(')) {
const def = __getOperatorValue(nextEl, segment);
const format = (def.split(',')[0] || '').trim();
const defined_date_path = (def.split(',')[1] || '').trim();
if(!['YY', 'MM', 'DD', 'HH'].includes(format)) {
throw new Error(`Date error: in ${segment}) should be followed by eligible formatter i.e. YY, MM, DD, HH`);
}
function __generateDate(date_str) {
function __checkDate(value) {
let date;
try {
date = new Date(value)
if (date instanceof Date && !isNaN(date)) {
date = new Date(value)
} else {
throw new Error(`Date error: in ${segment}) the value is not a valid date`);
}
} catch(e) {
throw new Error(`Date error: in ${segment}) the value is not a valid date`);
}
return date
}
const date = __checkDate(date_str);
let now;
if (defined_date_path) {
// This only works will parent keys (i.e. top-level)
const found_date = get(source, defined_date_path);
now = __checkDate(found_date);
} else {
now = new Date();
}
let result;
if (nextEl.includes('date(')) {
if (format === 'YY') {
result = date.getUTCFullYear();
} else if (format === 'MM') {
result = date.getUTCMonth() + 1;
} else if (format === 'DD') {
result = date.getUTCDate();
} else if (format === 'HH') {
result = date.getUTCHours();
} else {
result = 0;
}
} else {
const diff = (now.getTime() - date.getTime());
const YY = Math.abs(new Date(diff).getUTCFullYear() - 1970);
const months = (YY * 12);
const additional_month = new Date(
now.getTime() -
(
new Date(
now.getUTCFullYear() +
'-' +
(date.getUTCMonth() + 1) +
'-' +
date.getUTCDate()
)
)
).getUTCMonth();
const MM = months + additional_month;
const DD = Math.floor(diff/ 1000 / (24 * 3600));
const HH = DD * 24;
if (format === 'DD') {
result = DD;
} else if (format === 'MM') {
result = MM;
} else if (format === 'YY') {
result = YY;
} else if (format === 'HH') {
result = HH;
} else {
result = 0;
}
}
return result;
}
if (isArray(newObj)) {
return newObj.map(n => __generateDate(n));
} else {
return __generateDate(newObj);
}
}
if (nextEl.includes('regex(')) {
const def =__getOperatorValue(nextEl, segment);
let result;
try {
result = newObj.match(def);
} catch (e) {
throw new Error(`Type error: in ${segment}) the value is not a string`);
}
return result && result.length ? result[0] : '';
}
if (nextEl.includes('mean(')) {
const def =__getOperatorValue(nextEl, segment);
if (mean(newObj) === 0) return 0;
return round(mean(newObj), 2) || (def ? parseFloat(def) : 0);
}
if (nextEl.includes('min(')) {
const def = __getOperatorValue(nextEl, segment);
if (min(newObj) === 0) return 0;
return min(newObj) || (def ? parseFloat(def) : 0);
}
if (nextEl.includes('max(')) {
const def = __getOperatorValue(nextEl, segment);
if (max(newObj) === 0) return 0;
return max(newObj) || (def ? parseFloat(def) : 0);
}
if (nextEl.includes('range(')) {
return (max(newObj) - min(newObj) || 0);
}
if (nextEl.includes('default(')) {
const def = __getOperatorValue(nextEl, segment);
if (newObj === 0) return 0;
return newObj || (isNaN(parseFloat(def)) ? cleanStringQuotes(def) : parseFloat(def));
}
}
/**
* 1Liner class
*/
class L {
constructor (source){
this.source = source;
}
eachQuery(segment) {
const clean = segment.replace('each.', '')
const segments = clean.split('.');
const first_segment = segments[0];
const subsequent_segment = segments.splice(1, segments.length).join('.');
const each_items = this.singleQuery(first_segment);
const result = each_items.map(item_source => {
return this.singleQuery(`item.${subsequent_segment}`, {
item: [item_source]
})
});
return result;
}
multiQuery(segment, method) {
const queries = segment.split(',')
.map(s => {
return s.replace(`${method}([`, '').replace('])', '').trim();
})
.map(q => {
// If number rather than query then just return
if (!isNaN(parseInt(q))) return round(q, 2);
return this.singleQuery(q);
});
if (queries.filter(q => !isNumber(q)).length > 0) {
throw new Error(`Multi query error: Only numbers can be returned for multiple query statements ${segment}`);
}
return queries;
}
singleQuery(segment, amended_source) {
// Clone source object
let result = { ...(amended_source ? amended_source : this.source) };
const elements = segment.split('.');
// Return object
if(!(__checkOperator(segment))){
const obj = get(result, segment);
if (isUndefined(obj)) throw new Error(`Path error : No object path for ${segment}`);
return obj;
}
// Traverse object
elements.forEach((el, i) => {
const nextEl = elements[(i + 1)];
result = __recursive(result, el, nextEl, segment, this.source);
});
if (isUndefined(result)) throw new Error(`Path error: No object path for ${segment}`);
return result;
}
query(segment = '') {
if (segment.startsWith('max([')) {
const results = this.multiQuery(segment, 'max');
return max(results);
}
if (segment.startsWith('min([')) {
const results = this.multiQuery(segment, 'min');
return min(results);
}
if (segment.startsWith('range([')) {
const results = this.multiQuery(segment, 'range');
return (max(results) - min(results) || 0)
}
if (segment.startsWith('each.')) {
const results = this.eachQuery(segment, 'each');
return results;
}
return this.singleQuery(segment);
}
}
/**
* Exporting
*/
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = L;
} else {
if(typeof define === "function" && define.amd) {
define([], function() { return L; });
} else {
window.L = L;
}
}