UNPKG

popoto

Version:

Graph based search interface for Neo4j database.

672 lines (560 loc) 26.4 kB
import provider from "../provider/provider"; import dataModel from "../datamodel/dataModel"; var query = {}; /** * Define the number of results displayed in result list. */ query.MAX_RESULTS_COUNT = 100; // query.RESULTS_PAGE_NUMBER = 1; query.VALUE_QUERY_LIMIT = 100; query.USE_PARENT_RELATION = false; query.USE_RELATION_DIRECTION = true; query.RETURN_LABELS = false; query.COLLECT_RELATIONS_WITH_VALUES = false; query.prefilter = ""; query.prefilterParameters = {}; query.applyPrefilters = function (queryStructure) { queryStructure.statement = query.prefilter + queryStructure.statement; Object.keys(query.prefilterParameters).forEach(function (key) { queryStructure.parameters[key] = query.prefilterParameters[key]; }); return queryStructure; }; /** * Immutable constant object to identify Neo4j internal ID */ query.NEO4J_INTERNAL_ID = Object.freeze({queryInternalName: "NEO4JID"}); /** * Function used to filter returned relations * return false if the result should be filtered out. * * @param d relation returned object * @returns {boolean} */ query.filterRelation = function (d) { return true; }; /** * Generate the query to count nodes of a label. * If the label is defined as distinct in configuration the query will count only distinct values on constraint attribute. */ query.generateTaxonomyCountQuery = function (label) { var constraintAttr = provider.node.getConstraintAttribute(label); var whereElements = []; var predefinedConstraints = provider.node.getPredefinedConstraints(label); predefinedConstraints.forEach(function (predefinedConstraint) { whereElements.push(predefinedConstraint.replace(new RegExp("\\$identifier", 'g'), "n")); }); if (constraintAttr === query.NEO4J_INTERNAL_ID) { return "MATCH (n:`" + label + "`)" + ((whereElements.length > 0) ? " WHERE " + whereElements.join(" AND ") : "") + " RETURN count(DISTINCT ID(n)) as count" } else { return "MATCH (n:`" + label + "`)" + ((whereElements.length > 0) ? " WHERE " + whereElements.join(" AND ") : "") + " RETURN count(DISTINCT n." + constraintAttr + ") as count" } }; query.generateNegativeQueryElements = function () { var whereElements = []; var parameters = {}; var negativeNodes = dataModel.nodes.filter(function (n) { return n.isNegative === true; }); negativeNodes.forEach( function (n) { if (provider.node.getGenerateNegativeNodeValueConstraints(n) !== undefined) { var custom = provider.node.getGenerateNegativeNodeValueConstraints(n)(n); whereElements = whereElements.concat(custom.whereElements); for (var prop in custom.parameters) { if (custom.parameters.hasOwnProperty(prop)) { parameters[prop] = custom.parameters[prop]; } } } else { var linksToRoot = query.getLinksToRoot(n, dataModel.links); var i = linksToRoot.length - 1; var statement = "(NOT exists("; statement += "(" + dataModel.getRootNode().internalLabel + ")"; while (i >= 0) { var l = linksToRoot[i]; var targetNode = l.target; if (targetNode.isParentRelReverse === true && query.USE_RELATION_DIRECTION === true) { statement += "<-"; } else { statement += "-"; } statement += "[:`" + l.label + "`]"; if (targetNode.isParentRelReverse !== true && query.USE_RELATION_DIRECTION === true) { statement += "->"; } else { statement += "-"; } if (targetNode === n && targetNode.value !== undefined && targetNode.value.length > 0) { var constraintAttr = provider.node.getConstraintAttribute(targetNode.label); var paramName = targetNode.internalLabel + "_" + constraintAttr; if (targetNode.value.length > 1) { for (var pid = 0; pid < targetNode.value.length; pid++) { parameters[paramName + "_" + pid] = targetNode.value[pid].attributes[constraintAttr]; } statement += "(:`" + targetNode.label + "`{" + constraintAttr + ":$x$})"; } else { parameters[paramName] = targetNode.value[0].attributes[constraintAttr]; statement += "(:`" + targetNode.label + "`{" + constraintAttr + ":$" + paramName + "})"; } } else { statement += "(:`" + targetNode.label + "`)"; } i--; } statement += "))"; if (n.value !== undefined && n.value.length > 1) { var cAttr = provider.node.getConstraintAttribute(n.label); var pn = n.internalLabel + "_" + cAttr; for (var nid = 0; nid < targetNode.value.length; nid++) { whereElements.push(statement.replace("$x$", "$" + pn + "_" + nid)); } } else { whereElements.push(statement); } } } ); return { "whereElements": whereElements, "parameters": parameters }; }; /** * Generate Cypher query match and where elements from root node, selected node and a set of the graph links. * * @param rootNode root node in the graph. * @param selectedNode graph target node. * @param links list of links subset of the graph. * @returns {{matchElements: Array, whereElements: Array}} list of match and where elements. * @param isConstraintNeeded (used only for relation query) * @param useCustomConstraints define whether to use the custom constraints (actually it is used only for results) */ query.generateQueryElements = function (rootNode, selectedNode, links, isConstraintNeeded, useCustomConstraints) { var matchElements = []; var whereElements = []; var relationElements = []; var returnElements = []; var parameters = {}; var rootPredefinedConstraints = provider.node.getPredefinedConstraints(rootNode.label); rootPredefinedConstraints.forEach(function (predefinedConstraint) { whereElements.push(predefinedConstraint.replace(new RegExp("\\$identifier", 'g'), rootNode.internalLabel)); }); matchElements.push("(" + rootNode.internalLabel + ":`" + rootNode.label + "`)"); // Generate root node match element if (isConstraintNeeded || rootNode.immutable) { var rootValueConstraints = query.generateNodeValueConstraints(rootNode, useCustomConstraints); whereElements = whereElements.concat(rootValueConstraints.whereElements); for (var param in rootValueConstraints.parameters) { if (rootValueConstraints.parameters.hasOwnProperty(param)) { parameters[param] = rootValueConstraints.parameters[param]; } } } var relId = 0; // Generate match elements for each links links.forEach(function (l) { var sourceNode = l.source; var targetNode = l.target; var sourceRel = ""; var targetRel = ""; if (!query.USE_RELATION_DIRECTION) { sourceRel = "-"; targetRel = "-"; } else { if (targetNode.isParentRelReverse === true) { sourceRel = "<-"; targetRel = "-"; } else { sourceRel = "-"; targetRel = "->"; } } var relIdentifier = "r" + relId++; relationElements.push(relIdentifier); var predefinedConstraints = provider.node.getPredefinedConstraints(targetNode.label); predefinedConstraints.forEach(function (predefinedConstraint) { whereElements.push(predefinedConstraint.replace(new RegExp("\\$identifier", 'g'), targetNode.internalLabel)); }); if (query.COLLECT_RELATIONS_WITH_VALUES && targetNode === selectedNode) { returnElements.push("COLLECT(" + relIdentifier + ") AS incomingRels"); } var sourceLabelStatement = ""; if (!useCustomConstraints || provider.node.getGenerateNodeValueConstraints(sourceNode) === undefined) { sourceLabelStatement = ":`" + sourceNode.label + "`"; } var targetLabelStatement = ""; if (!useCustomConstraints || provider.node.getGenerateNodeValueConstraints(targetNode) === undefined) { targetLabelStatement = ":`" + targetNode.label + "`"; } matchElements.push("(" + sourceNode.internalLabel + sourceLabelStatement + ")" + sourceRel + "[" + relIdentifier + ":`" + l.label + "`]" + targetRel + "(" + targetNode.internalLabel + targetLabelStatement + ")"); if (targetNode !== selectedNode && (isConstraintNeeded || targetNode.immutable)) { var nodeValueConstraints = query.generateNodeValueConstraints(targetNode, useCustomConstraints); whereElements = whereElements.concat(nodeValueConstraints.whereElements); for (var param in nodeValueConstraints.parameters) { if (nodeValueConstraints.parameters.hasOwnProperty(param)) { parameters[param] = nodeValueConstraints.parameters[param]; } } } }); return { "matchElements": matchElements, "whereElements": whereElements, "relationElements": relationElements, "returnElements": returnElements, "parameters": parameters }; }; /** * Generate the where and parameter statements for the nodes with value * * @param node the node to generate value constraints * @param useCustomConstraints define whether to use custom generation in popoto config */ query.generateNodeValueConstraints = function (node, useCustomConstraints) { if (useCustomConstraints && provider.node.getGenerateNodeValueConstraints(node) !== undefined) { return provider.node.getGenerateNodeValueConstraints(node)(node); } else { var parameters = {}, whereElements = []; if (node.value !== undefined && node.value.length > 0) { var constraintAttr = provider.node.getConstraintAttribute(node.label); var paramName; if (constraintAttr === query.NEO4J_INTERNAL_ID) { paramName = node.internalLabel + "_internalID"; } else { paramName = node.internalLabel + "_" + constraintAttr; } if (node.value.length > 1) { // Generate IN constraint parameters[paramName] = []; node.value.forEach(function (value) { var constraintValue; if (constraintAttr === query.NEO4J_INTERNAL_ID) { constraintValue = value.internalID } else { constraintValue = value.attributes[constraintAttr]; } parameters[paramName].push(constraintValue); }); if (constraintAttr === query.NEO4J_INTERNAL_ID) { whereElements.push("ID(" + node.internalLabel + ") IN " + "$" + paramName); } else { whereElements.push(node.internalLabel + "." + constraintAttr + " IN " + "$" + paramName); } } else { // Generate = constraint if (constraintAttr === query.NEO4J_INTERNAL_ID) { parameters[paramName] = node.value[0].internalID; } else { parameters[paramName] = node.value[0].attributes[constraintAttr]; } var operator = "="; if (constraintAttr === query.NEO4J_INTERNAL_ID) { whereElements.push("ID(" + node.internalLabel + ") " + operator + " " + "$" + paramName); } else { whereElements.push(node.internalLabel + "." + constraintAttr + " " + operator + " " + "$" + paramName); } } } return { parameters: parameters, whereElements: whereElements } } }; /** * Filter links to get only paths from root to leaf containing a value or being the selectedNode. * All other paths in the graph containing no value are ignored. * * @param rootNode root node of the graph. * @param targetNode node in the graph target of the query. * @param initialLinks list of links representing the graph to filter. * @returns {Array} list of relevant links. */ query.getRelevantLinks = function (rootNode, targetNode, initialLinks) { var links = initialLinks.slice(); var finalLinks = []; // Filter all links to keep only those containing a value or being the selected node. // Negatives nodes are handled separately. var filteredLinks = links.filter(function (l) { return l.target === targetNode || ((l.target.value !== undefined && l.target.value.length > 0) && (!l.target.isNegative === true)); }); // All the filtered links are removed from initial links list. filteredLinks.forEach(function (l) { links.splice(links.indexOf(l), 1); }); // Then all the intermediate links up to the root node are added to get only the relevant links. filteredLinks.forEach(function (fl) { var sourceNode = fl.source; var search = true; while (search) { var intermediateLink = null; links.forEach(function (l) { if (l.target === sourceNode) { intermediateLink = l; } }); if (intermediateLink === null) { // no intermediate links needed search = false } else { if (intermediateLink.source === rootNode) { finalLinks.push(intermediateLink); links.splice(links.indexOf(intermediateLink), 1); search = false; } else { finalLinks.push(intermediateLink); links.splice(links.indexOf(intermediateLink), 1); sourceNode = intermediateLink.source; } } } }); return filteredLinks.concat(finalLinks); }; /** * Get the list of link defining the complete path from node to root. * All other links are ignored. * * @param node The node where to start in the graph. * @param links */ query.getLinksToRoot = function (node, links) { var pathLinks = []; var targetNode = node; while (targetNode !== dataModel.getRootNode()) { var nodeLink; for (var i = 0; i < links.length; i++) { var link = links[i]; if (link.target === targetNode) { nodeLink = link; break; } } if (nodeLink) { pathLinks.push(nodeLink); targetNode = nodeLink.source; } } return pathLinks; }; /** * Generate a Cypher query to retrieve the results matching the current graph. * * @param isGraph * @returns {{statement: string, parameters: (*|{})}} */ query.generateResultQuery = function (isGraph) { var rootNode = dataModel.getRootNode(); var negativeElements = query.generateNegativeQueryElements(); var queryElements = query.generateQueryElements(rootNode, rootNode, query.getRelevantLinks(rootNode, rootNode, dataModel.links), true, true); var queryMatchElements = queryElements.matchElements, queryWhereElements = queryElements.whereElements.concat(negativeElements.whereElements), queryRelationElements = queryElements.relationElements, queryReturnElements = [], queryEndElements = [], queryParameters = queryElements.parameters; for (var prop in negativeElements.parameters) { if (negativeElements.parameters.hasOwnProperty(prop)) { queryParameters[prop] = negativeElements.parameters[prop]; } } // Sort results by specified attribute var resultOrderByAttribute = provider.node.getResultOrderByAttribute(rootNode.label); if (resultOrderByAttribute !== undefined && resultOrderByAttribute !== null) { var sorts = []; var order = provider.node.isResultOrderAscending(rootNode.label); var orders = []; if (Array.isArray(order)) { orders = order.map(function (v) { return v ? "ASC" : "DESC"; }); } else { orders.push(order ? "ASC" : "DESC"); } if (Array.isArray(resultOrderByAttribute)) { sorts = resultOrderByAttribute.map(function (ra) { var index = resultOrderByAttribute.indexOf(ra); if (index < orders.length) { return ra + " " + orders[index]; } else { return ra + " " + orders[orders.length - 1]; } }) } else { sorts.push(resultOrderByAttribute + " " + orders[0]); } queryEndElements.push("ORDER BY " + sorts.join(", ")); } queryEndElements.push("LIMIT " + query.MAX_RESULTS_COUNT); if (isGraph) { // Only return relations queryReturnElements.push(rootNode.internalLabel); queryRelationElements.forEach( function (el) { queryReturnElements.push(el); } ); } else { var resultAttributes = provider.node.getReturnAttributes(rootNode.label); queryReturnElements = resultAttributes.map(function (attribute) { if (attribute === query.NEO4J_INTERNAL_ID) { return "ID(" + rootNode.internalLabel + ") AS " + query.NEO4J_INTERNAL_ID.queryInternalName; } else { return rootNode.internalLabel + "." + attribute + " AS " + attribute; } }); if (query.RETURN_LABELS === true) { var element = "labels(" + rootNode.internalLabel + ")"; if (resultAttributes.indexOf("labels") < 0) { element = element + " AS labels"; } queryReturnElements.push(element); } } var queryStatement = "MATCH " + queryMatchElements.join(", ") + ((queryWhereElements.length > 0) ? " WHERE " + queryWhereElements.join(" AND ") : "") + " RETURN DISTINCT " + queryReturnElements.join(", ") + " " + queryEndElements.join(" "); // Filter the query if defined in config var queryStructure = provider.node.filterResultQuery(rootNode.label, { statement: queryStatement, matchElements: queryMatchElements, whereElements: queryWhereElements, withElements: [], returnElements: queryReturnElements, endElements: queryEndElements, parameters: queryParameters }); return query.applyPrefilters(queryStructure); }; /** * Generate a cypher query to the get the node count, set as parameter matching the current graph. * * @param countedNode the counted node * @returns {string} the node count cypher query */ query.generateNodeCountQuery = function (countedNode) { var negativeElements = query.generateNegativeQueryElements(); var queryElements = query.generateQueryElements(dataModel.getRootNode(), countedNode, query.getRelevantLinks(dataModel.getRootNode(), countedNode, dataModel.links), true, true); var queryMatchElements = queryElements.matchElements, queryWhereElements = queryElements.whereElements.concat(negativeElements.whereElements), queryReturnElements = [], queryEndElements = [], queryParameters = queryElements.parameters; for (var prop in negativeElements.parameters) { if (negativeElements.parameters.hasOwnProperty(prop)) { queryParameters[prop] = negativeElements.parameters[prop]; } } var countAttr = provider.node.getConstraintAttribute(countedNode.label); if (countAttr === query.NEO4J_INTERNAL_ID) { queryReturnElements.push("count(DISTINCT ID(" + countedNode.internalLabel + ")) as count"); } else { queryReturnElements.push("count(DISTINCT " + countedNode.internalLabel + "." + countAttr + ") as count"); } var queryStatement = "MATCH " + queryMatchElements.join(", ") + ((queryWhereElements.length > 0) ? " WHERE " + queryWhereElements.join(" AND ") : "") + " RETURN " + queryReturnElements.join(", "); // Filter the query if defined in config var queryStructure = provider.node.filterNodeCountQuery(countedNode, { statement: queryStatement, matchElements: queryMatchElements, whereElements: queryWhereElements, returnElements: queryReturnElements, endElements: queryEndElements, parameters: queryParameters }); return query.applyPrefilters(queryStructure); }; /** * Generate a Cypher query from the graph model to get all the possible values for the targetNode element. * * @param targetNode node in the graph to get the values. * @returns {string} the query to execute to get all the values of targetNode corresponding to the graph. */ query.generateNodeValueQuery = function (targetNode) { var negativeElements = query.generateNegativeQueryElements(); var rootNode = dataModel.getRootNode(); var queryElements = query.generateQueryElements(rootNode, targetNode, query.getRelevantLinks(rootNode, targetNode, dataModel.links), true, false); var queryMatchElements = queryElements.matchElements, queryWhereElements = queryElements.whereElements.concat(negativeElements.whereElements), queryReturnElements = [], queryEndElements = [], queryParameters = queryElements.parameters; for (var prop in negativeElements.parameters) { if (negativeElements.parameters.hasOwnProperty(prop)) { queryParameters[prop] = negativeElements.parameters[prop]; } } // Sort results by specified attribute var valueOrderByAttribute = provider.node.getValueOrderByAttribute(targetNode.label); if (valueOrderByAttribute) { var order = provider.node.isValueOrderAscending(targetNode.label) ? "ASC" : "DESC"; queryEndElements.push("ORDER BY " + valueOrderByAttribute + " " + order); } queryEndElements.push("LIMIT " + query.VALUE_QUERY_LIMIT); var resultAttributes = provider.node.getReturnAttributes(targetNode.label); var constraintAttribute = provider.node.getConstraintAttribute(targetNode.label); for (var i = 0; i < resultAttributes.length; i++) { if (resultAttributes[i] === query.NEO4J_INTERNAL_ID) { queryReturnElements.push("ID(" + targetNode.internalLabel + ") AS " + query.NEO4J_INTERNAL_ID.queryInternalName); } else { queryReturnElements.push(targetNode.internalLabel + "." + resultAttributes[i] + " AS " + resultAttributes[i]); } } // Add count return attribute on root node var rootConstraintAttr = provider.node.getConstraintAttribute(rootNode.label); if (rootConstraintAttr === query.NEO4J_INTERNAL_ID) { queryReturnElements.push("count(DISTINCT ID(" + rootNode.internalLabel + ")) AS count"); } else { queryReturnElements.push("count(DISTINCT " + rootNode.internalLabel + "." + rootConstraintAttr + ") AS count"); } if (query.COLLECT_RELATIONS_WITH_VALUES) { queryElements.returnElements.forEach(function (re) { queryReturnElements.push(re); }); } var queryStatement = "MATCH " + queryMatchElements.join(", ") + ((queryWhereElements.length > 0) ? " WHERE " + queryWhereElements.join(" AND ") : "") + " RETURN " + queryReturnElements.join(", ") + " " + queryEndElements.join(" "); // Filter the query if defined in config var queryStructure = provider.node.filterNodeValueQuery(targetNode, { statement: queryStatement, matchElements: queryMatchElements, whereElements: queryWhereElements, returnElements: queryReturnElements, endElements: queryEndElements, parameters: queryParameters }); return query.applyPrefilters(queryStructure); }; /** * Generate a Cypher query to retrieve all the relation available for a given node. * * @param targetNode * @returns {string} */ query.generateNodeRelationQuery = function (targetNode) { var linksToRoot = query.getLinksToRoot(targetNode, dataModel.links); var queryElements = query.generateQueryElements(dataModel.getRootNode(), targetNode, linksToRoot, false, false); var queryMatchElements = queryElements.matchElements, queryWhereElements = queryElements.whereElements, queryReturnElements = [], queryEndElements = [], queryParameters = queryElements.parameters; var rel = query.USE_RELATION_DIRECTION ? "->" : "-"; queryMatchElements.push("(" + targetNode.internalLabel + ":`" + targetNode.label + "`)-[r]" + rel + "(x)"); queryReturnElements.push("type(r) AS label"); if (query.USE_PARENT_RELATION) { queryReturnElements.push("head(labels(x)) AS target"); } else { queryReturnElements.push("last(labels(x)) AS target"); } queryReturnElements.push("count(r) AS count"); queryEndElements.push("ORDER BY count(r) DESC"); var queryStatement = "MATCH " + queryMatchElements.join(", ") + ((queryWhereElements.length > 0) ? " WHERE " + queryWhereElements.join(" AND ") : "") + " RETURN " + queryReturnElements.join(", ") + " " + queryEndElements.join(" "); // Filter the query if defined in config var queryStructure = provider.node.filterNodeRelationQuery(targetNode, { statement: queryStatement, matchElements: queryMatchElements, whereElements: queryWhereElements, returnElements: queryReturnElements, endElements: queryEndElements, parameters: queryParameters }); return query.applyPrefilters(queryStructure); }; export default query