@restorecommerce/chassis-srv
Version:
Restore Commerce microservice chassis
621 lines • 23.6 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createGraphsAssociationFilter = exports.buildGraphSorter = exports.buildGraphLimiter = exports.recursiveFindEntities = exports.buildGraphFilter = exports.buildGraphField = exports.autoCastRootKey = exports.buildComparison = exports.autoCastValue = exports.autoCastKey = exports.toTraversalFilterObject = void 0;
const _ = __importStar(require("lodash"));
const long_1 = __importDefault(require("long"));
const graph_1 = require("@restorecommerce/rc-grpc-clients/dist/generated-server/io/restorecommerce/graph");
const filterOperationMap = new Map([
[0, 'eq'],
[1, 'lt'],
[2, 'lte'],
[3, 'gt'],
[4, 'gte'],
[5, 'isEmpty'],
[6, 'iLike'],
[7, 'in'],
[8, 'neq']
]);
const filterOperatorMap = new Map([
[0, 'and'],
[1, 'or']
]);
/**
* Takes filter object containing field, operation and value and inserts it
* to the obj using the operatorList for finding the last operator position and updates obj
* @param {filter} filter object containing field, operation, value and type
* @param {obj} obj converted filter object
* @param {operatorList} operatorList list of operators from original filter object
*/
const convertFilterToObject = (filter, obj, operatorList) => {
let temp = _.clone(obj);
let value;
if (!filter.type || filter.type === 'STRING' || filter.type === 0) {
value = filter.value;
}
else if ((filter.type === 'NUMBER' || filter.type === 1) && !isNaN(filter.value)) {
value = Number(filter.value);
}
else if (filter.type === 'BOOLEAN' || filter.type === 2) {
if (filter.value === 'true') {
value = true;
}
else if (filter.value === 'false') {
value = false;
}
}
else if (filter.type === 'ARRAY' || filter.type === 4) {
try {
value = JSON.parse(filter.value);
}
catch (err) {
// to handle JSON string parse error
if (err.message.indexOf('Unexpected token') > -1) {
value = JSON.parse(JSON.stringify(filter.value));
}
else {
throw err;
}
}
}
else if (filter.type === 'DATE' || filter.type === 3) {
value = (new Date(filter.value)).getTime();
}
for (let i = 0; i < operatorList.length; i++) {
if (_.isArray(temp)) {
temp = _.find(temp, operatorList[i]);
}
else {
temp = temp[operatorList[i]];
}
if (i === (operatorList.length - 1)) {
// push for final element in the operatorList array
if (filter.operation === 'eq' || filter.operation === 0) {
if (_.isArray(temp)) {
temp.push({ [filter.field]: value });
}
else {
temp[operatorList[i]].push({ [filter.field]: value });
}
}
else if (filter.operation === 'neq' || filter.operation === 8) {
if (_.isArray(temp)) {
temp.push({ [filter.field]: { $not: { $eq: value } } });
}
else {
temp[operatorList[i]].push({ [filter.field]: { $not: { $eq: value } } });
}
}
else {
let opValue;
if (typeof filter.operation === 'string' || filter.operation instanceof String) {
opValue = filter.operation;
}
else if (Number.isInteger(filter.operation)) {
opValue = filterOperationMap.get(filter.operation);
}
const op = `$${opValue}`;
if (_.isArray(temp)) {
temp.push({ [filter.field]: { [op]: value } });
}
else {
temp[operatorList[i]].push({ [filter.field]: { [op]: value } });
}
}
}
}
return obj;
};
/**
* Inserts the new operator into obj iterating throught the operator list and updates obj
* @param {obj} obj Converted filter object
* @param {operatorList} operatorList operator list
* @param {operatorNew} operatorNew new operator
*/
const insertNewOpAndUpdateObj = (obj, operatorList, operatorNew) => {
let pos = _.clone(obj);
for (let i = 0; i < operatorList.length; i++) {
if (_.isArray(pos)) {
pos = _.find(pos, operatorList[i]);
}
else {
pos = pos[operatorList[i]];
}
// push new operator after iterating to the last element in operatorList
if (i === (operatorList.length - 1)) {
pos.push({ [operatorNew]: [] });
}
}
return obj;
};
/**
* toTraversalFilterObject takes input contained in the proto structure defined in resource_base proto
* and converts it into Object understandable by the underlying DB implementation in chassis-srv
* @param {*} input Original filter input object
* @param {*} obj converted filter objected passed recursively
* @param {*} operatorList operatorlist updated and passed recursively
*/
const toTraversalFilterObject = (input, obj, operatorList) => {
// since toObject method is called recursively we are not adding the typing to input parameter
let filters;
if (input && !_.isEmpty(input.filters)) {
filters = input.filters;
}
else {
filters = input;
}
// use operator provided in input
if (input.operator) {
filters.operator = input.operator;
}
// by default use 'and' operator if no operator is specified
if (filters && _.isArray(filters.filters) && !filters.operator) {
filters.operator = 'and';
}
if (!obj) {
obj = {};
}
if (_.isArray(filters.filters) && filters.filters.length > 0) {
let operatorValue;
if (typeof filters.operator === 'string' || filters.operator instanceof String) {
operatorValue = filters.operator;
}
else if (Number.isInteger(filters.operator)) {
operatorValue = filterOperatorMap.get(filters.operator);
}
const newOperator = `$${operatorValue}`;
if (operatorList && newOperator) {
// insert obj with new operator
obj = insertNewOpAndUpdateObj(obj, operatorList, newOperator);
operatorList.push(newOperator);
}
else {
operatorList = [newOperator];
obj[newOperator] = [];
}
// pass operatorList and obj recursively
obj = (0, exports.toTraversalFilterObject)(filters.filters, obj, operatorList);
}
else if (_.isArray(filters)) {
if (!operatorList) {
const operator = input.operator ? `$${input.operator}` : '$and';
operatorList = [operator];
}
if (_.isEmpty(obj)) {
const operator = input.operator ? `$${input.operator}` : '$and';
obj = { [operator]: [] };
}
for (const filterObj of filters) {
obj = (0, exports.toTraversalFilterObject)(filterObj, obj, operatorList);
}
}
else if (filters.field && (filters.operation || filters.operation === 0) && filters.value != undefined) {
// object contains field, operation and value, update it on obj using convertFilterToObject()
if (!operatorList) {
const operator = input.operator ? `$${input.operator}` : '$and';
operatorList = [operator];
}
if (_.isEmpty(obj)) {
const operator = filters.operator ? `$${filters.operator}` : '$and';
obj = { [operator]: [] };
}
obj = convertFilterToObject(filters, obj, operatorList);
}
return obj;
};
exports.toTraversalFilterObject = toTraversalFilterObject;
/**
* Auto-casting reference value by using native function of arangoDB
*
* @param {string} key
* @param {object} value - raw value optional
* @return {object} interpreted value
*/
const autoCastKey = (key, value) => {
if (_.isDate(value)) { // Date
return `DATE_TIMESTAMP(v.${key})`;
}
return 'v.' + key;
};
exports.autoCastKey = autoCastKey;
/**
* Auto-casting raw data
*
* @param {object} value - raw value
* @returns {any} interpreted value
*/
const autoCastValue = (value) => {
if (_.isArray(value)) {
return value.map(value => value.toString());
}
if (_.isString(value)) { // String
return JSON.stringify(value);
}
if (_.isBoolean(value)) { // Boolean
return Boolean(value);
}
if (_.isNumber(value)) {
return _.toNumber(value);
}
if (long_1.default.isLong(value)) {
return value.toNumber();
}
if (_.isDate(value)) { // Date
return new Date(value);
}
return value;
};
exports.autoCastValue = autoCastValue;
/**
* Links children of filter together via a comparision operator.
* @param {any} filter
* @param {string} op comparision operator
* @return {any} query template string and bind variables
*/
const buildComparison = (filter, op, root) => {
const ele = _.map(filter, (e) => {
if (!_.isArray(e)) {
e = [e];
}
e = (0, exports.buildGraphFilter)(e, root);
return e.q;
});
let q = '( ';
for (let i = 0; i < ele.length; i += 1) {
if (i == ele.length - 1) {
q = `${q} ${ele[i]} )`;
}
else {
q = `${q} ${ele[i]} ${op} `;
}
}
return { q };
};
exports.buildComparison = buildComparison;
/**
* Auto-casting reference value by using native function of arangoDB
*
* @param {string} key
* @param {object} value - raw value optional
* @return {object} interpreted value
*/
const autoCastRootKey = (key, value) => {
if (_.isDate(value)) { // Date
return `DATE_TIMESTAMP(obj.${key})`;
}
return 'obj.' + key;
};
exports.autoCastRootKey = autoCastRootKey;
/**
* Creates a filter key, value.
* When the value is a string, boolean, number or date a equal comparision is created.
* Otherwise if the key corresponds to a known operator, the operator is constructed.
* @param {string} key
* @param {string|boolean|number|date|object} value
* @return {String} query template string
*/
const buildGraphField = (key, value, root) => {
let autoCastKeyFunction;
if (!root) {
autoCastKeyFunction = exports.autoCastKey;
}
else {
autoCastKeyFunction = exports.autoCastRootKey;
}
if (_.isString(value) || _.isBoolean(value) || _.isNumber(value || _.isDate(value))) {
return autoCastKeyFunction(key, value) + ' == ' + (0, exports.autoCastValue)(value);
}
if (!_.isNil(value.$eq)) {
return autoCastKeyFunction(key, value) + ' == ' + (0, exports.autoCastValue)(value.$eq);
}
if (value.$gt) {
return autoCastKeyFunction(key, value) + ' > ' + (0, exports.autoCastValue)(value.$gt);
}
if (value.$gte) {
return autoCastKeyFunction(key, value) + ' >= ' + (0, exports.autoCastValue)(value.$gte);
}
if (value.$lt) {
return autoCastKeyFunction(key, value) + ' < ' + (0, exports.autoCastValue)(value.$lt);
}
if (value.$lte) {
return autoCastKeyFunction(key, value) + ' <= ' + (0, exports.autoCastValue)(value.$lte);
}
if (!_.isNil(value.$ne)) {
return autoCastKeyFunction(key, value) + ' != ' + (0, exports.autoCastValue)(value.$ne);
}
if (value.$inVal) {
return (0, exports.autoCastValue)(value.$inVal) + ' IN ' + autoCastKeyFunction(key, value);
}
if (value.$in) {
if (_.isString(value.$in)) {
// if it is a field which should be an array
// (useful for querying within a document list-like attributen
return (0, exports.autoCastValue)(value.$in) + ' IN ' + autoCastKeyFunction(key);
}
// assuming it is a list of provided values
return autoCastKeyFunction(key, value) + ' IN ' + (0, exports.autoCastValue)(value.$in);
}
if (value.$nin) {
return autoCastKeyFunction(key, value) + ' NOT IN ' + (0, exports.autoCastValue)(value.$nin);
}
if (value.$iLike) {
// @param 'true' is for case insensitive
return 'LOWER(' + autoCastKeyFunction(key, value) + ') LIKE ' + (0, exports.autoCastValue)(value.$iLike.toLowerCase());
}
if (!_.isNil(value.$not)) {
const temp = (0, exports.buildGraphField)(key, value.$not, root);
return `!(${temp})`;
}
if (_.has(value, '$isEmpty')) {
// will always search for an empty string
return autoCastKeyFunction(key, '') + ' == ' + (0, exports.autoCastValue)('');
}
throw new Error(`unsupported operator ${_.keys(value)} in ${key}`);
};
exports.buildGraphField = buildGraphField;
/**
* Build ArangoDB query based on filter.
* @param {Object} filter key, value tree object
* @return {any} query template string and bind variables
*/
const buildGraphFilter = (filter, root) => {
if (!root) {
root = false;
}
if (filter.length > 0) {
let q = '';
let multipleFilters = false;
for (const eachFilter of filter) {
_.forEach(eachFilter, (value, key) => {
switch (key) {
case '$or':
if (!multipleFilters) {
if (_.isEmpty(value)) {
q = true;
}
else {
q = (0, exports.buildComparison)(value, '||', root).q;
}
multipleFilters = true;
}
else {
q = q + '&& ' + (0, exports.buildComparison)(value, '||', root).q;
}
break;
case '$and':
if (!multipleFilters) {
if (_.isEmpty(value)) {
q = false;
}
else {
q = (0, exports.buildComparison)(value, '&&', root).q;
}
multipleFilters = true;
}
else {
q = q + '&& ' + (0, exports.buildComparison)(value, '&&', root).q;
}
break;
default:
if (_.startsWith(key, '$')) {
throw new Error(`unsupported query operator ${key}`);
}
if (!multipleFilters) {
q = (0, exports.buildGraphField)(key, value, root);
multipleFilters = true;
}
else {
q = q + ' && ' + (0, exports.buildGraphField)(key, value, root);
}
break;
}
});
}
return { q };
}
};
exports.buildGraphFilter = buildGraphFilter;
/**
* Find's the list of entities from edge definition config depending on the direction
* recursively
* @param collection - root collection / start vertex
* @param edgeDefConfig - edge definition cofnig
* @param direction - direction OUTBOUND / INBOUND
* @param entitiesList - result of entities in the graph of edge definition config
*/
const recursiveFindEntities = (collection, edgeDefConfig, direction, entitiesList) => {
if (entitiesList.includes(collection)) {
return;
}
entitiesList.push(collection);
let items = [];
if (direction === graph_1.Options_Direction.OUTBOUND) {
items = edgeDefConfig.filter(col => col.from === collection);
}
else if (direction === graph_1.Options_Direction.INBOUND) {
items = edgeDefConfig.filter(col => col.to === collection);
}
for (const item of items) {
if (direction === graph_1.Options_Direction.OUTBOUND) {
(0, exports.recursiveFindEntities)(item.to, edgeDefConfig, direction, entitiesList);
}
else if (direction === graph_1.Options_Direction.INBOUND) {
(0, exports.recursiveFindEntities)(item.from, edgeDefConfig, direction, entitiesList);
}
}
return entitiesList;
};
exports.recursiveFindEntities = recursiveFindEntities;
/**
* Build limit and offset filters.
* @param {limit} limit
* @param {offset} offset
* @return {String} string limit filter
*/
const buildGraphLimiter = (limit, offset) => {
// LIMIT count
// LIMIT offset, count
if (!limit) {
limit = 1000;
}
if (!_.isNil(limit)) {
if (!_.isNil(offset)) {
return `LIMIT ${offset}, ${limit}`;
}
return `LIMIT ${limit}`;
}
return '';
};
exports.buildGraphLimiter = buildGraphLimiter;
/**
* Build sort filter.
* @param {Object} sort sort options
* @return {any} template sort string
*/
const buildGraphSorter = (sortList) => {
if (_.isNil(sortList) || _.isEmpty(sortList)) {
return '';
}
const sort = _.mapKeys(sortList, (value, key) => {
return (0, exports.autoCastRootKey)(key);
});
let sortKeysOrder = '';
let i = 1;
const objLength = Object.keys(sort).length;
for (const key in sort) {
if (objLength == i) {
// Do not append ',' for the last element
sortKeysOrder = `${sortKeysOrder} ${key} ${sort[key]} `;
}
else {
sortKeysOrder = `${sortKeysOrder} ${key} ${sort[key]},`;
}
i += 1;
}
return 'SORT ' + sortKeysOrder;
};
exports.buildGraphSorter = buildGraphSorter;
const createGraphsAssociationFilter = (filters, direction, traversalCollectionName, edgeDefConfig, filter) => {
let filterObj = [];
let filteredEntities = []; // used to find difference from graph edgeDefConfig and add missing entities to custom filter
let completeEntities = [];
let rootEntityFilter;
// convert the filter from proto structure (field, operation, value and operand) to {field: value } mapping
if (filters && !_.isEmpty(filters)) {
if (!_.isArray(filters)) {
filters = [filters];
}
for (const eachFilter of filters) {
if (eachFilter.entity && eachFilter.entity === traversalCollectionName) {
rootEntityFilter = (0, exports.toTraversalFilterObject)(eachFilter);
continue;
}
const traversalFilterObj = (0, exports.toTraversalFilterObject)(eachFilter);
if (eachFilter.entity && eachFilter.entity != traversalCollectionName) {
filteredEntities.push(eachFilter.entity);
traversalFilterObj.entity = eachFilter.entity;
}
else if (eachFilter.edge) {
// depending on direction
const entityConnectedToEdge = edgeDefConfig.filter(e => e.collection === eachFilter.edge);
if (entityConnectedToEdge?.length === 1) {
if (direction === graph_1.Options_Direction.OUTBOUND) {
filteredEntities.push(entityConnectedToEdge[0].to);
}
else if (direction === graph_1.Options_Direction.INBOUND) {
filteredEntities.push(entityConnectedToEdge[0].from);
}
}
traversalFilterObj.edge = eachFilter.edge;
}
filterObj.push(traversalFilterObj);
}
}
if (!_.isArray(filterObj)) {
filterObj = [filterObj];
}
if (filterObj?.length > 0) {
completeEntities = (0, exports.recursiveFindEntities)(traversalCollectionName, edgeDefConfig, direction, []);
}
// construct AQL custom filter based on filterObj using buildFilter api
let customFilter = '';
let rootCollectionFilter = '';
if (filterObj && filterObj.length > 0) {
for (let i = 0; i < filterObj.length; i++) {
let entity = '';
let edge = '';
if (filterObj[i].entity) {
entity = filterObj[i].entity;
delete filterObj[i].entity;
}
else if (filterObj[i].edge) {
edge = filterObj[i].edge;
delete filterObj[i].edge;
}
let filterString = (0, exports.buildGraphFilter)([filterObj[i]]).q;
if (typeof filterString === 'string' &&
filterString.startsWith('(') && filterString.endsWith(')')) {
if (entity) {
filterString = filterString.substring(0, 1) + ` v._id LIKE "${entity}%" && ` + filterString.substring(1);
if (traversalCollectionName && entity === traversalCollectionName) {
rootCollectionFilter = filterString;
}
}
else if (edge) {
filterString = filterString.substring(0, 1) + ` e._id LIKE "${edge}%" && ` + filterString.substring(1);
}
}
if (i === filterObj.length - 1) {
customFilter = customFilter + filterString;
}
else {
customFilter = customFilter + filterString + ' || ';
}
}
}
if (customFilter) {
filter = filter + ` FILTER ${customFilter}`;
// add missing entities to FILTER query
filteredEntities = filteredEntities.sort();
completeEntities = completeEntities.sort();
if (!_.isEqual(filteredEntities, completeEntities)) {
for (const removeEntity of _.intersection(filteredEntities, completeEntities)) {
completeEntities.splice(completeEntities.indexOf(removeEntity), 1);
}
}
// AQL query for missing entities
if (completeEntities && completeEntities.length > 0) {
for (const missingEntity of completeEntities) {
filter = filter + ` || ( v._id LIKE "${missingEntity}%" )`;
}
}
}
return { rootEntityFilter, associationFilter: filter };
};
exports.createGraphsAssociationFilter = createGraphsAssociationFilter;
//# sourceMappingURL=utils.js.map