UNPKG

neo4jkb

Version:

A graph knowledge base implemented in neo4j.

1,248 lines (1,134 loc) 53.8 kB
// dependencies var _ = require('lomath') var Promise = require('bluebird') var cons = require('./constrain') var parse = require('./parse') /** * This is the exported wrapper function: init the necessary params, then return the KB methods. The fields are set to neo4j defaults if not supplied. * @private * @param {JSON} options An optional JSON with the fields {NEO4J_AUTH: 'neo4j:neo4j', NEO4J_ADDR: 'localhost:7474'} containing username:password, the host address, and port number for neo4j * @return {Function} The KB functions. */ /* istanbul ignore next */ function Neo4jKB(options) { options = options || {}; if (!_.has(options, 'NEO4J_AUTH')) { throw new Error("You must at least supply a NEO4J_AUTH: '<username>:<password>' JSON argument.") }; this.options = options; // setting the exportable functions this.query = require('./query')(options) // the KB builders this.literalize = literalize this.addNode = addNode this.getNode = getNode this.addEdge = addEdge this.getEdge = getEdge this.pull = pull this.get = get this.push = push this.add = add // KB constraint this.cons = cons // the data parsers _.assign(this, parse) return this } module.exports = Neo4jKB ///////////// // Helpers // ///////////// /** * Parses prop JSON into a 'literal map' string as required by neo4j error (unable to user parameter map in MATCH). * @private * @param {JSON} [prop] Object to parse. * @param {string} [prop] Optional name for prop to use. * @return {string} The result literal map string. Empty string is prop is falsy. * * @example * var prop = {name: 'A', hash_by: 'name'} * literalizeProp(prop) * // => {name: {prop}.name, hash_by: {prop}.hash_by, hash: {prop}.hash} * * literalizeProp(prop, 'propA') * // => {name: {propA}.name, hash_by: {propA}.hash_by, hash: {propA}.hash} * */ /* istanbul ignore next */ function literalizeProp(prop, propName) { if (!prop) { return '' }; propName = propName || 'prop' var litArr = _.map(prop, function(v, k) { return k + ': {' + propName + '}.' + k }) return ' {' + litArr.join(', ') + '}' } /** * Generalization of literalizeProp. Parses a propDistLabel array of prop-label or dist-label into a 'literal map' string as required by neo4j error (unable to user parameter map in MATCH). On can pass the name of the identifier too. * Beware: in the query-param pair, the param prop JSON must use the key 'prop_<identifier>'. E.g. query param pair: * @param {Array} propDistLabel Array containing pair of (JSON)prop-(string)label or (string)dist-(string)label. * @param {string} [name='a'] Name of the identifier. * @return {string} The result literal map string. * * @example * var prop = {name: 'nodeName'} * var label = 'alpha' * var labelArr = ['alpha', 'beta'] * var dist = '*0..2' * * // for node * '(' + literalize([prop, label]) + ')' * // => (a:alpha {name: {prop_a}.name}) * * // for node with multiple labels; custom identifier name 'b' * // Note that the param must use the same key 'prop_b' too * var nodeStr = '(' + literalize([prop, labelArr], 'b') + ')' * // => (b:alpha:beta {name: {prop_b}.name}) * // to make the query param pair: * var query = `CREATE ${nodeStr} RETURN b` * var param = { prop_b: prop } * qpPair = [query, param] * // make the query: * KB.query(qpPair) * * // for edge, custom identifier name 'e' * // Note that the param must use the same key 'prop_e' too * '[' + literalize([prop, label], 'e') + ']' * // => [e:alpha {name: {prop_e}.name}] * * // for edge with dist and label, custom identifier name 'e' * '[' + literalize([dist, label], 'e') + ']' * // => [e:alpha *0..2] * */ function literalize(propDistLabel, name) { name = name || 'a' var part = sortPropLabel(propDistLabel), propDist = _.get(part, '0.0'), prop = _.isPlainObject(propDist) ? propDist : undefined, prop = literalizeProp(prop, 'prop_'+name), dist = KB.cons.stringifyDist(propDist), Label = _.get(part, '1.0'), LabelStr = KB.cons.stringifyLabel(Label, name); return `${name}${LabelStr}${prop}${dist}` } /** * Sort the array of prop-Label pair given with (propLabel), or ([propLabel]), where each is optional, into [[prop], [Label]]. Prop can also be a Dist string (startsWith '*') for edge. * @private * @param {Array} propLabel Together in an array. Either is optional. * @return {Array} Of sorted [[prop], [Label]], either can be empty. */ /* istanbul ignore next */ function sortPropLabel(propLabel) { return _.partition(_.flatten(arguments), function(arg) { // catch prop JSON, or *Dist string for edge return _.isPlainObject(arg) || _.startsWith(arg, '*') }) } /** * Takes a parsed query-param pair Array, scan for '[e:l1:l2...]', split into a copy of the same pair for each label, with that label only. * @private * @param {Array} qpPair query-param pair array, parsed for query. * @param {Boolean} relaxELabel=false A boolean to relax the contraint where E must be labeled. Pass true to relax. * @return {Array} Array of qpPair(s). * * @example * var propA = {name: 'A', hash_by: 'name'} * var propB = {name: 'B', hash_by: 'name'} * var labelA = 'alpha' * var labelB = 'alpha' * * var propE = {name: 'lexicography', hash_by: 'name'} * KB.cons.legalize(propE) * var labelE = ['next', 'after'] * * var qpPair = pushEdge([propA, labelA], [propE, labelE], [propB, labelB]); * console.log(splitLabelE(qpPair)) * // [ * // [ 'MATCH (a:alpha {name: {propA}.name, hash_by: {propA}.hash_by}), (b:alpha {name: {propB}.name, hash_by: {propB}.hash_by}) MERGE (a)-[e:next {hash: {propE}.hash}]->(b) ON CREATE SET e = {propE}, e.created_by={propE}.updated_by, e.created_when={propE}.updated_when ON MATCH SET e += {propE} RETURN e', { propA: [Object], propB: [Object], propE: [Object] } ], * // [ 'MATCH (a:alpha {name: {propA}.name, hash_by: {propA}.hash_by}), (b:alpha {name: {propB}.name, hash_by: {propB}.hash_by}) MERGE (a)-[e:after {hash: {propE}.hash}]->(b) ON CREATE SET e = {propE}, e.created_by={propE}.updated_by, e.created_when={propE}.updated_when ON MATCH SET e += {propE} RETURN e', { propA: [Object], propB: [Object], propE: [Object] } ] * // ] * */ /* istanbul ignore next */ function splitLabelE(qpPair) { var query = _.get(qpPair, 0), param = _.get(qpPair, 1) var relaxELabel = false; // ensure the query is MATCH without MERGE if (query.match(/^\s*MATCH\s+/i) && !query.match(/\s*MERGE\s+/i)) { relaxELabel = true; }; var matchELabel = query.match(/(?:\[e)(\:\S+)(\s+)/); // if no ELabel if (!matchELabel) { // if is relaxed, can be empty if (relaxELabel) { return [qpPair] } else { // check if the query is adding edge or node var addingEdge = query.match(/(\[e)/); if (addingEdge) { throw new Error("Edges (relationships) must have label(s).") } else { return [qpPair] } } }; // the extracted LabelStr, then split into array var LabelStr = matchELabel[1] var labelArr = _.trim(LabelStr, ':').split(':'); if (relaxELabel) { // if relaxed, combine existing labels with :label0|label1|... var orLabelStr = labelArr.join('|') var singleLabelQuery = query.replace(/(\[e)(\:\S+)(\s+)/, '$1' + KB.cons.stringifyLabel(orLabelStr)) return [ [singleLabelQuery, param] ] } else { // the splitted result: same qpPair for each labelE return _.map(labelArr, function(LabelStr) { var singleLabelQuery = query.replace(/(\[e)(\:\S+)(\s+)/, '$1' + KB.cons.stringifyLabel(LabelStr)) return [singleLabelQuery, param] }) } } // the first arg is an array, containing a (JSON || string) || (string || Arr of string) === JSON || string || Arr of string /** * Check if an entity (inside an array) resides within a propDistLabel array. Check if it's JSON prop || string Dist || string Label || Array of >1 string labels * @private * @param {*} entity Inside a propDistLabel array * @return {Boolean} true if entity is inside propDistLabel */ /* istanbul ignore next */ function isOfPropDistLabel(entity) { return _.isPlainObject(entity) || _.isString(entity) || (_.isArray(entity) && _.size(entity) > 1 && _.isString(entity[0])) } // console.log(isOfPropDistLabel({name: 'A'})) // console.log(isOfPropDistLabel('*0..1')) // console.log(isOfPropDistLabel('label')) // console.log(isOfPropDistLabel(['label0', 'label1'])) /** * Check if an array is propDistLabel. Calls isOfPropDistLabel internally. * @private * @param {Array} array * @return {Boolean} true if so. */ /* istanbul ignore next */ function isPropDistLabel(array) { return _.prod(_.map(array, isOfPropDistLabel)) } // console.log(isPropDistLabel([{name: 'A'}])) // console.log(isPropDistLabel([{name: 'A'}, 'label'])) // console.log(isPropDistLabel([{name: 'A'}, ['label0', 'label1']])) // console.log(isPropDistLabel(['*0..1', 'label'])) // console.log(isPropDistLabel(['label'])) // console.log(isPropDistLabel([['label0', 'label1']])) /** * Returns the tensor rank of an argument array from the batchQuery arg tail. * @private * @param {Array} tailArr The array of argument at the tail of arg of batchQuery. * @return {integer} The rank, starting from 1. */ /* istanbul ignore next */ function getRank(tailArr) { if (isPropDistLabel(tailArr)) { return 0; } else { return 1 + getRank(_.first(tailArr)); } } /** * Flatten an array to rank n. Basically flatten it for (getRank - n) times. If the original rank is lower, just return the array. * @private * @param {Array} arr The array to flatten to rank n. * @param {integer} n Target rank to flatten to. * @return {Array} The flattened (or not) array. */ /* istanbul ignore next */ function flattenToRank(arr, n) { var rankDiff = getRank(arr) - n if (rankDiff < 0) { return arr }; while (rankDiff--) { arr = _.flatten(arr) } return arr } // var arr = [[[[{name: 'a'}]]]] // console.log(flattenToRank(arr, 2)) ////////////////// // Node methods // ////////////////// /** * Adds node(s) to neo4j with a required JSON prop satisfying the KB constraints, and an optional Label string or array. * @param {*} single_query, As (fn, \*, \*, ...), e.g. (fn, propLabel) * @param {*} multi_queries As (fn, [\*], [\*], [\*]...), e.g. (fn, [propLabel0], [propLabel1], ...) * @param {*} multi_queries_one_array As (fn, [[\*], [\*], [\*]...]), e.g. (fn, [[propLabel0], [propLabel1], ...]) * @return {Promise} From the query. * * @example * var propA = {name: 'A', hash_by: 'name'} * var propB = {name: 'B', hash_by: 'name'} * // legalize the prop objects subject to constraints * KB.cons.legalize(propA) * KB.cons.legalize(propB) * addNode(propA, 'alpha').then(KB.log) * // {"results":[{"columns":["u"],"data":[{"row":[{"created_when":1452801392345,"updated_by":"tester","name":"A","hash_by":"name","created_by":"tester","hash":"A","updated_when":1452802417919}]}]}],"errors":[]} * // The node is added/updated to KB. * * // batch node query by array of pairs * addNode([propA, 'alpha'], [propB, 'alpha']).then(KB.log) * * // equivalently * addNode([[propA, 'alpha'], [propB, 'alpha']]).then(KB.log) * // {"results":[{"columns":["u"],"data":[{"row":[{"created_when":1452801392345,"updated_by":"tester","name":"A","hash_by":"name","created_by":"tester","hash":"A","updated_when":1452803465461}]}]},{"columns":["u"],"data":[{"row":[{"created_when":1452803465462,"name":"B","updated_by":"tester","hash_by":"name","created_by":"tester","hash":"B","updated_when":1452803465462}]}]}],"errors":[]} * // propA node is updated; propB node is added. * */ var addNode = _.partial(batchQuery, push) // var propA = { // name: 'A', // hash_by: 'name' // } // var propB = { // name: 'B', // hash_by: 'name' // } // KB.cons.legalize(propA) // KB.cons.legalize(propB) // console.log(pushNode(propA, 'alpha')) // // addNode(propA, 'alpha').then(KB.log); // // addNode([propA, 'alpha']).then(KB.log) // // XXXXXaddNode([propA, 'alpha'], [propB, 'alpha']).then(KB.log) // addNode([[[propA, 'alpha']], [[propB, 'alpha']]]).then(KB.log) /** * Get node(s) from neo4j with JSON prop, and optional Label. * @param {*} single_query, As (fn, \*, \*, ...), e.g. (fn, propLabel) * @param {*} multi_queries As (fn, [\*], [\*], [\*]...), e.g. (fn, [propLabel0], [propLabel1], ...) * @param {*} multi_queries_one_array As (fn, [[\*], [\*], [\*]...]), e.g. (fn, [[propLabel0], [propLabel1], ...]) * @return {Promise} From the query. * * @example * var prop2 = {name: 'A', hash_by: 'name'} * var prop3 = {name: 'B', hash_by: 'name'} * // no constrain needed when getting node from KB * * get nodes from just the prop * getNode(prop2).then(KB.log) * // {"results":[{"columns":["u"],"data":[{"row":[{"created_when":1452807183847,"updated_by":"bot","name":"A","hash_by":"name","created_by":"tester","hash":"A","updated_when":1453244315302}]}]}],"errors":[]} * * get nodes from just the label * getNode('alpha').then(KB.log) * // {"results":[{"columns":["u"],"data":[{"row":[{"created_when":1452807183847,"updated_by":"bot","name":"A","hash_by":"name","created_by":"tester","hash":"A","updated_when":1453244315302}]},{"row":[{"created_when":1452807183848,"updated_by":"bot","name":"B","hash_by":"name","created_by":"tester","hash":"B","updated_when":1453244315304}]},{"row":[{"created_when":1453143013572,"updated_by":"bot","name":"C","hash_by":"name","created_by":"bot","hash":"C","updated_when":1453143013572}]}]}],"errors":[]} * * // get nodes from a propLabel pair * getNode(prop2, 'alpha').then(KB.log) * // {"results":[{"columns":["u"],"data":[{"row":[{"created_when":1452801392345,"updated_by":"tester","name":"A","hash_by":"name","created_by":"tester","hash":"A","updated_when":1452803465461}]}]}],"errors":[]} * * // get nodes from many propLabel pairs * getNode([prop2, 'alpha'], [prop3, 'alpha']).then(KB.log) * // equivalently * getNode([[prop2, 'alpha'], [prop3, 'alpha']]).then(KB.log) * // {"results":[{"columns":["u"],"data":[{"row":[{"created_when":1452801392345,"updated_by":"tester","name":"A","hash_by":"name","created_by":"tester","hash":"A","updated_when":1452803465461}]}]},{"columns":["u"],"data":[{"row":[{"created_when":1452803465462,"updated_by":"tester","name":"B","hash_by":"name","created_by":"tester","hash":"B","updated_when":1452803465462}]}]}],"errors":[]} * */ var getNode = _.partial(batchQuery, pull) // prop2 = { // name: 'A', // hash_by: 'name' // } // prop3 = { // name: 'B', // hash_by: 'name' // } // getNode(prop2).then(KB.log) // getNode('alpha').then(KB.log) // getNode(prop2, 'alpha').then(KB.log) // getNode([prop2, 'alpha'], [prop3, 'alpha']).then(KB.log) // getNode([[prop2, 'alpha'], [prop3, 'alpha']]).then(KB.log) ////////////////// // Edge methods // ////////////////// /** * Adds edge(s) to neo4j with propLabel of nodes A -> B with the edge E. The propLabel for A and B is an array of a optional non-empty JSON prop (doesn't have to satisfy KB constraints), and an optional Label string or array. The prop for E must satisfy the KB constraints, and the Label for E is required. * @param {*} single_query, As (fn, \*, \*, ...), e.g. (fn, propLabelA, propLabelE, propLabelB) * @param {*} multi_queries As (fn, [\*], [\*], [\*]...), e.g. (fn, [propLabelA0, propLabelE0, propLabelB0], [propLabelA1, propLabelE1, propLabelB1], ...) * @param {*} multi_queries_one_array As (fn, [[\*], [\*], [\*]...]), e.g. (fn, [[propLabelA0, propLabelE0, propLabelB0], [propLabelA1, propLabelE1, propLabelB1], ...]) * @return {Promise} From the query. * * @example * var propE = {name: 'lexicography', hash_by: 'name'} * KB.cons.legalize(propE) * var labelE = 'next' * var labelE2 = 'after' * var labelEArr = ['next', 'after'] * * var propA = {name: 'A', hash_by: 'name'} * var propB = {name: 'B', hash_by: 'name'} * var labelA = 'alpha' * var labelB = 'alpha' * * // add edge E from node A to node B * addEdge([propA, labelA], [propE, labelE], [propB, labelB]).then(KB.log) * // {"results":[{"columns":["e"],"data":[{"row":[{"created_when":1452825908415,"updated_by":"bot","name":"lexicography","hash_by":"name","created_by":"tester","hash":"lexicography","updated_when":1452884323471}]}]},{"columns":["e"],"data":[{"row":[{"created_when":1452884323471,"name":"lexicography","updated_by":"bot","hash_by":"name","created_by":"bot","hash":"lexicography","updated_when":1452884323471}]}]}],"errors":[]} * // The edge labeled 'next' is added/updated to KB. * * Constraints only for propE, required Label for edge E. No constraints or requirements for nodes A and B. // addEdge([propE, labelE], [labelA], [propB, labelB]).then(KB.log) * // {"results":[{"columns":["e"],"data":[{"row":[{"created_when":1452825908415,"updated_by":"bot","name":"lexicography","hash_by":"name","created_by":"tester","hash":"lexicography","updated_when":1453259876938}]},{"row":[{"created_when":1453259876938,"name":"lexicography","updated_by":"bot","hash_by":"name","created_by":"bot","hash":"lexicography","updated_when":1453259876938}]},{"row":[{"created_when":1453259876938,"name":"lexicography","updated_by":"bot","hash_by":"name","created_by":"bot","hash":"lexicography","updated_when":1453259876938}]}]}],"errors":[]} * * // batch edge query by array of pairs * addEdge( * [ [propA, labelA], [propE, labelE], [propB, labelB] ], * [ [propA, labelA], [propE, labelE2], [propB, labelB] ] * ).then(KB.log) * * // equivalently * addEdge([ * [ [propA, labelA], [propE, labelE], [propB, labelB] ], * [ [propA, labelA], [propE, labelE2], [propB, labelB] ] * ]).then(KB.log) * // {"results":[{"columns":["e"],"data":[{"row":[{"created_when":1452825908415,"updated_by":"bot","name":"lexicography","hash_by":"name","created_by":"tester","hash":"lexicography","updated_when":1452884568091}]}]},{"columns":["e"],"data":[{"row":[{"created_when":1452884323471,"updated_by":"bot","name":"lexicography","hash_by":"name","created_by":"bot","hash":"lexicography","updated_when":1452884568091}]}]}],"errors":[]} * // edge 'next' is updated, edge 'after' is added * * shorthand for edge with multiple labels but same prop * addEdge([propA, labelA], [propE, labelEArr], [propB, labelB]).then(KB.log) * // {"results":[{"columns":["e"],"data":[{"row":[{"created_when":1452825908415,"updated_by":"bot","name":"lexicography","hash_by":"name","created_by":"tester","hash":"lexicography","updated_when":1452884620930}]}]},{"columns":["e"],"data":[{"row":[{"created_when":1452884323471,"updated_by":"bot","name":"lexicography","hash_by":"name","created_by":"bot","hash":"lexicography","updated_when":1452884620930}]}]}],"errors":[]} * */ var addEdge = _.partial(batchQuery, push) // var propE = {name: 'lexicography', hash_by: 'name'} // KB.cons.legalize(propE) // var labelE = 'next' // var labelE2 = 'after' // var labelEArr = ['next', 'after'] // var propA = {name: 'A', hash_by: 'name'} // var propB = {name: 'B', hash_by: 'name'} // var labelA = 'alpha' // var labelB = 'alpha' // console.log(splitLabelE(pushEdge([propA, labelA], [propE, labelE], [propB, labelB]))) // addEdge([propA, labelA], [propE, labelEArr], [propB, labelB]).then(KB.log) // addEdge([propE, labelE], [labelA], [propB, labelB]).then(KB.log) // addEdge( // [ [propA, labelA], [propE, labelE], [propB, labelB] ], // [ [propA, labelA], [propE, labelE2], [propB, labelB] ] // ).then(KB.log) // addEdge([ // [ [propA, labelA], [propE, labelE], [propB, labelB] ], // [ [propA, labelA], [propE, labelE2], [propB, labelB] ] // ]).then(KB.log) // addEdge([propA, labelA], [propE, labelEArr], [propB, labelB]).then(KB.log) /** * Get edge(s) from neo4j with propLabel of nodes A -> B with the edge E. The propLabel for A, B and E is an array of a optional non-empty JSON prop (doesn't have to satisfy KB constraints), and a (optional for A,B; required for E) Label string or array. * @param {*} single_query, As (fn, \*, \*, ...), e.g. (fn, propLabelA, propLabelE, propLabelB) * @param {*} multi_queries As (fn, [\*], [\*], [\*]...), e.g. (fn, [propLabelA0, propLabelE0, propLabelB0], [propLabelA1, propLabelE1, propLabelB1], ...) * @param {*} multi_queries_one_array As (fn, [[\*], [\*], [\*]...]), e.g. (fn, [[propLabelA0, propLabelE0, propLabelB0], [propLabelA1, propLabelE1, propLabelB1], ...]) * @return {Promise} From the query. * * @example * var propE = {name: 'lexicography', hash_by: 'name'} * var labelE = 'next' * var labelE2 = 'after' * var labelEArr = ['next', 'after'] * * var propA = {name: 'A', hash_by: 'name'} * var propB = {name: 'B', hash_by: 'name'} * var labelA = 'alpha' * var labelB = 'alpha' * * // The below are equivalent for the added edge above, and show that propLabelA and propLabelB are optional. * getEdge( * [propE, labelE] * ).then(KB.log) * * getEdge( * [propE, labelE], * [propA, labelA] * ).then(KB.log) * * // label is required for E; The rest are optional. * getEdge( * [labelE], * [propA] * ).then(KB.log) * // {"results":[{"columns":["e"],"data":[{"row":[{"created_when":1453143189686,"updated_by":"bot","name":"lexicography","hash_by":"name","created_by":"bot","hash":"lexicography","updated_when":1453143189686}]},{"row":[{"created_when":1452825908415,"updated_by":"bot","name":"lexicography","hash_by":"name","created_by":"tester","hash":"lexicography","updated_when":1453259876938}]}]}],"errors":[]} * * getEdge( * [propE, labelE], * [propA, labelA], * [propB, labelB] * ).then(KB.log) * // {"results":[{"columns":["e"],"data":[{"row":[{"created_when":1452825908415,"updated_by":"bot","name":"lexicography","hash_by":"name","created_by":"tester","hash":"lexicography","updated_when":1452885949550}]}]}],"errors":[]} * * * // the following are equivalent: batch edge query * getEdge( * [[propE, labelE] ], * [[propE, labelE2] ] * ).then(KB.log) * getEdge([ * [[propE, labelE] ], * [[propE, labelE2] ] * ]).then(KB.log) * // {"results":[{"columns":["e"],"data":[{"row":[{"created_when":1452825908415,"updated_by":"bot","name":"lexicography","hash_by":"name","created_by":"tester","hash":"lexicography","updated_when":1452885949550}]}]},{"columns":["e"],"data":[{"row":[{"created_when":1452884323471,"updated_by":"bot","name":"lexicography","hash_by":"name","created_by":"bot","hash":"lexicography","updated_when":1452885949550}]}]}],"errors":[]} * * // shorthand: pull multiple edges using multiple labels, and same prop. * getEdge( * [propE, labelEArr] * ).then(KB.log) * // {"results":[{"columns":["e"],"data":[{"row":[{"created_when":1452825908415,"updated_by":"bot","name":"lexicography","hash_by":"name","created_by":"tester","hash":"lexicography","updated_when":1452885949550}]}]},{"columns":["e"],"data":[{"row":[{"created_when":1452884323471,"updated_by":"bot","name":"lexicography","hash_by":"name","created_by":"bot","hash":"lexicography","updated_when":1452885949550}]}]}],"errors":[]} * */ var getEdge = _.partial(batchQuery, pull) // var propE = { // name: 'lexicography', // hash_by: 'name' // } // var labelE = 'next' // var labelE2 = 'after' // var labelEArr = ['next', 'after'] // var propA = { // name: 'A', // hash_by: 'name' // } // var propB = { // name: 'B', // hash_by: 'name' // } // var labelA = 'alpha' // var labelB = 'alpha' // console.log(pullEdge( // [propE, labelE] // )) // getEdge( // [propE, labelE] // ).then(KB.log) // getEdge( // [propE, labelE], // [propA, labelA] // ).then(KB.log) // getEdge( // [labelE], // [propA] // ).then(KB.log) // getEdge( // [propE, labelE], // [propA, labelA], // [propB, labelB] // ).then(KB.log) // getEdge( // [[propE, labelE] ], // [[propE, labelE2] ] // ).then(KB.log) // getEdge([ // [[propE, labelE] ], // [[propE, labelE2] ] // ]).then(KB.log) // getEdge( // [propE, labelEArr] // ).then(KB.log) /////////////////// // Graph methods // /////////////////// /** * The graph (node and edge) batching function. Applies the tail arguments to the query-param composer function fn: (propLabelA, propDistLabelE, propLabelB, wOp, sOp, rOp, pOp) or ([7-tuple], [7-tuple], ...), or ([[7-tuple], [7-tuple], ...]), then apply to a single query. Used to compose the high level KB_builder functions such as get etc. * Internally calls the splitLabelE (LabelE relaxed - not required) function to generate more query-param pairs which only single Elabels, since each edge can contain only a label. Applies fn by the argument tensor rank: either rank-1 or rank-2 and above (reduced to rank-2 and apply). Rank-1: ([p, L], [p|D, L], [p, L], wOp, rOp). * @private * @param {Function} fn The function for composing a valid query-params Array for query() to take. * @param {*} single_query, As (fn, \*, \*, ...), e.g. (fn, [p, L], [p|D, L], [p, L], wOp, rOp) * @param {*} multi_queries As (fn, [\*], [\*], [\*]...), e.g. (fn, [[p, L], [p|D, L], [p, L], wOp, rOp], [[p, L], [p|D, L], [p, L], wOp, rOp], ...) * @param {*} multi_queries_one_array As (fn, [[\*], [\*], [\*]...]), e.g. (fn, [[[p, L], [p|D, L], [p, L], wOp, rOp], [[p, L], [p|D, L], [p, L], wOp, rOp], ...]) * @return {Promise} From the query. * * @example * var get = _.partial(batchQuery, pull) * // => function to get nodes and edges from KB. */ function batchQuery(fn, T) { var tailArr = _.tail(_.toArray(arguments)); // determine the rank of T var rank = getRank(tailArr); // properly ranked argArr to pass to query at the end var argArr; // construct the argArr by rank; applies fn and splitLabelE /* istanbul ignore else */ if (rank > 1) { // flatten all higher to rank 2 tailArr = flattenToRank(tailArr, 2) argArr = _.flatten(_.map(tailArr, function(arr) { // here relaxing the ELabel constraint return splitLabelE(fn.apply(this, arr)) })) } else if (rank == 1) { argArr = splitLabelE(fn.apply(this, tailArr)) } else { throw new Error("Your argument rank is < 1. Rank must be positive.") } // console.log(argArr); return query(argArr) } // // a rank 1 arguments. // // Get only node(s) // batchQuery(pull, [{ // name: 'A' // }, 'alpha'], 'RETURN a') // // a rank 1 arguments. // // Get nodes and edges. Note that labelE is optional now // batchQuery(pull, [{ // name: 'A' // }, 'alpha'], ['*0..1'], 'RETURN b,e') // // a rank 2 arguments // // Get nodes and edges // batchQuery(pull, [ // [{ // name: 'A' // }, 'alpha'], // ['*0..1', 'next'], 'RETURN b,e' // ]) // // a rank 3 arguments, practically the highest or you're using it wrong. Properly split by LabelE too // batchQuery(pull, [ // [ // [{ // name: 'A' // }, 'alpha'], // ['*0..1', ['next', 'xiage']], 'RETURN b,e' // ], // [ // [{ // name: 'B' // }, 'alpha'], // ['next'], 'RETURN b,e' // ] // ]) /** * Returns a query-param pair for get, with string-cleaning (prevents SQL injection). This is a flexible method used for querying node(s), or optionally edge(s) and node(s). It also takes an optional WHERE filter sentence string, and a required RETURN sentence string. This is used to inject the query-param pair into query. The resultant query string is of the form: * For nodes: MATCH (a:LabelA {propA}) <wOp> <sOp> <rOp> * For nodes and edges: MATCH (a:LabelA {propA})-[e:LabelE ({propE}|*Dist)]-(b:LabelB {propB}) <wOp> <sOp> <rOp> <pOp> * * prop is the property JSON * Label is the label string or array of strings * Dist is the distance statement for edge; it is mutex with prop for edge. * <wOp> is the optional WHERE filter sentence, e.g. WHERE a.name="A" AND b.name="B" * <sOp> is the optional SET|REMOVE property-update sentence, e.g. SET a.age=10, a.sex="M" * <rOp> is the required RETURN|DELETE|DETACH DELETE sentence, e.g. RETURN b.hash, a.name; or DETACH DELETE a * <pOp> is the optional SHORETSTPATH|ALLSHORTESTPATHS sentence (no arguments), e.g. SHORTESTPATH * Note that the <Ops> can be specified at the tail of argument in any order since there is no ambiguity. * * The entity names 'a', 'e', 'b' respectively from (a)-[e]->(b) must be specified in the <wOp>, <sOp> and <rOp> for correct reference, e.g. RETURN a.name. Also note the direction of the edge is from 'a'->'b' * If a <pOp> of SHORTESTPATH|ALLSHORTESTPATH, then 'p' references the path object. * * For the edge e, either supply: * a JSON propE like {name="E"} for [e:LabelE {propE}] or, * a string Dist like '*0..2' for [e:LabelE *0..2] * this is because the two are already mutex in neo4j. * When <pOp> is used, propE is forbidden. * * @private * @param {Array} propLabelA The propLabel pair for node 'a'. The second argument (LabelA) is optional. * @param {Array} [propDistLabelE] The optional propLabel or distLabel pair for edge 'e'. The second argument (LabelE) is optional. * @param {Array} [propLabelB] The optional propLabel pair for node 'b'. The second argument (LabelB) is optional. * @param {string} [wOp] An optional, valid WHERE ... filter sentence. * @param {string} [sOp] An optional, valid SET|REMOVE ... property update sentence. * @param {string} rOp A required, valid RETURN|DELETE|DETACH DELETE ... return or delete sentence. * @param {string} [pOp] An optional SHORTESTPATH|ALLSHORTESTPATHS sentence to make the query return a path object 'p'. * @return {Array} Pair of query and params object. * * @example * * // return all nodes * KB.log(pull([], 'RETURN a')); * // [ 'MATCH (a ) RETURN a', {} ] * * // return nodes with the prop * KB.log(pull([{ * name: "A" * }], 'RETURN a')); * // [ 'MATCH (a {name: {propA}.name}) RETURN a', { propA: { name: 'A' } } ] * * // return nodes with the prop * KB.log(pull([{ * name: "A" * }], 'RETURN a')); * // [ 'MATCH (a {name: {propA}.name}) RETURN a', { propA: { name: 'A' } } ] * * // return all nodes with the prop and label * KB.log(pull([{ * name: "A" * }, 'alpha'], 'RETURN a')); * // [ 'MATCH (a:alpha {name: {propA}.name}) RETURN a', { propA: { name: 'A' } } ] * * // same as above * KB.log(pull([, 'alpha'], 'WHERE a.name="A"', 'RETURN a')); * // [ 'MATCH (a:alpha ) where a.name="A" RETURN a', {} ] * * // DETACH DELETE a node * KB.log(pull([, 'alpha'], 'WHERE a.name="C"', 'DETACH DELETE a')); * // [ 'MATCH (a ) where a.name="C" DETACH DELETE a', {} ] * * // SET properties * KB.log(pull(['alpha'], 'WHERE a.name="A"', 'SET a.age=10, a.sex="M"', 'RETURN a')); * // [ 'MATCH (a:alpha ) where a.name="A" SET a.age=10, a.sex="M" RETURN a', {} ] * * // REMOVE properties * KB.log(pull(['alpha'], 'WHERE a.name="A"', 'REMOVE a.age, a.sex', 'RETURN a')); * // [ 'MATCH (a:alpha ) where a.name="A" REMOVE a.age, a.sex RETURN a', {} ] * * // SHORTESTPATH return paths of: nodes and edges 0-2 units apart FROM node a, with the props and labels * KB.log(pull([{ * name: 'A' * }, 'alpha'], ['*0..2'], 'SHORTESTPATH', 'RETURN p, DISTINCT(nodes(p))')); * // [ 'MATCH (a:alpha {name: {propA}.name})-[e:next *0..1]->(b ) return b,e', { propA: { name: 'A' } } ] * * // return nodes and edges 0-1 units apart FROM node a, with the props and labels * KB.log(pull([{ * name: 'A' * }, 'alpha'], ['*0..1', 'next'], 'RETURN b,e')); * // [ 'MATCH (a:alpha {name: {propA}.name})-[e:next *0..1]->(b ) return b,e', { propA: { name: 'A' } } ] * * // return nodes and edges 0-1 units apart TO node B, with the props and labels * KB.log(pull([], ['*0..1', 'next'], [{ * name: 'B' * }, 'alpha'], 'RETURN a,e')); * ['MATCH (a )-[e:next *0..1]->(b:alpha {name: {propB}.name}) return a,e', { * propB: { * name: 'B' * } * }] * * // return nodes and edges units apart TO node B, with edge named 'E' and labeled 'next' * KB.log(pull([], [{ * name: 'E' * }, * ['next', 'xiage'] * ], [{ * name: 'B' * }, 'alpha'], 'RETURN a,e')); * ['MATCH (a )-[e:next {name: {propE}.name}]->(b:alpha {name: {propB}.name}) return a,e', { * propE: { * name: 'E' * }, * propB: { * name: 'B' * } * }] * * // same as above, but source nodes must now have name that is lexicographically lower than "B" * KB.log(pull([], [{ * name: 'E' * }, 'next'], [{ * name: 'B' * }, 'alpha'], 'WHERE a.name < "B"', 'RETURN a,e')); * // [ 'MATCH (a )-[e:next {name: {propE}.name}]->(b:alpha {name: {propB}.name}) WHERE a.name < "B" return a,e', { propE: { name: 'E' }, propB: { name: 'B' } } ] * * // if E has an array of multiple labels, can use with splitLabelE * KB.log(splitLabelE(pull([], [{ * name: 'E' * }, * ['next', 'xiage'] * ], [{ * name: 'B' * }, 'alpha'], 'WHERE a.name < "B"', 'RETURN a,e'))); * // [["MATCH (a)-[e:next|xiage {name: {propE}.name}]->(b:alpha {name: {propB}.name}) WHERE a.name < \"B\" RETURN a,e",{"propE":{"name":"E"},"propB":{"name":"B"}}]] * */ function pull(propLabelA, propDistLabelE, propLabelB, wOp, sOp, rOp, pOp) { // partition into arr and str arguments var part = _.partition(arguments, _.isArray) var arrArr = _.first(part), strArr = _.last(part); // setting args accordingly var propLabelA = arrArr[0], propDistLabelE = arrArr[1], propLabelB = arrArr[2]; // the WHERE, SET, RETURN, PATH clauses var wOpStr = _.find(strArr, KB.cons.isWOp) || ''; var sOpStr = _.find(strArr, KB.cons.isSOp) || ''; var rOpStr = _.find(strArr, KB.cons.isROp) || ''; // if RETURN is not supplied, if (!rOpStr) { if (propDistLabelE) { // return the found edge rOpStr = 'RETURN e' } else { // or return the node if no edge query is made rOpStr = 'RETURN a' } }; var pOpStr = _.find(strArr, KB.cons.isPOp) || ''; // console.log('propLabelA', propLabelA) // console.log('propDistLabelE', propDistLabelE) // console.log('propLabelB', propLabelB) // console.log("wOpStr", wOpStr) // console.log("sOpStr", sOpStr) // console.log("rOpStr", rOpStr) // console.log("pOpStr", pOpStr) // declare the head, body, tail of the query string, and the props // head = MATCH (a:LabelA {propA}) // body = -[e:LabelE {propE}|*Dist]->(b:LabelB {propB}) // tail = WHERE ... SET|REMOVE ... RETURN|DELETE|DETACH DELETE ... var head, body, tail, props; // Head: first node arg var partA = sortPropLabel(propLabelA), propA = _.get(partA, '0.0'), LabelA = _.get(partA, '1.0'), LabelStrA = KB.cons.stringifyLabel(LabelA); head = 'MATCH (a' + LabelStrA + literalizeProp(propA, 'propA') + ')' // Body: optional edge and end node args if (propDistLabelE) { // edge, doesn't need to pass cons var partE = sortPropLabel(propDistLabelE), propDistE = _.get(partE, '0.0'), // take prop XOR dist for edge propE = _.isPlainObject(propDistE) ? propDistE : undefined, distE = KB.cons.stringifyDist(propDistE), LabelE = _.get(partE, '1.0'), LabelStrE = KB.cons.stringifyLabel(LabelE); // node B var partB = sortPropLabel(propLabelB), propB = _.get(partB, '0.0'), LabelB = _.get(partB, '1.0'), LabelStrB = KB.cons.stringifyLabel(LabelB); // if it's a path query, discard propE, reform string if (pOpStr) { propE = undefined body = ', (b' + LabelStrB + literalizeProp(propB, 'propB') + '), ' + 'p=' + pOpStr + '((a)-[e' + LabelStrE + distE + ']->(b))' } else { body = '-[e' + LabelStrE + literalizeProp(propE, 'propE') + distE + ']->' + '(b' + LabelStrB + literalizeProp(propB, 'propB') + ')' } props = _.pickBy({ propA: propA, propE: propE, propB: propB }) } else { // just a node query body = '' props = _.pickBy({ propA: propA }) } // Tail tail = (' ' + wOpStr + ' ' + sOpStr + ' ' + rOpStr).replace(/\s{2,}/g, ' ') return [ head + body + tail, props ] } // // return all nodes // KB.log(pull([], 'RETURN a')); // // [ 'MATCH (a ) RETURN a', {} ] // // return nodes with the prop // KB.log(pull([{ // name: "A" // }], 'RETURN a')); // // [ 'MATCH (a {name: {propA}.name}) RETURN a', { propA: { name: 'A' } } ] // // return nodes with the prop // KB.log(pull([{ // name: "A" // }], 'RETURN a')); // // [ 'MATCH (a {name: {propA}.name}) RETURN a', { propA: { name: 'A' } } ] // // return all nodes with the prop and label // KB.log(pull([{ // name: "A" // }, 'alpha'], 'RETURN a')); // // [ 'MATCH (a:alpha {name: {propA}.name}) RETURN a', { propA: { name: 'A' } } ] // // same as above // KB.log(pull([, 'alpha'], 'WHERE a.name="A"', 'RETURN a')); // // [ 'MATCH (a:alpha ) where a.name="A" RETURN a', {} ] // // DETACH DELETE a node // KB.log(pull([, 'alpha'], 'WHERE a.name="C"', 'DETACH DELETE a')); // // [ 'MATCH (a ) where a.name="C" DETACH DELETE a', {} ] // // SET properties // KB.log(pull(['alpha'], 'WHERE a.name="A"', 'SET a.age=10, a.sex="M"', 'RETURN a')); // // [ 'MATCH (a:alpha ) where a.name="A" SET a.age=10, a.sex="M" RETURN a', {} ] // // REMOVE properties // KB.log(pull(['alpha'], 'WHERE a.name="A"', 'REMOVE a.age, a.sex', 'RETURN a')); // // [ 'MATCH (a:alpha ) where a.name="A" REMOVE a.age, a.sex RETURN a', {} ] // // SHORTESTPATH return paths of: nodes and edges 0-2 units apart FROM node a, with the props and labels // KB.log(pull([{ // name: 'A' // }, 'alpha'], ['*0..2'], 'SHORTESTPATH', 'RETURN p, DISTINCT(nodes(p))')); // // [ 'MATCH (a:alpha {name: {propA}.name})-[e:next *0..1]->(b ) return b,e', { propA: { name: 'A' } } ] // // return nodes and edges 0-1 units apart FROM node a, with the props and labels // KB.log(pull([{ // name: 'A' // }, 'alpha'], ['*0..1', 'next'], 'RETURN b,e')); // // [ 'MATCH (a:alpha {name: {propA}.name})-[e:next *0..1]->(b ) return b,e', { propA: { name: 'A' } } ] // // return nodes and edges 0-1 units apart TO node B, with the props and labels // KB.log(pull([], ['*0..1', 'next'], [{ // name: 'B' // }, 'alpha'], 'RETURN a,e')); // ['MATCH (a )-[e:next *0..1]->(b:alpha {name: {propB}.name}) return a,e', { // propB: { // name: 'B' // } // }] // // return nodes and edges units apart TO node B, with edge named 'E' and labeled 'next' // KB.log(pull([], [{ // name: 'E' // }, // ['next', 'xiage'] // ], [{ // name: 'B' // }, 'alpha'], 'RETURN a,e')); // ['MATCH (a )-[e:next {name: {propE}.name}]->(b:alpha {name: {propB}.name}) return a,e', { // propE: { // name: 'E' // }, // propB: { // name: 'B' // } // }] // // same as above, but source nodes must now have name that is lexicographically lower than "B" // KB.log(pull([], [{ // name: 'E' // }, 'next'], [{ // name: 'B' // }, 'alpha'], 'WHERE a.name < "B"', 'RETURN a,e')); // // [ 'MATCH (a )-[e:next {name: {propE}.name}]->(b:alpha {name: {propB}.name}) WHERE a.name < "B" return a,e', { propE: { name: 'E' }, propB: { name: 'B' } } ] // // if E has an array of multiple labels, can use with splitLabelE // KB.log(splitLabelE(pull([], [{ // name: 'E' // }, // ['next', 'xiage'] // ], [{ // name: 'B' // }, 'alpha'], 'WHERE a.name < "B"', 'RETURN a,e'))); // // [["MATCH (a)-[e:next|xiage {name: {propE}.name}]->(b:alpha {name: {propB}.name}) WHERE a.name < \"B\" RETURN a,e",{"propE":{"name":"E"},"propB":{"name":"B"}}]] /** * Get graph and do whatever u want with the search results: filter, RETURN, DELETE, DETACH DELETE. Graph: node(s) and edge(s) from neo4j with propLabel of nodes A -> B with the edge E. The propLabel for A, B and E is an array of a optional non-empty JSON prop (doesn't have to satisfy KB constraints), and a (optional for A,B; required for E) Label string or array. * This is a flexible method used for querying node(s), or optionally edge(s) and node(s). It also takes an optional WHERE filter sentence string, and a required RETURN sentence string.The resultant query string is of the form: * For nodes: MATCH (a:LabelA {propA}) <wOp> <sOp> <rOp> * For nodes and edges: MATCH (a:LabelA {propA})-[e:LabelE ({propE}|*Dist)]-(b:LabelB {propB}) <wOp> <sOp> <rOp> <pOp> * * @param {*} single_query, As (fn, \*, \*, ...), e.g. (fn, [p, L], [p|D, L], [p, L], wOp, rOp) * @param {*} multi_queries As (fn, [\*], [\*], [\*]...), e.g. (fn, [[p, L], [p|D, L], [p, L], wOp, rOp], [[p, L], [p|D, L], [p, L], wOp, rOp], ...) * @param {*} multi_queries_one_array As (fn, [[\*], [\*], [\*]...]), e.g. (fn, [[[p, L], [p|D, L], [p, L], wOp, rOp], [[p, L], [p|D, L], [p, L], wOp, rOp], ...]) * @return {Promise} From the query. * * @example * // a rank 1 arguments. * // Get only node(s) * get([{ * name: 'A' * }, 'alpha'], 'RETURN a').then(KB.log) * * // a rank 1 arguments. * // Delete node(s) * get([{ * name: 'C' * }, 'alpha'], 'DETACH DELETE a').then(KB.log) * * // a rank 1 arguments. * // Get nodes and edges. Note that labelE is optional now * get([{ * name: 'A' * }, 'alpha'], ['*0..1'], 'RETURN b,e').then(KB.log) * * // a rank 2 arguments * // Get nodes and edges * get([ * [{ * name: 'A' * }, 'alpha'], * ['*0..1', 'next'], 'RETURN b,e' * ]).then(KB.log) * * // a rank 2 arguments * // Get nodes and edges. Edges can have multiple labels in query; piped * get([ * [{ * name: 'A' * }, 'alpha'], * ['*0..1', ['next', 'xiage']], 'RETURN b,e' * ]).then(KB.log) * */ var get = _.partial(batchQuery, pull) // // a rank 1 arguments. // // Get only node(s) // get([{ // name: 'A' // }, 'alpha'], 'RETURN a').then(KB.log) // // a rank 1 arguments. // // Delete node(s) // get([{ // name: 'C' // }, 'alpha'], 'DETACH DELETE a').then(KB.log) // // a rank 1 arguments. // // Get nodes and edges. Note that labelE is optional now // get([{ // name: 'A' // }, 'alpha'], ['*0..1'], 'RETURN b,e').then(KB.log) // // a rank 2 arguments // // Get nodes and edges // get([ // [{ // name: 'A' // }, 'alpha'], // ['*0..1', 'next'], 'RETURN b,e' // ]).then(KB.log) // // a rank 2 arguments // // Get nodes and edges. Edges can have multiple labels in query; piped // get([ // [{ // name: 'A' // }, 'alpha'], // ['*0..1', ['next', 'xiage']], 'RETURN b,e' // ]).then(KB.log) /** * Returns a query-param pair for add, taking propLabels of nodes A -> B with the edge E. There are two use-cases: * addNode: supply only one propLabelA argument, containing a non-empty JSON prop satisfying the KB constraints, and an optional Label string or array. * addEdge: supply all three propLabels for A, E, B. The propLabel for A and B is an array of a optional non-empty JSON prop (doesn't have to satisfy KB constraints), and an optional Label string or array. The prop for E must satisfy the KB constraints, and the Label for E is required. * This is used to inject the query-param pair into query. * * @private * @param {Array} propLabelA propLabel pair of (source) node A. If this is the only argument supplied, the JSON prop must satisfy the KB cosntraints. The label is optional. * @param {Array} [propLabelE] propLabel pair of edge E from A to B. the JSON prop must satisfy the KB constrains, and the label is required (used for hashing). * @param {Array} [propLabelB] propLabel pair of target node B. Doesn't have to satisfy KB constraints. * @return {Array} Pair of query and params object; or empty array if prop didn't pass the KB.cons. By hashing, this will update the node or edge if it already exists. * * @example * // nodes * var propA = { * name: "A", * hash_by: "name" * } * var propB = { * name: "B", * hash_by: "name" * } * KB.cons.legalize(propA) * KB.cons.legalize(propB) * * // edge * var propE = { * name: "E" * } * KB.cons.legalize(propE) * * for adding a node * KB.log(push([propA, 'test'])); * // ["MERGE (a:test {hash: {propA}.hash}) ON CREATE SET a = {propA}, a.created_by={propA}.updated_by, a.created_when={propA}.updated_when ON MATCH SET a += {propA} RETURN a",{"propA":{"name":"A","hash_by":"name","hash":"A","updated_by":"bot","updated_when":"2016-01-23T16:11:02.962Z"}}] * * KB.log(push([propA, 'test'], [propE, 'test_next'])) * // Error: You must provide propLabel for either A or A,E,B. * * KB.log(push([propA, 'test'], [propE, 'test_next'], [propB, 'test'])) * // ["MATCH (a:test {name: {propA}.name, hash_by: {propA}.hash_by, hash: {propA}.hash, updated_by: {propA}.updated_by, updated_when: {propA}.updated_when}), (b:test {name: {propB}.name, hash_by: {propB}.hash_by, hash: {propB}.hash, updated_by: {propB}.updated_by, updated_when: {propB}.updated_when}) MERGE (a)-[e:test_next ]->(b) ON CREATE SET e = {propE}, e.created_by={propE}.updated_by, e.created_when={propE}.updated_when ON MATCH SET e += {propE} RETURN e",{"propE":{"name":"E","hash_by":"hash","hash":"test","updated_by":"bot","updated_when":"2016-01-23T16:12:08.583Z"},"propA":{"name":"A","hash_by":"name","hash":"test","updated_by":"bot","updated_when":"2016-01-23T16:12:08.579Z"},"propB":{"name":"B","hash_by":"name","hash":"test","updated_by":"bot","updated_when":"2016-01-23T16:12:08.583Z"}}] * */ function push(propLabelA, propLabelE, propLabelB) { // nodes, dont need to pass cons var partA = sortPropLabel(propLabelA), propA = _.get(partA, '0.0'), LabelA = _.get(partA, '1.0'), LabelStrA = KB.cons.stringifyLabel(LabelA); // if propLabelE not present, is addNode if (propLabelA && !propLabelE && !propLabelB) { /* istanbul ignore else */ if (KB.cons.pass(propA)) { return [ // check existence, find by hash 'MERGE (a' + LabelStrA + ' {hash: {propA}.hash}) ' + // create if no hash matched 'ON CREATE SET a = {propA}, a.created_by={propA}.updated_by, a.created_when={propA}.updated_when ' + // update if hash matched 'ON MATCH SET a += {propA} RETURN a', { propA: propA } ] }; } else if (propLabelA && propLabelE && propLabelB) { // edge, needs to pass cons var partE = sortPropLabel(propLabelE), propE = _.get(partE, '0.0'), LabelE = _.get(partE, '1.0'), LabelStrE = KB.cons.stringifyLabel(LabelE); // nodes, dont need to pass cons var partB = sortPropLabel(propLabelB), propB = _.get(partB, '0.0'), LabelB = _.get(partB, '1.0'), LabelStrB = KB.cons.stringifyLabel(LabelB); /* istanbul ignore else */ if (KB.cons.pass(propE)) { return [ // a and b nodes must already exist 'MATCH (a' + LabelStrA + literalizeProp(propA, 'propA') + '), (b' + LabelStrB + literalizeProp(propB, 'propB') + ') ' + // check if e already exists by propE hash 'MERGE (a)-[e' + LabelStrE + ']->(b) ' + // 'MERGE (a)-[e' + LabelStrE + '{hash: {propE}.hash}]->(b) ' + // create if no hash matched 'ON CREATE SET e = {propE}, e.created_by={propE}.updated_by, e.created_when={propE}.updated_when ' + // update if hash matched 'ON MATCH SET e += {propE} RETURN e', _.pickBy({ propE: propE, propA: propA, propB: propB }) ] }; } else { throw new Error("You must provide propLabel for either A or A,E,B.") } } // var propA = { // name: "A", // hash_by: "name" // } // var propB = { // name: "B", // hash_by: "name" // } // KB.cons.legalize(propA) // KB.cons.legalize(propB) // var propE = { // name: "E" // } // KB.cons.legalize(propE) // KB.log(push([propA, 'test'])); // KB.log(push([propA, 'test'], [propE, 'test_next'])) // KB.log(push([propA, 'test'], [propE, 'test_next'], [propB, 'test'])) /** * Get graph and do whatever u want with the search results: filter, RETURN, DELETE, DETACH DELETE. Graph: node(s) and edge(s) from neo4j with propLabel of nodes A -> B with the edge E. The propLabel for A, B and E is an array of a optional non-empty JSON prop (doesn't have to satisfy KB constraints), and a (optional for A,B; required for E) Label string or array. * This is a flexible method used for querying node(s), or optionally edge(s) and node(s). It also takes an optional WHERE filter sentence string, and a required RETURN sentence string.The resultant query string is of the form: * For nodes: MATCH (a:LabelA {propA}) <wOp> <sOp> <rOp> * For nodes and edges: MATCH (a:LabelA {propA})-[e:LabelE ({propE}|*Dist)]-(b:LabelB {propB}) <wOp> <sOp> <rOp> <pOp> * * @param {*} single_query, As (fn, \*, \*, ...), e.g. (fn, [p, L], [p|D, L], [p, L], wOp, rOp) * @param {*} multi_queries As (fn, [\*], [\*], [\*]...), e.g. (fn, [[p, L], [p|D, L], [p, L], wOp, rOp], [[p, L], [p|D, L], [p, L], wOp, rOp], ...) * @param {*} multi_queries_one_array As (fn, [[\*], [\*], [\*]...]), e.g. (fn, [[[p, L], [p|D, L], [p, L], wOp, rOp], [[p, L], [p|D, L], [p, L], wOp, rOp], ...]) * @return {Promise} From the query. * * @example * // nodes * var propA = { * name: "A", * hash_by: "name" * } * var propB = { * name: "B", * hash_by: "name" * } * KB.cons.legalize(propA) * KB.cons.legalize(propB) * // edge * var propE = { * name: "E" * } * KB.cons.legalize(propE) * * // add a node * add([propA, 'test']).then(KB.log) * // [{"columns":["a"],"data":[{"row":[{"created_when":"2016-01-23T16:21:51.674Z","name":"A","updated_by":"bot","hash_by":"name","created_by":"bot","hash":"A","updated_when":"2016-01-23T16:21:51.674Z"}]}]}] * * // add nodes * add([[propA, 'test'], ], [[propB, 'test'], ]).then(KB.log) * // equivalently * add([[[propA, 'test'], ], [[propB, 'test'], ]]).then(KB.log) * // [{"columns":["a"],"data":[{"row":[{"created_when":"2016-01-23T16:21:51.674Z","name":"A","updated_by":"bot","hash_by":"name","created_by":"bot","hash":"A","updated_when":"2016-01-23T16:24:14.242Z"}]}]},{"columns":["a"],"data":[{"row":[{"created_when":"2016-01-23T16:23:53.406Z","name":"B","updated_by":"bot","hash_by":"name","created_by":"bot","hash":"B","updated_when":"2016-01-2