UNPKG

@restorecommerce/chassis-srv

Version:
621 lines 23.6 kB
"use strict"; 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