UNPKG

@restorecommerce/chassis-srv

Version:

Restore Commerce microservice chassis

588 lines 21.5 kB
import { clone, find, isEmptyish, isDate, isString, isBoolean, isNumber, isNullish, keys, forEachObj, startsWith, mapKeys, isDeepEqual, intersection } from 'remeda'; import Long from 'long'; import { Options_Direction, Options_Direction as Direction, } from '@restorecommerce/rc-grpc-clients/dist/generated-server/io/restorecommerce/graph.js'; 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 = 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 (Array.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 (Array.isArray(temp)) { temp.push({ [filter.field]: value }); } else { temp[operatorList[i]].push({ [filter.field]: value }); } } else if (filter.operation === 'neq' || filter.operation === 8) { if (Array.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 (Array.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 (Array.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 */ export const toTraversalFilterObject = (input, obj, operatorList) => { // since toObject method is called recursively we are not adding the typing to input parameter let filters; if (input && !isEmptyish(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 && Array.isArray(filters.filters) && !filters.operator) { filters.operator = 'and'; } if (!obj) { obj = {}; } if (Array.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 = toTraversalFilterObject(filters.filters, obj, operatorList); } else if (Array.isArray(filters)) { if (!operatorList) { const operator = input.operator ? `$${input.operator}` : '$and'; operatorList = [operator]; } if (isEmptyish(obj)) { const operator = input.operator ? `$${input.operator}` : '$and'; obj = { [operator]: [] }; } for (const filterObj of filters) { obj = 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 (isEmptyish(obj)) { const operator = filters.operator ? `$${filters.operator}` : '$and'; obj = { [operator]: [] }; } obj = convertFilterToObject(filters, obj, operatorList); } return obj; }; /** * Auto-casting reference value by using native function of arangoDB * * @param {string} key * @param {object} value - raw value optional * @return {object} interpreted value */ export const autoCastKey = (key, value) => { if (isDate(value)) { // Date return `DATE_TIMESTAMP(v.${key})`; } return 'v.' + key; }; /** * Auto-casting raw data * * @param {object} value - raw value * @returns {any} interpreted value */ export const autoCastValue = (value) => { if (Array.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 Number(value); } if (Long.isLong(value)) { return value.toNumber(); } if (isDate(value)) { // Date return new Date(value); } return value; }; /** * 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 */ export const buildComparison = (filter, op, root) => { const ele = filter.map((e) => { if (!Array.isArray(e)) { e = [e]; } e = 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 }; }; /** * Auto-casting reference value by using native function of arangoDB * * @param {string} key * @param {object} value - raw value optional * @return {object} interpreted value */ export const autoCastRootKey = (key, value) => { if (isDate(value)) { // Date return `DATE_TIMESTAMP(obj.${key})`; } return 'obj.' + key; }; /** * 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 */ export const buildGraphField = (key, value, root) => { let autoCastKeyFunction; if (!root) { autoCastKeyFunction = autoCastKey; } else { autoCastKeyFunction = autoCastRootKey; } if (isString(value) || isBoolean(value) || isNumber(value || isDate(value))) { return autoCastKeyFunction(key, value) + ' == ' + autoCastValue(value); } if (!isNullish(value.$eq)) { return autoCastKeyFunction(key, value) + ' == ' + autoCastValue(value.$eq); } if (value.$gt) { return autoCastKeyFunction(key, value) + ' > ' + autoCastValue(value.$gt); } if (value.$gte) { return autoCastKeyFunction(key, value) + ' >= ' + autoCastValue(value.$gte); } if (value.$lt) { return autoCastKeyFunction(key, value) + ' < ' + autoCastValue(value.$lt); } if (value.$lte) { return autoCastKeyFunction(key, value) + ' <= ' + autoCastValue(value.$lte); } if (!isNullish(value.$ne)) { return autoCastKeyFunction(key, value) + ' != ' + autoCastValue(value.$ne); } if (value.$inVal) { return 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 autoCastValue(value.$in) + ' IN ' + autoCastKeyFunction(key); } // assuming it is a list of provided values return autoCastKeyFunction(key, value) + ' IN ' + autoCastValue(value.$in); } if (value.$nin) { return autoCastKeyFunction(key, value) + ' NOT IN ' + autoCastValue(value.$nin); } if (value.$iLike) { // @param 'true' is for case insensitive return 'LOWER(' + autoCastKeyFunction(key, value) + ') LIKE ' + autoCastValue(value.$iLike.toLowerCase()); } if (!isNullish(value.$not)) { const temp = buildGraphField(key, value.$not, root); return `!(${temp})`; } if ('$isEmpty' in value) { // will always search for an empty string return autoCastKeyFunction(key, '') + ' == ' + autoCastValue(''); } throw new Error(`unsupported operator ${keys(value)} in ${key}`); }; /** * Build ArangoDB query based on filter. * @param {Object} filter key, value tree object * @return {any} query template string and bind variables */ export const buildGraphFilter = (filter, root) => { if (!root) { root = false; } if (filter.length > 0) { let q = ''; let multipleFilters = false; for (const eachFilter of filter) { forEachObj(eachFilter, (value, key) => { switch (key) { case '$or': if (!multipleFilters) { if (isEmptyish(value)) { q = true; } else { q = buildComparison(value, '||', root).q; } multipleFilters = true; } else { q = q + '&& ' + buildComparison(value, '||', root).q; } break; case '$and': if (!multipleFilters) { if (isEmptyish(value)) { q = false; } else { q = buildComparison(value, '&&', root).q; } multipleFilters = true; } else { q = q + '&& ' + buildComparison(value, '&&', root).q; } break; default: if (startsWith(key, '$')) { throw new Error(`unsupported query operator ${key}`); } if (!multipleFilters) { q = buildGraphField(key, value, root); multipleFilters = true; } else { q = q + ' && ' + buildGraphField(key, value, root); } break; } }); } return { q }; } }; /** * 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 */ export const recursiveFindEntities = (collection, edgeDefConfig, direction, entitiesList) => { if (entitiesList.includes(collection)) { return; } entitiesList.push(collection); let items = []; if (direction === Direction.OUTBOUND) { items = edgeDefConfig.filter((col) => col.from === collection); } else if (direction === Direction.INBOUND) { items = edgeDefConfig.filter((col) => col.to === collection); } for (const item of items) { if (direction === Direction.OUTBOUND) { recursiveFindEntities(item.to, edgeDefConfig, direction, entitiesList); } else if (direction === Direction.INBOUND) { recursiveFindEntities(item.from, edgeDefConfig, direction, entitiesList); } } return entitiesList; }; /** * Build limit and offset filters. * @param {limit} limit * @param {offset} offset * @return {String} string limit filter */ export const buildGraphLimiter = (limit, offset) => { // LIMIT count // LIMIT offset, count if (!limit) { limit = 1000; } if (!isNullish(limit)) { if (!isNullish(offset)) { return `LIMIT ${offset}, ${limit}`; } return `LIMIT ${limit}`; } return ''; }; /** * Build sort filter. * @param {Object} sort sort options * @return {any} template sort string */ export const buildGraphSorter = (sortList) => { if (isNullish(sortList) || isEmptyish(sortList)) { return ''; } const sort = mapKeys(sortList, (key, value) => { return 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; }; export 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 && !isEmptyish(filters)) { if (!Array.isArray(filters)) { filters = [filters]; } for (const eachFilter of filters) { if (eachFilter.entity && eachFilter.entity === traversalCollectionName) { rootEntityFilter = toTraversalFilterObject(eachFilter); continue; } const traversalFilterObj = 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 === Options_Direction.OUTBOUND) { filteredEntities.push(entityConnectedToEdge[0].to); } else if (direction === Options_Direction.INBOUND) { filteredEntities.push(entityConnectedToEdge[0].from); } } traversalFilterObj.edge = eachFilter.edge; } filterObj.push(traversalFilterObj); } } if (!Array.isArray(filterObj)) { filterObj = [filterObj]; } if (filterObj?.length > 0) { completeEntities = 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 = 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 (!isDeepEqual(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 }; }; export const toArray = (t) => Array.isArray(t) || isNullish(t) ? t : [t]; export class LoggedError extends Error { constructor(logger, msg, ...meta) { logger?.error(msg, ...meta); super(msg); } } //# sourceMappingURL=utils.js.map