diff --git a/.circleci/config.yml b/.circleci/config.yml index 995fe8ec89..301acd63b3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -35,6 +35,31 @@ jobs: - run: istanbul cover ./node_modules/mocha/bin/_mocha -- -R spec --exit - run: codecov -t ${CODECOV_TOKEN} + docs: + docker: + - image: circleci/node:10.15 + + working_directory: ~/repo + + steps: + - checkout + - run: + name: Install and configure dependencies + command: | + npm install -g --silent gh-pages@2.0.1 + npm install -g --silent jsdoc + git config user.email "ci-build@redisgraph.io" + git config user.name "ci-build" + - add_ssh_keys: + fingerprints: + - "76:ad:7c:fa:32:41:29:d0:cc:f6:63:06:62:b6:54:9c" + - run: + name: Create docs with jsdoc + command: jsdoc ./src -r -d ./docs -R ./README.md + - run: + name: Deploy docs to gh-pages branch + command: gh-pages --dist ./docs --branch master --dest ./docs + workflows: version: 2 commit: @@ -50,3 +75,4 @@ workflows: - master jobs: - build + - docs diff --git a/README.md b/README.md index 8541c20516..13b87f2285 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,12 @@ Installation is done using the npm install redisgraph.js ``` +For installing the latest snapshot use +```bash +npm install github:RedisGraph/redisgraph.js.git +``` + + ## Overview ### Official Releases @@ -34,39 +40,38 @@ const RedisGraph = require("redisgraph.js").Graph; let graph = new RedisGraph("social"); -graph.query("CREATE (:person{name:'roi',age:32})").then(() => { - graph.query("CREATE (:person{name:'amit',age:30})").then(()=>{ - graph.query("MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(b)").then(()=>{ - graph.query("MATCH (a:person)-[:knows]->(:person) RETURN a.name").then(res=>{ - while (res.hasNext()) { - let record = res.next(); - console.log(record.get("a.name")); - } - console.log(res.getStatistics().queryExecutionTime()); - let param = {'age': 30}; - graph.query("MATCH (a {age: $age}) return a.name", param).then(res=>{ - while (res.hasNext()) { - let record = res.next(); - console.log(record.get("a.name")); - } - graph.query("MATCH p = (a:person)-[:knows]->(:person) RETURN p").then(res=>{ - while (res.hasNext()) { - let record = res.next(); - // See path.js for more path API. - console.log(record.get("p").nodeCount); - graph.deleteGraph(); - process.exit(); - } - }) - }) - }) - }) - }) - }) - .catch(err => { - console.log(err); - }); - +(async () =>{ + await graph.query("CREATE (:person{name:'roi',age:32})"); + await graph.query("CREATE (:person{name:'amit',age:30})"); + await graph.query("MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(b)"); + + // Match query. + let res = await graph.query("MATCH (a:person)-[:knows]->(:person) RETURN a.name"); + while (res.hasNext()) { + let record = res.next(); + console.log(record.get("a.name")); + } + console.log(res.getStatistics().queryExecutionTime()); + + // Match with parameters. + let param = {'age': 30}; + res = await graph.query("MATCH (a {age: $age}) return a.name", param); + while (res.hasNext()) { + let record = res.next(); + console.log(record.get("a.name")); + } + + // Named paths matching. + res = await graph.query("MATCH p = (a:person)-[:knows]->(:person) RETURN p"); + while (res.hasNext()) { + let record = res.next(); + // See path.js for more path API. + console.log(record.get("p").nodeCount); + graph.deleteGraph(); + process.exit(); + } + + })(); ``` diff --git a/examples/package.json b/examples/package.json index f1de96eb7e..e4bcb55f1c 100644 --- a/examples/package.json +++ b/examples/package.json @@ -9,7 +9,7 @@ "url": "git://github.com/redislabs/redisgraph.js.git" }, "dependencies": { - "redisgraph.js": "^1.2.0" + "redisgraph.js": "../" }, "main": "redisGraphExample.js" } diff --git a/examples/redisGraphExample.js b/examples/redisGraphExample.js index dac36f4ed9..3442735d83 100644 --- a/examples/redisGraphExample.js +++ b/examples/redisGraphExample.js @@ -2,35 +2,44 @@ const RedisGraph = require("redisgraph.js").Graph; let graph = new RedisGraph("social"); -graph.query("CREATE (:person{name:'roi',age:32})").then(() => { - graph.query("CREATE (:person{name:'amit',age:30})").then(()=>{ - graph.query("MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(b)").then(()=>{ - graph.query("MATCH (a:person)-[:knows]->(:person) RETURN a.name").then(res=>{ - while (res.hasNext()) { - let record = res.next(); - console.log(record.get("a.name")); - } - console.log(res.getStatistics().queryExecutionTime()); - let param = {'age': 30}; - graph.query("MATCH (a {age: $age}) return a.name", param).then(res=>{ - while (res.hasNext()) { - let record = res.next(); - console.log(record.get("a.name")); - } - graph.query("MATCH p = (a:person)-[:knows]->(:person) RETURN p").then(res=>{ - while (res.hasNext()) { - let record = res.next(); - // See path.js for more path API. - console.log(record.get("p").nodeCount); - graph.deleteGraph(); - process.exit(); - } - }) - }) - }) - }) - }) - }) - .catch(err => { - console.log(err); - }); +try { + (async () => { + await graph.query("CREATE (:person{name:'roi',age:32})"); + await graph.query("CREATE (:person{name:'amit',age:30})"); + await graph.query( + "MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(b)" + ); + + // Match query. + let res = await graph.query( + "MATCH (a:person)-[:knows]->(:person) RETURN a.name" + ); + while (res.hasNext()) { + let record = res.next(); + console.log(record.get("a.name")); + } + console.log(res.getStatistics().queryExecutionTime()); + + // Match with parameters. + let param = { age: 30 }; + res = await graph.query("MATCH (a {age: $age}) return a.name", param); + while (res.hasNext()) { + let record = res.next(); + console.log(record.get("a.name")); + } + + // Named paths matching. + res = await graph.query( + "MATCH p = (a:person)-[:knows]->(:person) RETURN p" + ); + while (res.hasNext()) { + let record = res.next(); + // See path.js for more path API. + console.log(record.get("p").nodeCount); + graph.deleteGraph(); + process.exit(); + } + })(); +} catch (err) { + console.log(err); +} diff --git a/src/edge.js b/src/edge.js index d671e4b35b..ec9a52dc07 100644 --- a/src/edge.js +++ b/src/edge.js @@ -3,20 +3,36 @@ * An edge connecting two nodes. */ class Edge { - constructor(srcNode, relation, destNode, properties) { - this.id = undefined; //edge's id - set by RedisGraph - this.relation = relation; //edge's relationship type - this.srcNode = srcNode; //edge's source node - this.destNode = destNode; //edge's destinatio node - this.properties = properties; //edge's list of properties (list of Key:Value) - } + /** + * Builds an Edge object. + * @constructor + * @param {Node} srcNode - Source node of the edge. + * @param {string} relation - Relationship type of the edge. + * @param {Node} destNode - Destination node of the edge. + * @param {Map} properties - Properties map of the edge. + */ + constructor(srcNode, relation, destNode, properties) { + this.id = undefined; //edge's id - set by RedisGraph + this.relation = relation; //edge's relationship type + this.srcNode = srcNode; //edge's source node + this.destNode = destNode; //edge's destination node + this.properties = properties; //edge's list of properties (list of Key:Value) + } - setId(id) { - this.id = id; - } - toString() { - return JSON.stringify(this); + /** + * Sets the edge ID. + * @param {int} id + */ + setId(id) { + this.id = id; } + + /** + * @returns The string representation of the edge. + */ + toString() { + return JSON.stringify(this); + } } module.exports = Edge; diff --git a/src/graph.js b/src/graph.js index 5467f43535..3a857b9a9e 100644 --- a/src/graph.js +++ b/src/graph.js @@ -11,20 +11,20 @@ class Graph { * Creates a client to a specific graph running on the specific host/post * See: node_redis for more options on createClient * - * @param graphId the graph id - * @param host Redis host or node_redis client - * @param port Redis port - * @param options node_redis options + * @param {string} graphId the graph id + * @param {string | RedisClient} [host] Redis host or node_redis client + * @param {string} [port] Redis port + * @param {ClientOpts} [options] node_redis options */ constructor(graphId, host, port, options) { - this._graphId = graphId; // Graph ID - this._labels = []; // List of node labels. - this._relationshipTypes = []; // List of relation types. - this._properties = []; // List of properties. + this._graphId = graphId; // Graph ID + this._labels = []; // List of node labels. + this._relationshipTypes = []; // List of relation types. + this._properties = []; // List of properties. - this._labelsPromise = undefined; // used as a synchronization mechanizom for labels retrival - this._propertyPromise = undefined; // used as a synchronization mechanizom for property names retrival - this._relationshipPromise = undefined; // used as a synchronization mechanizom for relationship types retrival + this._labelsPromise = undefined; // used as a synchronization mechanizom for labels retrival + this._propertyPromise = undefined; // used as a synchronization mechanizom for property names retrival + this._relationshipPromise = undefined; // used as a synchronization mechanizom for relationship types retrival let client = host instanceof redis.RedisClient @@ -34,9 +34,10 @@ class Graph { } /** - * auxilary function to extract string(s) data from procedures such as: + * Auxiliary function to extract string(s) data from procedures such as: * db.labels, db.propertyKeys and db.relationshipTypes - * @param resultSet - a procedure result set + * @param {ResultSet} resultSet - a procedure result set + * @returns {string[]} strings array. */ _extractStrings(resultSet) { var strings = []; @@ -44,68 +45,73 @@ class Graph { strings.push(resultSet.next().getString(0)); } return strings; - } - - paramToString(paramValue) { - if(paramValue == null) - return "null" - let paramType = typeof(paramValue); - if(paramType == "string") { - let strValue = ""; - if (paramValue[0] != "\"") - strValue += "\""; - strValue += paramValue; - if(paramValue[paramValue.length-1] != "\"") - strValue += "\""; - return strValue; - } - if(Array.isArray(paramValue)) { - let stringsArr = new Array(paramValue.length); - for(var i = 0; i < paramValue.length; i++) { - stringsArr[i] = this.paramToString(paramValue[i]); - } - return ["[", stringsArr.join(", "),"]"].join(""); - } - return paramValue; - } + } /** - * Extracts parameters from dictionary into cypher parameters string. - * - * @param params parameters dictionary. - * @return a cypher parameters string. + * Transforms a parameter value to string. + * @param {object} paramValue + * @returns {string} the string representation of paramValue. */ - buildParamsHeader(params) { - let paramsArray = ["CYPHER"] + paramToString(paramValue) { + if (paramValue == null) return "null"; + let paramType = typeof paramValue; + if (paramType == "string") { + let strValue = ""; + if (paramValue[0] != '"') strValue += '"'; + strValue += paramValue; + if (paramValue[paramValue.length - 1] != '"') strValue += '"'; + return strValue; + } + if (Array.isArray(paramValue)) { + let stringsArr = new Array(paramValue.length); + for (var i = 0; i < paramValue.length; i++) { + stringsArr[i] = this.paramToString(paramValue[i]); + } + return ["[", stringsArr.join(", "), "]"].join(""); + } + return paramValue; + } + + /** + * Extracts parameters from dictionary into cypher parameters string. + * @param {Map} params parameters dictionary. + * @return {string} a cypher parameters string. + */ + buildParamsHeader(params) { + let paramsArray = ["CYPHER"]; - for (var key in params) { - let value = this.paramToString(params[key]); - paramsArray.push(`${key}=${value}`); - } - paramsArray.push(' '); - return paramsArray.join(' '); - } + for (var key in params) { + let value = this.paramToString(params[key]); + paramsArray.push(`${key}=${value}`); + } + paramsArray.push(" "); + return paramsArray.join(" "); + } /** - * Execute a Cypher query (async) - * - * @param query Cypher query - * @param params Parameters map - * @return a promise contains a result set + * Execute a Cypher query + * @async + * @param {string} query Cypher query + * @param {Map} [params] Parameters map + * @returns {ResultSet} a promise contains a result set */ async query(query, params) { - if(params){ - query = this.buildParamsHeader(params) + query; - } - var res = await this._sendCommand("graph.QUERY", [this._graphId, query, "--compact"]); + if (params) { + query = this.buildParamsHeader(params) + query; + } + var res = await this._sendCommand("graph.QUERY", [ + this._graphId, + query, + "--compact" + ]); var resultSet = new ResultSet(this); return resultSet.parseResponse(res); } /** - * Deletes the entire graph (async) - * - * @return a promise contains the delete operation running time statistics + * Deletes the entire graph + * @async + * @returns {ResultSet} a promise contains the delete operation running time statistics */ async deleteGraph() { var res = await this._sendCommand("graph.DELETE", [this._graphId]); @@ -118,82 +124,85 @@ class Graph { } /** - * Calls procedure - * - * @param procedure Procedure to call - * @param args Arguments to pass - * @param y Yield outputs - * @return a promise contains the procedure result set data - */ + * Calls procedure + * @param {string} procedure Procedure to call + * @param {string[]} [args] Arguments to pass + * @param {string[]} [y] Yield outputs + * @returns {ResultSet} a promise contains the procedure result set data + */ callProcedure(procedure, args = new Array(), y = new Array()) { - let q = "CALL " + procedure + "(" + args.join(',') + ")" + y.join(' '); + let q = "CALL " + procedure + "(" + args.join(",") + ")" + y.join(" "); return this.query(q); } /** * Retrieves all labels in graph. + * @async */ async labels() { if (this._labelsPromise == undefined) { - this._labelsPromise = this.callProcedure("db.labels").then(response => { - return this._extractStrings(response); - }) - this._labels = await (this._labelsPromise); + this._labelsPromise = this.callProcedure("db.labels").then( + response => { + return this._extractStrings(response); + } + ); + this._labels = await this._labelsPromise; this._labelsPromise = undefined; - } - else { + } else { await this._labelsPromise; } } /** * Retrieves all relationship types in graph. + * @async */ async relationshipTypes() { if (this._relationshipPromise == undefined) { - this._relationshipPromise = this.callProcedure("db.relationshipTypes").then(response => { + this._relationshipPromise = this.callProcedure( + "db.relationshipTypes" + ).then(response => { return this._extractStrings(response); }); - this._relationshipTypes = await (this._relationshipPromise); + this._relationshipTypes = await this._relationshipPromise; this._relationshipPromise = undefined; - } - else { + } else { await this._relationshipPromise; } } /** * Retrieves all properties in graph. + * @async */ async propertyKeys() { if (this._propertyPromise == undefined) { - this._propertyPromise = this.callProcedure("db.propertyKeys").then(response => { - return this._extractStrings(response); - }) + this._propertyPromise = this.callProcedure("db.propertyKeys").then( + response => { + return this._extractStrings(response); + } + ); this._properties = await this._propertyPromise; this._propertyPromise = undefined; - } - else{ + } else { await this._propertyPromise; } - - } /** * Retrieves label by ID. - * - * @param id internal ID of label. - * @return String label. + * @param {int} id internal ID of label. + * @returns {string} String label. */ getLabel(id) { return this._labels[id]; } /** - * Retrive all the labels from the graph and returns the wanted label - * @param id internal ID of label. - * @return String label. + * Retrieve all the labels from the graph and returns the wanted label + * @async + * @param {int} id internal ID of label. + * @returns {string} String label. */ async fetchAndGetLabel(id) { await this.labels(); @@ -202,8 +211,7 @@ class Graph { /** * Retrieves relationship type by ID. - * - * @param id internal ID of relationship type. + * @param {int} id internal ID of relationship type. * @return String relationship type. */ getRelationship(id) { @@ -211,9 +219,10 @@ class Graph { } /** - * Retrives al the relationshipe types from the graph, and returns the wanted type - * @param id internal ID of relationship type. - * @return String relationship type. + * Retrieves al the relationships types from the graph, and returns the wanted type + * @async + * @param {int} id internal ID of relationship type. + * @returns {string} String relationship type. */ async fetchAndGetRelationship(id) { await this.relationshipTypes(); @@ -222,18 +231,18 @@ class Graph { /** * Retrieves property name by ID. - * - * @param id internal ID of property. - * @return String property. + * @param {int} id internal ID of property. + * @returns {string} String property. */ getProperty(id) { return this._properties[id]; } /** - * Retrives al the properties from the graph, and returns the wanted property - * @param id internal ID of property. - * @return String property. + * Retrieves al the properties from the graph, and returns the wanted property + * @async + * @param {int} id internal ID of property. + * @returns {string} String property. */ async fetchAndGetProperty(id) { await this.propertyKeys(); diff --git a/src/label.js b/src/label.js index 39f7d2de4f..0a6d8ce184 100644 --- a/src/label.js +++ b/src/label.js @@ -9,6 +9,8 @@ var Label = Object.freeze({ RELATIONSHIPS_DELETED: "Relationships deleted", PROPERTIES_SET: "Properties set", RELATIONSHIPS_CREATED: "Relationships created", + INDICES_CREATED: "Indices created", + INDICES_DELETED: "Indices deleted", QUERY_INTERNAL_EXECUTION_TIME: "Query internal execution time" }); diff --git a/src/node.js b/src/node.js index 3018abb8ae..8b40ba856e 100644 --- a/src/node.js +++ b/src/node.js @@ -3,19 +3,32 @@ * A node within the garph. */ class Node { - constructor(label, properties) { - this.id = undefined; //node's id - set by RedisGraph - this.label = label; //node's label - this.properties = properties; //node's list of properties (list of Key:Value) - } + /** + * Builds a node object. + * @constructor + * @param {string} label - node label. + * @param {Map} properties - properties map. + */ + constructor(label, properties) { + this.id = undefined; //node's id - set by RedisGraph + this.label = label; //node's label + this.properties = properties; //node's list of properties (list of Key:Value) + } - setId(id){ - this.id = id; - } + /** + * Sets the node id. + * @param {int} id + */ + setId(id) { + this.id = id; + } - toString() { - return JSON.stringify(this); - } + /** + * @returns {string} The string representation of the node. + */ + toString() { + return JSON.stringify(this); + } } module.exports = Node; diff --git a/src/path.js b/src/path.js index 83f0bab15d..1dde85936d 100644 --- a/src/path.js +++ b/src/path.js @@ -1,46 +1,88 @@ "use strict"; class Path { - constructor(nodes, edges){ - this.nodes = nodes; - this.edges = edges; - } + /** + * @constructor + * @param {Node[]} nodes - path's node list. + * @param {Edge[]} edges - path's edge list. + */ + constructor(nodes, edges) { + this.nodes = nodes; + this.edges = edges; + } - get Nodes(){ - return this.nodes; - } + /** + * Returns the path's nodes as list. + * @returns {Node[]} path's nodes. + */ + get Nodes() { + return this.nodes; + } - get Edges(){ - return this.edges; + /** + * Returns the path's edges as list. + * @returns {Edge[]} paths' edges. + */ + get Edges() { + return this.edges; } - getNode(index){ - return this.nodes[index]; - } - - getEdge(index){ - return this.edges[index]; - } + /** + * Returns a node in a given index. + * @param {int} index + * @returns {Node} node in the given index. + */ + getNode(index) { + return this.nodes[index]; + } - get firstNode(){ - return this.nodes[0]; - } + /** + * Returns an edge in a given index. + * @param {int} index + * @returns {Edge} edge in a given index. + */ + getEdge(index) { + return this.edges[index]; + } - get lastNode(){ - return this.nodes[this.nodes.length -1]; - } + /** + * Returns the path's first node. + * @returns {Node} first node. + */ + get firstNode() { + return this.nodes[0]; + } - get nodeCount(){ - return this.nodes.length; - } + /** + * Returns the last node of the path. + * @returns {Node} last node. + */ + get lastNode() { + return this.nodes[this.nodes.length - 1]; + } - get edgeCount(){ - return this.edges.length; - } + /** + * Returns the amount of nodes in th path. + * @returns {int} amount of nodes. + */ + get nodeCount() { + return this.nodes.length; + } - toString() { - return JSON.stringify(this); - } + /** + * Returns the amount of edges in the path. + * @returns {int} amount of edges. + */ + get edgeCount() { + return this.edges.length; + } + /** + * Returns the path string representation. + * @returns {string} path string representation. + */ + toString() { + return JSON.stringify(this); + } } module.exports = Path; diff --git a/src/record.js b/src/record.js index afd1ede43e..990c5aa10f 100644 --- a/src/record.js +++ b/src/record.js @@ -3,11 +3,22 @@ * Hold a query record */ class Record { + /** + * Builds a Record object + * @constructor + * @param {string[]} header + * @param {object[]} values + */ constructor(header, values) { this._header = header; this._values = values; } + /** + * Returns a value of the given schema key or in the given position. + * @param {string | int} key + * @returns {object} Requested value. + */ get(key) { let index = key; if (typeof key === "string") { @@ -15,32 +26,51 @@ class Record { } return this._values[index]; } - + + /** + * Returns a string representation for the value of the given schema key or in the given position. + * @param {string | int} key + * @returns {string} Requested string representation of the value. + */ getString(key) { let index = key; if (typeof key === "string") { index = this._header.indexOf(key); } - + if (this._values[index]) { return this._values[index].toString(); } - + return null; } + /** + * @returns {string[]} The record header - List of strings. + */ keys() { return this._header; } + /** + * @returns {object[]} The record values - List of values. + */ values() { return this._values; } + /** + * Returns if the header contains a given key. + * @param {string} key + * @returns {boolean} true if header contains key. + */ containsKey(key) { return this._header.includes(key); } + /** + * @returns {int} The amount of values in the record. + */ size() { return this._header.length; } diff --git a/src/resultSet.js b/src/resultSet.js index 3afea4a353..554af9401b 100644 --- a/src/resultSet.js +++ b/src/resultSet.js @@ -1,263 +1,330 @@ "use strict"; const Statistics = require("./statistics"), - Record = require("./record"), - Node = require("./node"), - Edge = require("./edge"), - Path = require("./path"), - ReplyError = require("redis").ReplyError; + Record = require("./record"), + Node = require("./node"), + Edge = require("./edge"), + Path = require("./path"), + ReplyError = require("redis").ReplyError; const ResultSetColumnTypes = { - COLUMN_UNKNOWN: 0, - COLUMN_SCALAR: 1, - COLUMN_NODE: 2, - COLUMN_RELATION: 3 -} + COLUMN_UNKNOWN: 0, + COLUMN_SCALAR: 1, + COLUMN_NODE: 2, + COLUMN_RELATION: 3 +}; const ResultSetValueTypes = { - VALUE_UNKNOWN: 0, - VALUE_NULL: 1, - VALUE_STRING: 2, - VALUE_INTEGER: 3, - VALUE_BOOLEAN: 4, - VALUE_DOUBLE: 5, - VALUE_ARRAY: 6, - VALUE_EDGE: 7, - VALUE_NODE: 8, - VALUE_PATH: 9 -} + VALUE_UNKNOWN: 0, + VALUE_NULL: 1, + VALUE_STRING: 2, + VALUE_INTEGER: 3, + VALUE_BOOLEAN: 4, + VALUE_DOUBLE: 5, + VALUE_ARRAY: 6, + VALUE_EDGE: 7, + VALUE_NODE: 8, + VALUE_PATH: 9 +}; /** * Hold a query result */ class ResultSet { - constructor(graph) { - this._graph = graph; //_graph is graph api - this._position = 0; //allowing iterator like behevior - this._resultsCount = 0; //total number of records in this result set - this._header = []; //reponse schema columns labels - this._results = []; //result records - } - /** - * - * @param resp - raw response representation - the raw representation of response is at most 3 lists of objects. - * The last list is the statistics list. + * Builds an empty ResultSet object. + * @constructor + * @param {Graph} graph */ - async parseResponse(resp) { - if(Array.isArray(resp)) { - let statistics = resp[resp.length - 1]; - if(statistics instanceof ReplyError) throw statistics; - if(resp.length < 3) { - this._statistics = new Statistics(statistics); - } else { - await this.parseResults(resp); - this._resultsCount = this._results.length; - this._statistics = new Statistics(resp[2]); - } - } - else { - this._statistics = new Statistics(resp); - } - return this; - } + constructor(graph) { + this._graph = graph; //_graph is graph api + this._position = 0; //allowing iterator like behevior + this._resultsCount = 0; //total number of records in this result set + this._header = []; //reponse schema columns labels + this._results = []; //result records + } - async parseResults(resp) { - this.parseHeader(resp[0]); - await this.parseRecords(resp); - } + /** + * Parse raw response data to ResultSet object. + * @async + * @param {object[]} resp - raw response representation - the raw representation of response is at most 3 lists of objects. + * The last list is the statistics list. + */ + async parseResponse(resp) { + if (Array.isArray(resp)) { + let statistics = resp[resp.length - 1]; + if (statistics instanceof ReplyError) throw statistics; + if (resp.length < 3) { + this._statistics = new Statistics(statistics); + } else { + await this.parseResults(resp); + this._resultsCount = this._results.length; + this._statistics = new Statistics(resp[2]); + } + } else { + this._statistics = new Statistics(resp); + } + return this; + } /** - * A raw representation of a header (query response schema) is a list. - * Each entry in the list is a tuple (list of size 2). - * tuple[0] represents the type of the column, and tuple[1] represents the name of the column. - * @param rawHeader + * Parse a raw response body into header an records. + * @async + * @param {object[]} resp raw response */ - parseHeader(rawHeader) { - // An array of column name/column type pairs. - this._header = rawHeader; - // Discard header types. - this._typelessHeader = new Array(this._header.length); - for (var i = 0; i < this._header.length; i++) { - this._typelessHeader[i] = this._header[i][1]; - } - } + async parseResults(resp) { + this.parseHeader(resp[0]); + await this.parseRecords(resp); + } - /** - * The raw representation of response is at most 3 lists of objects. rawResultSet[1] contains the data records. - * Each entry in the record can be either a node, an edge or a scalar - * @param rawResultSet - */ - async parseRecords(rawResultSet) { - let result_set = rawResultSet[1]; - this._results = new Array(result_set.length); + /** + * A raw representation of a header (query response schema) is a list. + * Each entry in the list is a tuple (list of size 2). + * tuple[0] represents the type of the column, and tuple[1] represents the name of the column. + * @param {object[]} rawHeader raw header + */ + parseHeader(rawHeader) { + // An array of column name/column type pairs. + this._header = rawHeader; + // Discard header types. + this._typelessHeader = new Array(this._header.length); + for (var i = 0; i < this._header.length; i++) { + this._typelessHeader[i] = this._header[i][1]; + } + } - for (var i = 0; i < result_set.length; i++) { - let row = result_set[i]; - let record = new Array(row.length); - for (var j = 0; j < row.length; j++) { - let cell = row[j]; - let cellType = this._header[j][0]; - switch (cellType) { - case ResultSetColumnTypes.COLUMN_SCALAR: - record[j] = await this.parseScalar(cell); - break; - case ResultSetColumnTypes.COLUMN_NODE: - record[j] = await this.parseNode(cell); - break; - case ResultSetColumnTypes.COLUMN_RELATION: - record[j] = await this.parseEdge(cell); - break; - default: - console.log("Unknown column type.\n" + cellType); - break; - } - } - this._results[i] = new Record(this._typelessHeader, record); - } - } + /** + * The raw representation of response is at most 3 lists of objects. rawResultSet[1] contains the data records. + * Each entry in the record can be either a node, an edge or a scalar + * @async + * @param {object[]} rawResultSet raw result set representation + */ + async parseRecords(rawResultSet) { + let result_set = rawResultSet[1]; + this._results = new Array(result_set.length); - async parseEntityProperties(props) { - // [[name, value, value type] X N] - let properties = {} - for (var i = 0; i < props.length; i++) { - let prop = props[i]; - var propIndex = prop[0]; - let prop_name = this._graph.getProperty(propIndex); - // will try to get the right property for at most 10 times - var tries = 0; - while (prop_name == undefined && tries < 10) { - prop_name = await this._graph.fetchAndGetProperty(propIndex); - tries++; - } - if (prop_name == undefined) { - console.warn("unable to retrive property name value for propety index " + propIndex); - } - let prop_value = await this.parseScalar(prop.slice(1, prop.length)); - properties[prop_name] = prop_value; - } - return properties; - } + for (var i = 0; i < result_set.length; i++) { + let row = result_set[i]; + let record = new Array(row.length); + for (var j = 0; j < row.length; j++) { + let cell = row[j]; + let cellType = this._header[j][0]; + switch (cellType) { + case ResultSetColumnTypes.COLUMN_SCALAR: + record[j] = await this.parseScalar(cell); + break; + case ResultSetColumnTypes.COLUMN_NODE: + record[j] = await this.parseNode(cell); + break; + case ResultSetColumnTypes.COLUMN_RELATION: + record[j] = await this.parseEdge(cell); + break; + default: + console.log("Unknown column type.\n" + cellType); + break; + } + } + this._results[i] = new Record(this._typelessHeader, record); + } + } - async parseNode(cell) { - // Node ID (integer), - // [label string offset (integer)], - // [[name, value, value type] X N] + /** + * Parse raw entity properties representation into a Map + * @async + * @param {object[]} props raw properties representation + * @returns {Map} Map with the parsed properties. + */ + async parseEntityProperties(props) { + // [[name, value, value type] X N] + let properties = {}; + for (var i = 0; i < props.length; i++) { + let prop = props[i]; + var propIndex = prop[0]; + let prop_name = this._graph.getProperty(propIndex); + // will try to get the right property for at most 10 times + var tries = 0; + while (prop_name == undefined && tries < 10) { + prop_name = await this._graph.fetchAndGetProperty(propIndex); + tries++; + } + if (prop_name == undefined) { + console.warn( + "unable to retrive property name value for propety index " + + propIndex + ); + } + let prop_value = await this.parseScalar(prop.slice(1, prop.length)); + properties[prop_name] = prop_value; + } + return properties; + } - let node_id = cell[0]; - let label = this._graph.getLabel(cell[1][0]); - // will try to get the right label for at most 10 times - var tries = 0; - while (label == undefined && tries < 10) { - label = await this._graph.fetchAndGetLabel(cell[1][0]); - tries++; - } - if (label == undefined) { - console.warn("unable to retrive label value for label index " + cell[1][0]); - } - let properties = await this.parseEntityProperties(cell[2]); - let node = new Node(label, properties); - node.setId(node_id); - return node; - } + /** + * Parse raw node representation into a Node object. + * @async + * @param {object[]} cell raw node representation. + * @returns {Node} Node object. + */ + async parseNode(cell) { + // Node ID (integer), + // [label string offset (integer)], + // [[name, value, value type] X N] + + let node_id = cell[0]; + let label = this._graph.getLabel(cell[1][0]); + // will try to get the right label for at most 10 times + var tries = 0; + while (label == undefined && tries < 10) { + label = await this._graph.fetchAndGetLabel(cell[1][0]); + tries++; + } + if (label == undefined) { + console.warn( + "unable to retrive label value for label index " + cell[1][0] + ); + } + let properties = await this.parseEntityProperties(cell[2]); + let node = new Node(label, properties); + node.setId(node_id); + return node; + } - async parseEdge(cell) { - // Edge ID (integer), - // reltype string offset (integer), - // src node ID offset (integer), - // dest node ID offset (integer), - // [[name, value, value type] X N] + /** + * Parse a raw edge representation into an Edge object. + * @async + * @param {object[]} cell raw edge representation + * @returns {Edge} Edge object. + */ + async parseEdge(cell) { + // Edge ID (integer), + // reltype string offset (integer), + // src node ID offset (integer), + // dest node ID offset (integer), + // [[name, value, value type] X N] - let edge_id = cell[0]; - let relation = this._graph.getRelationship(cell[1]); - // will try to get the right relationship type for at most 10 times - var tries = 0; - while (relation == undefined && tries < 10) { - relation = await this._graph.fetchAndGetRelationship(cell[1]) - tries++; - } - if (relation == undefined) { - console.warn("unable to retrive relationship type value for relationship index " + cell[1]); - } - let src_node_id = cell[2]; - let dest_node_id = cell[3]; - let properties = await this.parseEntityProperties(cell[4]); - let edge = new Edge(src_node_id, relation, dest_node_id, properties); - edge.setId(edge_id); - return edge; - } + let edge_id = cell[0]; + let relation = this._graph.getRelationship(cell[1]); + // will try to get the right relationship type for at most 10 times + var tries = 0; + while (relation == undefined && tries < 10) { + relation = await this._graph.fetchAndGetRelationship(cell[1]); + tries++; + } + if (relation == undefined) { + console.warn( + "unable to retrive relationship type value for relationship index " + + cell[1] + ); + } + let src_node_id = cell[2]; + let dest_node_id = cell[3]; + let properties = await this.parseEntityProperties(cell[4]); + let edge = new Edge(src_node_id, relation, dest_node_id, properties); + edge.setId(edge_id); + return edge; + } - async parseArray(rawArray) { - for (var i = 0; i < rawArray.length; i++) { - rawArray[i] = await this.parseScalar(rawArray[i]); - } - return rawArray; - } + /** + * Parse and in-place replace raw array into an array of values or objects. + * @async + * @param {object[]} rawArray raw array representation + * @returns {object[]} Parsed array. + */ + async parseArray(rawArray) { + for (var i = 0; i < rawArray.length; i++) { + rawArray[i] = await this.parseScalar(rawArray[i]); + } + return rawArray; + } - async parsePath(rawPath) { - let nodes = await this.parseScalar(rawPath[0]); - let edges = await this.parseScalar(rawPath[1]); - return new Path(nodes, edges); - } + /** + * Parse a raw path representation into Path object. + * @async + * @param {object[]} rawPath raw path representation + * @returns {Path} Path object. + */ + async parsePath(rawPath) { + let nodes = await this.parseScalar(rawPath[0]); + let edges = await this.parseScalar(rawPath[1]); + return new Path(nodes, edges); + } - async parseScalar(cell) { - let scalar_type = cell[0]; - let value = cell[1]; - let scalar = undefined; + /** + * Parse a raw value into its actual value. + * @async + * @param {object[]} cell raw value representation + * @returns {object} Actual value - scalar, array, Node, Edge, Path + */ + async parseScalar(cell) { + let scalar_type = cell[0]; + let value = cell[1]; + let scalar = undefined; - switch (scalar_type) { - case ResultSetValueTypes.VALUE_NULL: - scalar = null; - break; - case ResultSetValueTypes.VALUE_STRING: - scalar = String(value); - break; - case ResultSetValueTypes.VALUE_INTEGER: - case ResultSetValueTypes.VALUE_DOUBLE: - scalar = Number(value); - break; - case ResultSetValueTypes.VALUE_BOOLEAN: - if (value === "true") { - scalar = true; - } else if (value === "false") { - scalar = false; - } else { - console.log("Unknown boolean type\n"); - } - break; - case ResultSetValueTypes.VALUE_ARRAY: - scalar = this.parseArray(value); - break; - case ResultSetValueTypes.VALUE_NODE: - scalar = await this.parseNode(value); - break; - case ResultSetValueTypes.VALUE_EDGE: - scalar = await this.parseEdge(value); - break; - case ResultSetValueTypes.VALUE_PATH: - scalar = await this.parsePath(value); - break; - case ResultSetValueTypes.VALUE_UNKNOWN: - console.log("Unknown scalar type\n"); - break; - } - return scalar; - } + switch (scalar_type) { + case ResultSetValueTypes.VALUE_NULL: + scalar = null; + break; + case ResultSetValueTypes.VALUE_STRING: + scalar = String(value); + break; + case ResultSetValueTypes.VALUE_INTEGER: + case ResultSetValueTypes.VALUE_DOUBLE: + scalar = Number(value); + break; + case ResultSetValueTypes.VALUE_BOOLEAN: + if (value === "true") { + scalar = true; + } else if (value === "false") { + scalar = false; + } else { + console.log("Unknown boolean type\n"); + } + break; + case ResultSetValueTypes.VALUE_ARRAY: + scalar = this.parseArray(value); + break; + case ResultSetValueTypes.VALUE_NODE: + scalar = await this.parseNode(value); + break; + case ResultSetValueTypes.VALUE_EDGE: + scalar = await this.parseEdge(value); + break; + case ResultSetValueTypes.VALUE_PATH: + scalar = await this.parsePath(value); + break; + case ResultSetValueTypes.VALUE_UNKNOWN: + console.log("Unknown scalar type\n"); + break; + } + return scalar; + } - getHeader() { - return this._typelessHeader; - } + /** + * @returns {string[] }ResultSet's header. + */ + getHeader() { + return this._typelessHeader; + } - hasNext() { - return this._position < this._resultsCount; - } + /** + * @returns {boolean} If the ResultSet object can return additional records. + */ + hasNext() { + return this._position < this._resultsCount; + } - next() { - return this._results[this._position++]; - } + /** + * @returns {Record} The current record. + */ + next() { + return this._results[this._position++]; + } - getStatistics() { - return this._statistics; - } + /** + * @returns {Statistics} ResultsSet's statistics. + */ + getStatistics() { + return this._statistics; + } } module.exports = ResultSet; diff --git a/src/statistics.js b/src/statistics.js index 0618a0aa9d..f6042bc32d 100644 --- a/src/statistics.js +++ b/src/statistics.js @@ -2,18 +2,26 @@ const Label = require("./label"); class Statistics { + /** + * Builds a query statistics object out of raw data. + * @constructor + * @param {object[]} raw - raw data. + */ constructor(raw) { this._raw = raw; } + /** + * Returns a statistics value according to the statistics label. + * @param {Label} label - Statistics label. + */ getStringValue(label) { return this.getStatistics()[label]; } /** * Return the query statistics - * - * @return statistics object + * @return {Statistics} statistics object */ getStatistics() { if (!this._statistics) { @@ -26,41 +34,85 @@ class Statistics { return this._statistics; } + /** + * Returns the integer value of a requested label. + * @param {Label} label + * @returns {int} The actual value if exists, 0 otherwise. + */ getIntValue(label) { let value = this.getStringValue(label); return value ? parseInt(value) : 0; } - + /** + * Returns the float value of a requested label. + * @param {Label} label + * @returns {float} The actual value if exists, 0 otherwise. + */ getFloatValue(label) { let value = this.getStringValue(label); return value ? parseFloat(value) : 0; } + /** + * @returns {int} The amount of nodes created by th query. + */ nodesCreated() { return this.getIntValue(Label.NODES_CREATED); } + /** + * @returns {int} The amount of nodes deleted by the query. + */ nodesDeleted() { return this.getIntValue(Label.NODES_DELETED); } + /** + * @returns {int} The amount of labels created by the query. + */ labelsAdded() { return this.getIntValue(Label.LABELS_ADDED); } + /** + * @returns {int} The amount of relationships deleted by the query. + */ relationshipsDeleted() { return this.getIntValue(Label.RELATIONSHIPS_DELETED); } + /** + * @returns {int} The amount of relationships created by the query. + */ relationshipsCreated() { return this.getIntValue(Label.RELATIONSHIPS_CREATED); } + /** + * @returns {int} The amount of properties set by the query. + */ propertiesSet() { return this.getIntValue(Label.PROPERTIES_SET); } + /** + * @returns {int} The amount of indices created by the query. + */ + indicesCreated() { + return this.getIntValue(Label.INDICES_CREATED); + } + + /** + * @returns {int} The amount of indices deleted by the query. + */ + indicesDeleted() { + return this.getIntValue(Label.INDICES_DELETED); + } + + /** + * @returns {float} The query execution time in ms. + */ queryExecutionTime() { return this.getFloatValue(Label.QUERY_INTERNAL_EXECUTION_TIME); } diff --git a/test/pathBuilder.js b/test/pathBuilder.js index 74eb46a0d8..aaaf07030d 100644 --- a/test/pathBuilder.js +++ b/test/pathBuilder.js @@ -1,36 +1,37 @@ "use strict"; const Node = require("../src/node"), - Edge = require("../src/edge"), - Path = require("../src/path"); + Edge = require("../src/edge"), + Path = require("../src/path"); class PathBuilder { - constructor(nodeCount){ - this.nodes = new Array(); - this.edges = new Array(); - this.currentAppendClass = Node; - } + constructor(nodeCount) { + this.nodes = new Array(); + this.edges = new Array(); + this.currentAppendClass = Node; + } - append(obj){ - if(! obj instanceof this.currentAppendClass) throw "Error in path build insertion order and types." - if(obj instanceof Node) return this._appendNode(obj); - else return this._appendEdge(obj); - } + append(obj) { + if (!obj instanceof this.currentAppendClass) + throw "Error in path build insertion order and types."; + if (obj instanceof Node) return this._appendNode(obj); + else return this._appendEdge(obj); + } - build(){ - return new Path(this.nodes, this.edges); - } + build() { + return new Path(this.nodes, this.edges); + } - _appendNode(node){ - this.nodes.push(node); - this.currentAppendClass = Edge; - return this; - } + _appendNode(node) { + this.nodes.push(node); + this.currentAppendClass = Edge; + return this; + } - _appendEdge(edge){ - this.edges.push(edge); - this.currentAppendClass = Node; - return this; - } + _appendEdge(edge) { + this.edges.push(edge); + this.currentAppendClass = Node; + return this; + } } module.exports = PathBuilder; diff --git a/test/redisGraphAPITest.js b/test/redisGraphAPITest.js index fa99835b39..7a7835bc61 100644 --- a/test/redisGraphAPITest.js +++ b/test/redisGraphAPITest.js @@ -1,395 +1,426 @@ "use strict"; const assert = require("assert"), - redis = require("redis"), - Label = require("../src/label"), - RedisGraph = require("../src/graph"), - Node = require("../src/node"), - Edge = require("../src/edge"), - Path = require("../src/path"), - PathBuilder = require("./pathBuilder"), - deepEqual = require('deep-equal'); - -describe('RedisGraphAPI Test', function () { - const api = new RedisGraph("social"); - - beforeEach(() => { - return api.deleteGraph().catch(() => { }); - }); - - it("test bring your client", () => { - return new RedisGraph("social", redis.createClient()); - }); - - it("test Create Node", (done) => { - // Create a node - api.query("CREATE ({name:'roi', age:34})").then(result => { - assert.ok(!result.hasNext()); - assert.equal(1, result.getStatistics().nodesCreated()); - assert.ifError( - result.getStatistics().getStringValue(Label.NODES_DELETED) - ); - assert.ifError( - result.getStatistics().getStringValue(Label.RELATIONSHIPS_CREATED) - ); - assert.ifError( - result.getStatistics().getStringValue(Label.RELATIONSHIPS_DELETED) - ); - assert.equal(2, result.getStatistics().propertiesSet()); - assert.ok(result.getStatistics().queryExecutionTime()); // not 0 - assert.ok(result.getStatistics().getStringValue(Label.QUERY_INTERNAL_EXECUTION_TIME)); // exsits - done(); - }).catch(error => { - console.log(error); - }) - }); - - it("test Create Labeled Node", (done) => { - // Create a node with a label - api.query("CREATE (:human {name:'danny', age:12})").then(result => { - assert.ok(!result.hasNext()); - assert.equal( - "1", - result.getStatistics().getStringValue(Label.NODES_CREATED) - ); - assert.equal( - "2", - result.getStatistics().getStringValue(Label.PROPERTIES_SET) - ); - assert.ok( - result - .getStatistics() - .getStringValue(Label.QUERY_INTERNAL_EXECUTION_TIME) - ); - done(); - }).catch(error => { - console.log(error); - }) - }); - - it("test Connect Nodes", (done) => { - // Create both source and destination nodes - let createResult1 = api.query("CREATE (:person {name:'roi', age:34})"); - let createResult2 = api.query("CREATE (:person {name:'amit', age:32})"); - - // Connect source and destination nodes. - api.query( - "MATCH (a:person {name:'roi'}), (b:person {name:'amit'}) CREATE (a)-[:knows]->(b)" - ).then(matchResult => { - assert.ok(!matchResult.hasNext()); - assert.ifError( - matchResult.getStatistics().getStringValue(Label.NODES_CREATED) - ); - assert.ifError( - matchResult.getStatistics().getStringValue(Label.PROPERTIES_SET) - ); - assert.equal(1, matchResult.getStatistics().relationshipsCreated()); - assert.equal(0, matchResult.getStatistics().relationshipsDeleted()); - assert.ok( - matchResult - .getStatistics() - .getStringValue(Label.QUERY_INTERNAL_EXECUTION_TIME) - ); - done(); - }).catch(error => { - console.log(error); - }) - }); - - it('test Query', (done) => { - // Create both source and destination nodes - api.query("CREATE (r:human {name:'roi', age:34}), (a:human {name:'amit', age:32}), (r)-[:knows]->(a)") - .then((createResult) => { - // Query - api.query("MATCH (r:human)-[:knows]->(a:human) RETURN r.age, r.name") - .then((resultSet) => { - assert.ok(resultSet.hasNext()); - assert.equal(0, resultSet.getStatistics().nodesCreated()); - assert.equal(0, resultSet.getStatistics().nodesDeleted()); - assert.equal(0, resultSet.getStatistics().labelsAdded()); - assert.equal(0, resultSet.getStatistics().propertiesSet()); - assert.equal(0, resultSet.getStatistics().relationshipsCreated()); - assert.equal(0, resultSet.getStatistics().relationshipsDeleted()); - assert.ok(resultSet.getStatistics().getStringValue(Label.QUERY_INTERNAL_EXECUTION_TIME)); - - assert.deepStrictEqual(['r.age', 'r.name'], resultSet.getHeader()); - - let record = resultSet.next(); - assert.equal(34, record.get(0)); - assert.equal("34", record.getString(0)); - assert.equal("roi", record.getString(1)); - assert.equal("roi", record.getString("r.name")); - - assert.deepStrictEqual(['r.age', 'r.name'], record.keys()); - assert.deepStrictEqual([34, 'roi'], record.values()); - assert.equal(false, record.containsKey("aa")); - assert.equal(true, record.containsKey("r.name")); - assert.equal(2, record.size()); - done() - }) - }).catch(error => { - console.log(error); - }) - - }); - - it('test query full entity', (done) => { - // Create both source and destination nodes - api.query("CREATE (:person {name:'roi', age:34})").then(response => { - api.query("CREATE (:person{name:'amit',age:30})").then(response2 => { - api.query("MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') " + - "CREATE (a)-[:knows{place:'TLV', since:2000,doubleValue:3.14, boolValue:false, nullValue:null}]->(b)") - .then((createResult) => { - // Query - api.query("MATCH (a:person)-[r:knows]->(b:person) RETURN a,r") - - .then((resultSet) => { - assert.ok(resultSet.hasNext()); - assert.equal(0, resultSet.getStatistics().nodesCreated()); - assert.equal(0, resultSet.getStatistics().nodesDeleted()); - assert.equal(0, resultSet.getStatistics().labelsAdded()); - assert.equal(0, resultSet.getStatistics().propertiesSet()); - assert.equal(0, resultSet.getStatistics().relationshipsCreated()); - assert.equal(0, resultSet.getStatistics().relationshipsDeleted()); - assert.ok(resultSet.getStatistics().getStringValue(Label.QUERY_INTERNAL_EXECUTION_TIME)); - - assert.deepStrictEqual(['a', 'r'], resultSet.getHeader()); - - let record = resultSet.next(); - let n = record.get(0); - assert.equal(34, n.properties['age']); - assert.equal("roi", n.properties['name']); - assert.equal("person", n.label); - assert.equal(0, n.id); - var r = record.get(1); - assert.equal("knows", r.relation); - assert.equal(0, r.id); - assert.equal(0, r.srcNode); - assert.equal(1, r.destNode); - assert.equal("TLV", r.properties['place']); - assert.equal(2000, r.properties["since"]); - assert.equal(3.14, r.properties["doubleValue"]); - assert.equal(false, r.properties["boolValue"]); - assert.equal(undefined, r.properties["nullValue"]); - done(); - }) - }) - }) - }).catch(error => { - console.log(error); - }) - }) - - it('test null value to string', (done) => { - api.query("CREATE ( {nullValue:null} )").then(response => { - api.query("MATCH (n) RETURN n.nullValue").then(resultSet => { - assert.ok(resultSet.hasNext()) - let record = resultSet.next(); - assert.equal(undefined, record.get(0)); - assert.equal(null, record.getString(0)) - done(); - }) - }).catch(error => { - console.log(error); - }) - }) - - it('test empty result set', (done) => { - api.query("CREATE (r:human {name:'roi', age:34})"). - then(response => { - api.query("CREATE (:person{name:'amit',age:30})") - .then(response => { - api.query("MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(a)") - .then(response2 => { - api.query("MATCH (a:person)-[:knows]->(:person) RETURN a") - .then(resultSet => { - assert.ok(!resultSet.hasNext()); - assert.equal(resultSet.getHeader()[0], 'a'); - assert.equal(0, resultSet.getStatistics().nodesCreated()); - assert.equal(0, resultSet.getStatistics().nodesDeleted()); - assert.equal(0, resultSet.getStatistics().labelsAdded()); - assert.equal(0, resultSet.getStatistics().propertiesSet()); - assert.equal(0, resultSet.getStatistics().relationshipsCreated()); - assert.equal(0, resultSet.getStatistics().relationshipsDeleted()); - assert.ok(resultSet.getStatistics().getStringValue(Label.QUERY_INTERNAL_EXECUTION_TIME)); - done(); - }) - }) - }) - }).catch(error => { - console.log(error); - }) - }) - - it('test array', (done) => { - api.query("CREATE (:person{name:'a',age:32,array:[0,1,2]})").then(() => { - api.query("CREATE (:person{name:'b',age:30,array:[3,4,5]})").then(() => { - api.query("WITH [0,1,2] as x return x").then(resultSet => { - assert.ok(resultSet.hasNext()); - assert.deepStrictEqual(['x'], resultSet.getHeader()); - let record = resultSet.next(); - assert.deepStrictEqual([0, 1, 2], record.get(0)); - assert.ok(!resultSet.hasNext()); - - api.query("MATCH(n) return collect(n) as x").then(newResultSet => { - assert.ok(newResultSet.hasNext()); - var nodeA = new Node("person", { name: 'a', age: 32, array: [0, 1, 2] }); - nodeA.setId(0); - var nodeB = new Node("person", { name: 'b', age: 30, array: [3, 4, 5] }); - nodeB.setId(1); - assert.deepStrictEqual(['x'], newResultSet.getHeader()); - let record = newResultSet.next(); - assert.deepStrictEqual([nodeA, nodeB], record.get(0)); - done(); - }) - }) - }) - }).catch(error => { - console.log(error); - }) - }) - - it('test multi thread', (done) => { - api.query("CREATE (:person {name:'roi', age:34})").then(response => { - var promises = [] - for (var i = 0; i < 10; i++) { - promises.push(api.query("MATCH (a:person {name:'roi'}) RETURN a")); - } - Promise.all(promises).then(values => { - for (var resultSet of values) { - let record = resultSet.next(); - let n = record.get(0); - assert.equal(34, n.properties['age']); - assert.equal("roi", n.properties['name']); - assert.equal("person", n.label); - assert.equal(0, n.id); - } - done(); - }) - }).catch(error => { - console.log(error); - }) - }) - - it('testCompileTimeException', (done) => { - api.query("CREATE ()").then(response => { - api.query("RETURN toUpper(5)").then(response => assert(false)).catch (err => { - assert(err instanceof redis.ReplyError); - assert.equal(err.message, "Type mismatch: expected String but was Integer"); - done(); - }) - }).catch(error => { - console.log(error); - }) - }) - - it('testRuntimeException', (done) => { - api.query("CREATE ({val:5})").then(response => { - api.query("MATCH (n) RETURN toUpper(n.val)").then(response => assert(false)).catch (err => { - assert(err instanceof redis.ReplyError); - assert.equal(err.message, "Type mismatch: expected String but was Integer"); - done(); - }) - }).catch(error => { - console.log(error); - }) - }) - - it('unitTestPath', ()=>{ - let node0 = new Node("L1", {}); - node0.setId(0); - let node1 = new Node("L1", {}); - node1.setId(1); - - let edge01 = new Edge(0, "R1", 1, {}); - edge01.setId(0); - - let path01 = new PathBuilder().append(node0).append(edge01).append(node1).build(); - - assert.equal(1, path01.edgeCount); - assert.equal(2, path01.nodeCount); - assert.deepEqual(node0, path01.firstNode); - assert.deepEqual(node0, path01.getNode(0)); - assert.deepEqual(node1, path01.lastNode); - assert.deepEqual(node1, path01.getNode(1)); - assert.deepEqual(edge01, path01.getEdge(0)); - assert.deepEqual([node0, node1], path01.nodes); - assert.deepEqual([edge01], path01.edges); - - }) - - it('testPath', (done)=>{ - api.query("CREATE (:L1)-[:R1]->(:L1)-[:R1]->(:L1)").then(response => { - api.query("MATCH p = (:L1)-[:R1*]->(:L1) RETURN p").then(response =>{ - let node0 = new Node("L1", {}); - node0.setId(0); - let node1 = new Node("L1", {}); - node1.setId(1); - let node2 = new Node("L1", {}); - node2.setId(2); - let edge01 = new Edge(0, "R1", 1, {}); - edge01.setId(0); - let edge12 = new Edge(1, "R1", 2, {}); - edge12.setId(1); - - let path01 = new PathBuilder().append(node0).append(edge01).append(node1).build(); - let path12 = new PathBuilder().append(node1).append(edge12).append(node2).build(); - let path02 = new PathBuilder().append(node0).append(edge01).append(node1).append(edge12).append(node2).build(); - - let paths = new Set([path01, path12, path02]); - while(response.hasNext()){ - let p = response.next().get("p"); - let pInPaths = false; - let path = null; - let pathsIterator = paths.values(); - for( pathsIterator; path = pathsIterator.next().value;){ - if(deepEqual(p ,path)){ - pInPaths = true; - break; - } - } - assert(pInPaths); - paths.delete(path); - } - assert.equal(0, paths.size); - done(); - }) - }).catch(error => { - console.log(error); - }) - }) - - it('testParam', (done) => { - let params = [1, 2.3, true, false, null, "str", [1,2,3], ["1", "2", "3"], null]; - let promises =[]; - for (var i =0; i < params.length; i++){ - let param = {'param':params[i]}; - promises.push(api.query("RETURN $param", param)); - } - Promise.all(promises).then(values => { - for (var i =0; i < values.length; i++) { - let resultSet = values[i]; - let record = resultSet.next(); - let param = record.get(0); - assert.deepEqual(param, params[i]); - } - done(); - }).catch(error => { - console.log(error); - }) - }) - it('testMissingParameter', (done)=> { - api.query("RETURN $param").then(response => assert(false)).catch (err => { - assert(err instanceof redis.ReplyError); - assert.equal(err.message, "Missing parameters"); - api.query("RETURN $param", null).then(response => assert(false)).catch (err => { - assert(err instanceof redis.ReplyError); - assert.equal(err.message, "Missing parameters"); - api.query("RETURN $param", {}).then(response => assert(false)).catch (err => { - assert(err instanceof redis.ReplyError); - assert.equal(err.message, "Missing parameters"); - done(); - }) - }) - }) - }) + redis = require("redis"), + Label = require("../src/label"), + RedisGraph = require("../src/graph"), + Node = require("../src/node"), + Edge = require("../src/edge"), + Path = require("../src/path"), + PathBuilder = require("./pathBuilder"), + deepEqual = require("deep-equal"); + +describe("RedisGraphAPI Test", () => { + const api = new RedisGraph("social"); + + beforeEach(() => { + return api.deleteGraph().catch(() => {}); + }); + + it("test bring your client", () => { + return new RedisGraph("social", redis.createClient()); + }); + + it("test Create Node", async () => { + // Create a node + let result = await api.query("CREATE ({name:'roi', age:34})"); + assert.ok(!result.hasNext()); + assert.equal(1, result.getStatistics().nodesCreated()); + assert.ifError( + result.getStatistics().getStringValue(Label.NODES_DELETED) + ); + assert.ifError( + result.getStatistics().getStringValue(Label.RELATIONSHIPS_CREATED) + ); + assert.ifError( + result.getStatistics().getStringValue(Label.RELATIONSHIPS_DELETED) + ); + assert.equal(2, result.getStatistics().propertiesSet()); + assert.ok(result.getStatistics().queryExecutionTime()); // not 0 + assert.ok( + result + .getStatistics() + .getStringValue(Label.QUERY_INTERNAL_EXECUTION_TIME) + ); // exsits + assert.ok( + result + .getStatistics() + .getStringValue(Label.QUERY_INTERNAL_EXECUTION_TIME) + ); // exsits + assert.ok( + result + .getStatistics() + .getStringValue(Label.QUERY_INTERNAL_EXECUTION_TIME) + ); // exsits + }); + + it("test Create Labeled Node", async () => { + // Create a node with a label + let result = await api.query("CREATE (:human {name:'danny', age:12})"); + assert.ok(!result.hasNext()); + assert.equal( + "1", + result.getStatistics().getStringValue(Label.NODES_CREATED) + ); + assert.equal( + "2", + result.getStatistics().getStringValue(Label.PROPERTIES_SET) + ); + assert.ok( + result + .getStatistics() + .getStringValue(Label.QUERY_INTERNAL_EXECUTION_TIME) + ); + }); + + it("test Connect Nodes", async () => { + // Create both source and destination nodes + await api.query("CREATE (:person {name:'roi', age:34})"); + await api.query("CREATE (:person {name:'amit', age:32})"); + + // Connect source and destination nodes. + let matchResult = await api.query( + "MATCH (a:person {name:'roi'}), \ + (b:person {name:'amit'}) CREATE (a)-[:knows]->(b)" + ); + assert.ok(!matchResult.hasNext()); + assert.ifError( + matchResult.getStatistics().getStringValue(Label.NODES_CREATED) + ); + assert.ifError( + matchResult.getStatistics().getStringValue(Label.PROPERTIES_SET) + ); + assert.equal(1, matchResult.getStatistics().relationshipsCreated()); + assert.equal(0, matchResult.getStatistics().relationshipsDeleted()); + assert.ok( + matchResult + .getStatistics() + .getStringValue(Label.QUERY_INTERNAL_EXECUTION_TIME) + ); + }); + + it("test Query", async () => { + // Create both source and destination nodes + await api.query( + "CREATE (r:human {name:'roi', age:34}), (a:human {name:'amit', age:32}), (r)-[:knows]->(a)" + ); + // Query + let resultSet = await api.query( + "MATCH (r:human)-[:knows]->(a:human) RETURN r.age, r.name" + ); + assert.ok(resultSet.hasNext()); + assert.equal(0, resultSet.getStatistics().nodesCreated()); + assert.equal(0, resultSet.getStatistics().nodesDeleted()); + assert.equal(0, resultSet.getStatistics().labelsAdded()); + assert.equal(0, resultSet.getStatistics().propertiesSet()); + assert.equal(0, resultSet.getStatistics().relationshipsCreated()); + assert.equal(0, resultSet.getStatistics().relationshipsDeleted()); + assert.ok( + resultSet + .getStatistics() + .getStringValue(Label.QUERY_INTERNAL_EXECUTION_TIME) + ); + + assert.deepStrictEqual(["r.age", "r.name"], resultSet.getHeader()); + + let record = resultSet.next(); + assert.equal(34, record.get(0)); + assert.equal("34", record.getString(0)); + assert.equal("roi", record.getString(1)); + assert.equal("roi", record.getString("r.name")); + + assert.deepStrictEqual(["r.age", "r.name"], record.keys()); + assert.deepStrictEqual([34, "roi"], record.values()); + assert.equal(false, record.containsKey("aa")); + assert.equal(true, record.containsKey("r.name")); + assert.equal(2, record.size()); + }); + + it("test query full entity", async () => { + // Create both source and destination nodes + await api.query("CREATE (:person {name:'roi', age:34})"); + await api.query("CREATE (:person{name:'amit',age:30})"); + await api.query( + "MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') \ + CREATE (a)-[:knows{place:'TLV', since:2000,doubleValue:3.14, boolValue:false, nullValue:null}]->(b)" + ); + // Query + let resultSet = await api.query( + "MATCH (a:person)-[r:knows]->(b:person) RETURN a,r" + ); + + assert.ok(resultSet.hasNext()); + assert.equal(0, resultSet.getStatistics().nodesCreated()); + assert.equal(0, resultSet.getStatistics().nodesDeleted()); + assert.equal(0, resultSet.getStatistics().labelsAdded()); + assert.equal(0, resultSet.getStatistics().propertiesSet()); + assert.equal(0, resultSet.getStatistics().relationshipsCreated()); + assert.equal(0, resultSet.getStatistics().relationshipsDeleted()); + assert.ok( + resultSet + .getStatistics() + .getStringValue(Label.QUERY_INTERNAL_EXECUTION_TIME) + ); + + assert.deepStrictEqual(["a", "r"], resultSet.getHeader()); + + let record = resultSet.next(); + let n = record.get(0); + assert.equal(34, n.properties["age"]); + assert.equal("roi", n.properties["name"]); + assert.equal("person", n.label); + assert.equal(0, n.id); + var r = record.get(1); + assert.equal("knows", r.relation); + assert.equal(0, r.id); + assert.equal(0, r.srcNode); + assert.equal(1, r.destNode); + assert.equal("TLV", r.properties["place"]); + assert.equal(2000, r.properties["since"]); + assert.equal(3.14, r.properties["doubleValue"]); + assert.equal(false, r.properties["boolValue"]); + assert.equal(undefined, r.properties["nullValue"]); + }); + + it("test null value to string", async () => { + await api.query("CREATE ( {nullValue:null} )"); + let resultSet = await api.query("MATCH (n) RETURN n.nullValue"); + assert.ok(resultSet.hasNext()); + let record = resultSet.next(); + assert.equal(undefined, record.get(0)); + assert.equal(null, record.getString(0)); + }); + + it("test empty result set", async () => { + await api.query("CREATE (r:human {name:'roi', age:34})"); + await api.query("CREATE (:person{name:'amit',age:30})"); + await api.query( + "MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') \ + CREATE (a)-[:knows]->(a)" + ); + + let resultSet = await api.query( + "MATCH (a:person)-[:knows]->(:person) RETURN a" + ); + + assert.ok(!resultSet.hasNext()); + assert.equal(resultSet.getHeader()[0], "a"); + assert.equal(0, resultSet.getStatistics().nodesCreated()); + assert.equal(0, resultSet.getStatistics().nodesDeleted()); + assert.equal(0, resultSet.getStatistics().labelsAdded()); + assert.equal(0, resultSet.getStatistics().propertiesSet()); + assert.equal(0, resultSet.getStatistics().relationshipsCreated()); + assert.equal(0, resultSet.getStatistics().relationshipsDeleted()); + assert.ok( + resultSet + .getStatistics() + .getStringValue(Label.QUERY_INTERNAL_EXECUTION_TIME) + ); + }); + + it("test array", async () => { + await api.query("CREATE (:person{name:'a',age:32,array:[0,1,2]})"); + await api.query("CREATE (:person{name:'b',age:30,array:[3,4,5]})"); + let resultSet = await api.query("WITH [0,1,2] as x return x"); + assert.ok(resultSet.hasNext()); + assert.deepStrictEqual(["x"], resultSet.getHeader()); + let record = resultSet.next(); + assert.deepStrictEqual([0, 1, 2], record.get(0)); + assert.ok(!resultSet.hasNext()); + + let newResultSet = await api.query("MATCH(n) return collect(n) as x"); + assert.ok(newResultSet.hasNext()); + var nodeA = new Node("person", { + name: "a", + age: 32, + array: [0, 1, 2] + }); + nodeA.setId(0); + var nodeB = new Node("person", { + name: "b", + age: 30, + array: [3, 4, 5] + }); + nodeB.setId(1); + assert.deepStrictEqual(["x"], newResultSet.getHeader()); + record = newResultSet.next(); + assert.deepStrictEqual([nodeA, nodeB], record.get(0)); + }); + + it("test multi thread", async () => { + await api.query("CREATE (:person {name:'roi', age:34})"); + var promises = []; + for (var i = 0; i < 10; i++) { + promises.push(api.query("MATCH (a:person {name:'roi'}) RETURN a")); + } + let values = await Promise.all(promises); + for (var resultSet of values) { + let record = resultSet.next(); + let n = record.get(0); + assert.equal(34, n.properties["age"]); + assert.equal("roi", n.properties["name"]); + assert.equal("person", n.label); + assert.equal(0, n.id); + } + }); + + it("testCompileTimeException", async () => { + await api.query("CREATE ()"); + try { + await api.query("RETURN toUpper(5)"); + assert(false); + } catch (err) { + assert(err instanceof redis.ReplyError); + assert.equal( + err.message, + "Type mismatch: expected String but was Integer" + ); + } + }); + + it("testRuntimeException", async () => { + await api.query("CREATE ({val:5})"); + try { + await api.query("MATCH (n) RETURN toUpper(n.val)"); + assert(false); + } catch (err) { + assert(err instanceof redis.ReplyError); + assert.equal( + err.message, + "Type mismatch: expected String but was Integer" + ); + } + }); + + it("unitTestPath", () => { + let node0 = new Node("L1", {}); + node0.setId(0); + let node1 = new Node("L1", {}); + node1.setId(1); + + let edge01 = new Edge(0, "R1", 1, {}); + edge01.setId(0); + + let path01 = new PathBuilder() + .append(node0) + .append(edge01) + .append(node1) + .build(); + + assert.equal(1, path01.edgeCount); + assert.equal(2, path01.nodeCount); + assert.deepEqual(node0, path01.firstNode); + assert.deepEqual(node0, path01.getNode(0)); + assert.deepEqual(node1, path01.lastNode); + assert.deepEqual(node1, path01.getNode(1)); + assert.deepEqual(edge01, path01.getEdge(0)); + assert.deepEqual([node0, node1], path01.nodes); + assert.deepEqual([edge01], path01.edges); + }); + + it("testPath", async () => { + await api.query("CREATE (:L1)-[:R1]->(:L1)-[:R1]->(:L1)"); + let response = await api.query( + "MATCH p = (:L1)-[:R1*]->(:L1) RETURN p" + ); + let node0 = new Node("L1", {}); + node0.setId(0); + let node1 = new Node("L1", {}); + node1.setId(1); + let node2 = new Node("L1", {}); + node2.setId(2); + let edge01 = new Edge(0, "R1", 1, {}); + edge01.setId(0); + let edge12 = new Edge(1, "R1", 2, {}); + edge12.setId(1); + + let path01 = new PathBuilder() + .append(node0) + .append(edge01) + .append(node1) + .build(); + let path12 = new PathBuilder() + .append(node1) + .append(edge12) + .append(node2) + .build(); + let path02 = new PathBuilder() + .append(node0) + .append(edge01) + .append(node1) + .append(edge12) + .append(node2) + .build(); + + let paths = new Set([path01, path12, path02]); + while (response.hasNext()) { + let p = response.next().get("p"); + let pInPaths = false; + let path = null; + let pathsIterator = paths.values(); + for (pathsIterator; (path = pathsIterator.next().value); ) { + if (deepEqual(p, path)) { + pInPaths = true; + break; + } + } + assert(pInPaths); + paths.delete(path); + } + assert.equal(0, paths.size); + }); + + it("testParam", async () => { + let params = [ + 1, + 2.3, + true, + false, + null, + "str", + [1, 2, 3], + ["1", "2", "3"], + null + ]; + let promises = []; + for (var i = 0; i < params.length; i++) { + let param = { param: params[i] }; + promises.push(api.query("RETURN $param", param)); + } + let values = await Promise.all(promises); + for (var i = 0; i < values.length; i++) { + let resultSet = values[i]; + let record = resultSet.next(); + let param = record.get(0); + assert.deepEqual(param, params[i]); + } + }); + + it("testMissingParameter", async () => { + try { + await api.query("RETURN $param"); + assert(false); + } catch (err) { + assert(err instanceof redis.ReplyError); + assert.equal(err.message, "Missing parameters"); + } + }); + + it("testIndexResponse", async () => { + let response = await api.query("CREATE INDEX ON :person(age)"); + assert.equal(1, response.getStatistics().indicesCreated()); + response = await api.query("CREATE INDEX ON :person(age)"); + assert.equal(0, response.getStatistics().indicesCreated()); + response = await api.query("DROP INDEX ON :person(age)"); + assert.equal(1, response.getStatistics().indicesDeleted()); + try { + await api.query("DROP INDEX ON :person(age)"); + assert(false); + } catch (err) { + assert(err instanceof redis.ReplyError); + assert.equal( + err.message, + "ERR Unable to drop index on :person(age): no such index." + ); + } + }); });