fly-json-odm
Version:
An Object Document Mapper to handle JSON on the fly for NodeJS or Browser
963 lines (924 loc) • 28.2 kB
JavaScript
/*! FlyJson v1.22.0 | (c) 2021 M ABD AZIZ ALFIAN | MIT License | https://github.com/aalfiann/fly-json-odm */
'use strict';
const Helper = require('./helper');
const operator = require('./operator');
/**
* FlyJson class
*/
class FlyJson extends Helper {
/**
* Constructor
* @param {object} mixins [Optional] Register your custom function as mixin
*/
constructor (mixins) {
super();
this.data1 = [];
this.data2 = [];
this.query = [];
this.result = [];
if (this.isObject(mixins)) {
for (const key in mixins) {
if (mixins[key] && typeof mixins[key] === 'function') {
this[key] = (fn) => {
mixins[key](fn);
return this;
};
}
}
}
}
/**
* Sort by
* @param {string} field this is the key name
* @param {bool} reverse reverse means sort descending
* @param {fn} primer this is the primer function
* @return {fn}
*/
_sortBy (field, reverse, primer) {
const key = primer
? function (x) { return primer(x[field]); }
: function (x) { return x[field]; };
reverse = !reverse ? 1 : -1;
return (a, b) => {
a = key(a);
b = key(b);
return reverse * ((a > b) - (b > a));
};
}
/**
* Find disctint in all objects
* @param {array object} source this is an array objects
* @param {object} obj this is the single current object
* @return {boolean}
*/
_findDistinct (source, obj) {
let found = false;
for (let i = 0; i < source.length; i++) {
const count = Object.keys(obj).length;
let recount = 0;
this.foreach(obj, function (v, k) {
if (source[i][k] === v) {
recount++;
}
});
if (count === recount) {
found = true;
}
}
return found;
}
/**
* Set Mode for clone array
* Note: Use this before set(data)
*
* @param {string} name Mode name shallow | deep
*/
setMode (name) {
this.mode = name.toString().toLowerCase();
return this;
}
/**
* Set json array as data table
* @param {array} data this is json data
* @return {this}
*/
set (data) {
if (this.fastCheckArrayObject(data)) {
if (this.mode === 'shallow') {
this.data1 = this.shallowClone(data);
} else {
this.data1 = this.deepClone(data);
}
} else {
throw new Error('Set data must be an array contains object.');
}
return this;
}
/**
* Show current data list
* @returns {array} object
*/
list () {
return this.data1;
}
/**
* Insert new data into data table
* @param {object} obj this is the object data
* @return {this}
*/
insert (obj) {
if (this.isObject(obj) && !this.isEmptyObject(obj)) {
this.data1.push(obj);
} else {
throw new Error('New value must be an object and not empty');
}
return this;
}
/**
* Insert many new data into data table
* @param {array} data this is the data array object
* @return {this}
*/
insertMany (data) {
if (this.isArray(data)) {
const l = data.length;
for (let i = 0; i < l; i++) {
if (this.isObject(data[i]) && !this.isEmptyObject(data[i])) {
this.data1.push(data[i]);
} else {
throw new Error('New value must be an object and not empty');
}
}
} else {
throw new Error('Data must be an array object');
}
return this;
}
/**
* Update single data in data table
* @param {string} key this is the key name
* @param {*} value this is the value of key name
* @param {object} obj this is the new value to replace all old data
* @return {this}
*/
update (key, value, obj) {
if (this.isEmpty(key) || this.isEmpty(value)) {
throw new Error('Key and Value must be defined and value must be unique');
}
if (!this.isObject(obj) || this.isEmptyObject(obj)) {
throw new Error('New value must be an object and not empty');
}
const l = this.data1.length;
for (let i = 0; i < l; i++) {
if (this.data1[i][key] === value) {
this.data1.splice(i, 1);
this.data1.push(Object.assign({ [key]: value }, obj));
break;
}
}
return this;
}
/**
* Update many data in data table
* @param {string} key this is the key name
* @param {array} data this is the data array object for update
* @return {this}
*/
updateMany (key, data) {
if (this.isEmpty(key) || !this.isString(key)) {
throw new Error('Key and Value must be defined and value must be unique');
}
if (this.isEmptyArray(data) || !this.fastCheckArrayObject(data)) {
throw new Error('Data to update must be an array object and not empty');
}
const l = this.data1.length;
const len = data.length;
const newdata = [];
let result;
for (let i = 0; i < l; i++) {
result = false;
for (let x = 0; x < len; x++) {
if (this.data1[i][key] === data[x][key]) {
result = true;
newdata.push(data[x]);
}
}
if (result === false) {
newdata.push(this.data1[i]);
}
}
this.data1 = newdata;
return this;
}
/**
* Modify single data in data table
* @param {string} key this is the key name
* @param {*} value this is the value of key name
* @param {object} obj this is the new value to add or modify old data
* @return {this}
*/
modify (key, value, obj) {
if (this.isEmpty(key) || this.isEmpty(value)) {
throw new Error('Key and Value must be defined and value must be unique');
}
if (!this.isObject(obj) || this.isEmptyObject(obj)) {
throw new Error('New value must be an object and not empty');
}
const l = this.data1.length;
let data;
for (let i = 0; i < l; i++) {
if (this.data1[i][key] === value) {
data = this.data1[i];
this.data1.splice(i, 1);
this.data1.push(Object.assign({ [key]: value }, data, obj));
break;
}
}
return this;
}
/**
* Modify many data in data table
* @param {string} key this is the key name
* @param {array} data this is the data array object for modify
* @return {this}
*/
modifyMany (key, data) {
if (this.isEmpty(key) || !this.isString(key)) {
throw new Error('Key must be defined');
}
if (this.isEmptyArray(data) || !this.fastCheckArrayObject(data)) {
throw new Error('Data to modify must be an array object and not empty');
}
if (this.mode === 'shallow') {
throw new Error('Shallow mode is not allowed for modifyMany!');
}
const l = this.data1.length;
const len = data.length;
const newdata = [];
let old; let result;
for (let i = 0; i < l; i++) {
result = false;
for (let x = 0; x < len; x++) {
if (this.data1[i][key] === data[x][key]) {
result = true;
old = this.data1[i];
newdata.push(Object.assign(old, data[x]));
}
}
if (result === false) {
newdata.push(this.data1[i]);
}
}
this.data1 = newdata;
return this;
}
/**
* Delete single data in data table
* @param {string} key this is the key name
* @param {*} value this is the value of key name
* @return {this}
*/
delete (key, value) {
if (!this.isEmpty(key) && !this.isEmpty(value)) {
const l = this.data1.length;
for (let i = 0; i < l; i++) {
if (this.data1[i][key] === value) {
this.data1.splice(i, 1);
break;
}
}
} else {
throw new Error('Key and Value must be defined also remember that Value must be unique.');
}
return this;
}
/**
* Delete many data in data table
* @param {string} key this is the key name
* @param {array} data this is the array of key value to be deleted
* @return {this}
*/
deleteMany (key, data) {
if (!this.isEmpty(key) && !this.isEmptyArray(data)) {
const l = this.data1.length;
const len = data.length;
const newdata = [];
let result = false;
for (let i = 0; i < l; i++) {
result = false;
for (let x = 0; x < len; x++) {
if (this.data1[i][key] === data[x]) {
result = true;
}
}
if (result === false) {
newdata.push(this.data1[i]);
}
}
this.data1 = newdata;
} else {
throw new Error('Key and Data array of key value must be defined.');
}
return this;
}
/**
* Filter data by select name key
* @param {array} key
* @return {this}
*/
select (key) {
if (!this.isEmpty(key) && this.isArray(key) && !this.isEmptyArray(key)) {
const newdata = [];
let res;
const l = this.data1.length;
const dl = key.length;
for (let i = 0; i < l; i++) {
res = {};
for (let x = 0; x < dl; x++) {
if (this.data1[i][key[x]] !== undefined) {
res[key[x]] = this.data1[i][key[x]];
}
}
newdata.push(res);
}
this.data1 = newdata;
}
return this;
}
/**
* Filter data by where
* @param {*} args
* @return {this}
*/
where (...args) {
let result = [];
if (!this.isEmpty(args[0]) && this.isString(args[0]) && (args[1] !== undefined)) {
let a;
let b;
let c = true;
let mid;
if (args.length > 2) {
mid = args[1];
a = args[0];
b = args[2];
if (!this.isEmpty(args[3])) c = args[3];
} else {
mid = '===';
a = args[0];
b = args[1];
c = true;
}
mid = mid.toString().toLowerCase();
const search = { [a]: b };
let v; let s;
const self = this;
const data = this.data1.filter(function (o) {
return Object.keys(search).every(function (k) {
v = o[k];
s = search[k];
if (c === false && mid !== 'regex') {
if (!self.isObject(o[k])) {
v = (o[k]) ? o[k].toString().toLowerCase() : o[k];
}
s = search[k].toString().toLowerCase();
}
switch (mid) {
case '=':
return operator.unstrict(mid, v, s);
case '!==':
return v !== s;
case '==':
return operator.unstrict(mid, v, s);
case '!=':
return operator.unstrict(mid, v, s);
case '>':
return v > s;
case '>=':
return v >= s;
case '<':
return v < s;
case '<=':
return v <= s;
case 'in':
if (self.isString(v)) {
return (v.indexOf(s) !== -1);
}
result = [];
if (v) {
self.foreach(v, function (value) {
if (c) {
if (value === s) {
result.push(value);
}
} else {
if (self.isString(value)) {
value = value.toLowerCase();
}
if (value === s) {
result.push(value);
}
}
});
}
return (result.length > 0);
case 'in like':
if (self.isString(v)) {
return (v.indexOf(s) !== -1);
}
result = [];
if (v) {
self.foreach(v, function (value) {
if (c) {
if (self.isString(value)) {
if (value.toString().indexOf(s) !== -1) {
result.push(value);
}
}
} else {
if (self.isString(value)) {
value = value.toLowerCase();
if (value.indexOf(s) !== -1) {
result.push(value);
}
}
}
});
}
return (result.length > 0);
case 'not in':
if (self.isString(v)) {
return (v.indexOf(s) === -1);
}
result = [];
if (v && v.length) {
self.foreach(v, function (value) {
if (value !== s) {
result.push(value);
}
});
return (result.length === v.length);
} else {
if (self.isObject(v)) {
self.foreach(v, function (value) {
if (c) {
if (value !== s) {
result.push(value);
}
} else {
if (self.isString(value)) {
value = value.toLowerCase();
}
if (value !== s) {
result.push(value);
}
}
});
return (result.length === Object.keys(v).length);
}
return false;
}
case 'not in like':
if (self.isString(v)) {
return (v.indexOf(s) === -1);
}
result = [];
if (v && v.length) {
self.foreach(v, function (value) {
if (value !== null && value !== undefined) {
if (!self.isString(value)) {
value = value.toString();
}
}
if (self.isString(value)) {
if (value.indexOf(s) === -1) {
result.push(value);
}
} else {
result.push(value);
}
});
return (result.length === v.length);
} else {
if (self.isObject(v)) {
self.foreach(v, function (value) {
if (c) {
if (value !== null && value !== undefined) {
if (!self.isString(value)) {
value = value.toString();
}
}
if (self.isString(value)) {
if (value.indexOf(s) === -1) {
result.push(value);
}
} else {
result.push(value);
}
} else {
if (value !== null && value !== undefined) {
if (!self.isString(value)) {
value = value.toString();
}
}
if (self.isString(value)) {
value = value.toLowerCase();
if (value.indexOf(s) === -1) {
result.push(value);
}
} else {
result.push(value);
}
}
});
return (result.length === Object.keys(v).length);
}
return false;
}
case 'not':
return v !== s;
case 'like':
return (v.indexOf(s) !== -1);
case 'not like':
return (v.indexOf(s) === -1);
case 'regex':
return (s.test(v));
case 'func':
return s(v);
case 'function':
return s(v);
default:
return v === s;
}
});
});
if (this.scope === 'query') {
this.result = data;
} else {
this.data1 = data;
}
}
return this;
}
/**
* Beginning to build query with condition OR
* @return {this}
*/
begin () {
this.scope = 'query';
return this;
}
/**
* Add new OR condition
* @return {this}
*/
or () {
if (this.scope === 'query') {
const l = this.result.length;
for (let i = 0; i < l; i++) {
this.query.push(this.result[i]);
}
}
return this;
}
/**
* Ending of build query with condition OR
* @return {this}
*/
end () {
if (this.scope === 'query') {
const l = this.result.length;
for (let i = 0; i < l; i++) {
this.query.push(this.result[i]);
}
this.data1 = this.query;
this.query = [];
this.result = [];
this.scope = '';
}
return this;
}
/**
* Distinct Data
* @param {string} fieldName [Optional] Finding duplicate data by fieldname
* @return {this}
*/
distinct (fieldName) {
fieldName = (fieldName === undefined) ? '' : fieldName;
if ((!this.isEmpty(fieldName) && !this.isString(fieldName)) || this.isArray(fieldName) || this.isObject(fieldName)) {
throw new Error('Field name must be string.');
}
const array = this.data1;
const unique = [];
const result = [];
const li = array.length;
if (!this.isEmpty(fieldName) && this.isString(fieldName)) {
for (let i = 0; i < li; i++) {
if (array[i][fieldName] !== undefined && !unique[array[i][fieldName]]) {
result.push(array[i]);
unique[array[i][fieldName]] = 1;
}
}
} else {
for (let i = 0; i < li; i++) {
if (this._findDistinct(unique, array[i]) === false) {
result.push(array[i]);
unique.push(array[i]);
}
}
}
this.data1 = result;
return this;
}
/**
* Fuzzy Search
* @param {string|number} query Text or number to search.
* @param {array} keys Keys is the field to match search.
* @param {boolean} caseSensitive [Optional] Search with match case sensitive. Default is false.
* @param {boolean} sort [Optional] When true it will sort the results by best match. Default is false.
* @returns {this}
*/
fuzzySearch (query, keys, caseSensitive = false, sort = false) {
if (query === undefined || query === null) {
throw new Error('Query is required.');
}
if (keys === undefined || keys === null || this.isEmptyArray(keys)) {
throw new Error('Field keys is required.');
}
this.data1 = this.fuzzy(this.data1, query, keys, caseSensitive, sort);
return this;
}
/**
* Cleanup all temporary object
* @return {this}
*/
clean () {
this.data1 = [];
this.data2 = [];
this.query = [];
this.result = [];
this.metadata = {};
this.scope = '';
this.mode = '';
this.name = '';
return this;
}
/**
* Joining two data table
* @param {string} name this is the name key for joined data
* @param {array} data this is the array of data table
* @return {this}
*/
join (name, data) {
if (!this.isEmpty(name) && this.isString(name)) {
if (this.fastCheckArrayObject(data)) {
if (this.mode === 'shallow') {
this.data2 = this.shallowClone(data);
} else {
this.data2 = this.deepClone(data);
}
this.name = name;
this.scope = 'join';
} else {
throw new Error('Data must be an array object.');
}
} else {
throw new Error('Name is required and it must be string.');
}
return this;
}
/**
* Merge two data table
* @param {string} a this is identifier key name of data table 1
* @param {string} b this is identifier key name of data table 2
* @return {this}
*/
merge (a, b) {
if (this.scope === 'join') {
if (!this.isEmpty(a) && this.isString(a)) {
if (!this.isEmpty(b) && this.isString(b)) {
const indexB = this.data2.reduce((result, item) => {
result[item[b]] = item;
return result;
}, {});
this.scope = '';
this.data1 = this.data1.map(item => Object.assign(item, indexB[item[a]]));
} else {
throw new Error('Unique identifier key for table 2 is required.');
}
} else {
throw new Error('Unique identifier key for table 1 is required.');
}
} else {
throw new Error('You should join first before doing merge.');
}
return this;
}
/**
* Set identifier to joining two data table
* @param {string} a this is identifier key name of data table 1
* @param {string} b this is identifier key name of data table 2
* @param {bool} nested [Optional] this will make the joined data nested or as array. Default is true
* @param {bool} caseSensitive [Optional] this will filter the joined data (only work if nested is false). Default is true
* @return {this}
*/
on (a, b, nested, caseSensitive) {
const self = this;
nested = (nested === undefined ? true : nested);
caseSensitive = (caseSensitive === undefined ? true : caseSensitive);
if (self.scope === 'join') {
if (!this.isEmpty(a) && this.isString(a)) {
if (!this.isEmpty(b) && this.isString(b)) {
const indexB = self.data2.reduce((result, item) => {
result[item[b]] = item;
return result;
}, {});
const result = [];
self.data1.map(function (value, index) {
const newdata = {};
const arr = Object.keys(self.data1[index]);
const l = arr.length;
for (let i = 0; i < l; i++) {
if (arr[i] === a) {
if (self.name === arr[i]) {
if (nested) {
newdata[arr[i]] = indexB[self.data1[index][arr[i]]];
} else {
newdata[arr[i]] = self.data2.filter(function (item) {
if (caseSensitive) {
return item[b] === value[arr[i]];
} else {
if (self.isString(item[b]) && self.isString(value[arr[i]])) {
return item[b].toLowerCase() === value[arr[i]].toLowerCase();
}
return item[b] === value[arr[i]];
}
});
}
} else {
if (nested) {
newdata[self.name] = indexB[self.data1[index][arr[i]]];
} else {
newdata[self.name] = self.data2.filter(function (item) {
if (caseSensitive) {
return item[b] === value[arr[i]];
} else {
if (self.isString(item[b]) && self.isString(value[arr[i]])) {
return item[b].toLowerCase() === value[arr[i]].toLowerCase();
}
return item[b] === value[arr[i]];
}
});
}
newdata[arr[i]] = value[arr[i]];
}
} else {
newdata[arr[i]] = value[arr[i]];
}
}
return result.push(newdata);
});
self.scope = '';
self.data1 = result;
} else {
throw new Error('Unique identifier key for table 2 is required.');
}
} else {
throw new Error('Unique identifier key for table 1 is required.');
}
} else {
throw new Error('You should join first before doing join on.');
}
return this;
}
/**
* Sort data ascending or descending by key name
* @param {string} name this is the name key
* @param {bool} desc [Optional] this is the sort order. Default is false
* @param {primer} primer this is the primer function
* @return {this}
*/
orderBy (name, desc, primer) {
desc = (desc === undefined ? false : desc);
if (!this.isEmpty(name) && this.isString(name) && this.isBoolean(desc)) {
this.data1.sort(this._sortBy(name, desc, primer));
}
return this;
}
/**
* Group detail data by key name
* @param {string} name Column name
* @param {string} groupName [Optional] Set new group name
* @return {this}
*/
groupDetail (name, groupName) {
groupName = (groupName === undefined ? '' : groupName);
if (this.isEmpty(name) || !this.isString(name)) {
throw new Error('name is required and must be string.');
}
if (!this.isString(groupName)) {
throw new Error('group name must be string.');
}
const data = this.data1.reduce((objectsByKeyValue, obj) => {
const value = obj[name];
objectsByKeyValue[value] = (objectsByKeyValue[value] || []).concat(obj);
return objectsByKeyValue;
}, {});
const group = [];
if (groupName) {
group.push({ [groupName]: data });
} else {
group.push({ [name]: data });
}
this.data1 = group;
return this;
}
/**
* Group data by name or with sum by field name
* @param {string} name Column name
* @param {array} sumField [Optional] column name for SUM
* @return {this}
*/
groupBy (name, sumField) {
sumField = (sumField === undefined ? [] : sumField);
if (this.isEmpty(name) || !this.isString(name)) {
throw new Error('name is required and must be string.');
}
if (!this.isArray(sumField)) {
throw new Error('field name for sum must be array.');
}
const l = sumField.length;
const data = this.data1.reduce(function (res, obj) {
obj.item_count = 1;
if (!(obj[name] in res)) {
res.__array.push(res[obj[name]] = obj);
} else {
for (let i = 0; i < l; i++) {
res[obj[name]][sumField[i]] += obj[sumField[i]];
}
res[obj[name]].item_count += 1;
}
// average
for (let i = 0; i < l; i++) {
res[obj[name]]['average_' + sumField[i]] = (res[obj[name]][sumField[i]] / res[obj[name]].item_count);
}
return res;
}, { __array: [] });
this.data1 = data.__array;
return this;
}
/**
* Skip data by size
* @param {string|integer} size
* @return {this}
*/
skip (size) {
if (!this.isEmpty(size) && this.isInteger(size)) {
this.data1 = this.data1.slice(size);
}
return this;
}
/**
* Take data by size
* @param {string|integer} size
* @return {this}
*/
take (size) {
if (!this.isEmpty(size)) {
size = parseInt(size);
if (this.isInteger(size)) {
if (this.data1.length > size) {
this.data1.length = size;
}
}
}
return this;
}
/**
* Paginate data by page and pageSize
* @param {string|integer} page Page number
* @param {string|integer} pageSize Total size or item or limit per page
* @return {this}
*/
paginate (page, pageSize) {
if (!this.isEmpty(page) && !this.isEmpty(pageSize)) {
page = parseInt(page);
pageSize = parseInt(pageSize);
if (this.isInteger(page) && this.isInteger(pageSize)) {
const count = this.data1.length;
--page; // because pages logically start with 1, but technically with 0
this.data1 = this.data1.slice(page * pageSize, (page + 1) * pageSize);
this.metadata = {
page: (page + 1),
page_size: pageSize,
total_page: Math.ceil(count / pageSize),
total_records: count
};
}
}
return this;
}
/**
* Make asynchronous process with Promise
* @param {*} fn
* @return {this}
*/
promisify (fn) {
const self = this;
return new Promise(function (resolve, reject) {
try {
resolve(fn.call(self, self));
} catch (err) {
reject(err);
}
});
}
/**
* Return output data table
* @return {array}
*/
exec () {
this.mode = '';
return this.data1;
}
}
module.exports = FlyJson;