From 3e4959503ce9a4304a06b94943bbf109b22cd120 Mon Sep 17 00:00:00 2001 From: Robert Kesterson Date: Fri, 5 Feb 2021 18:22:23 -0500 Subject: [PATCH] Converted to typescript --- .circleci/config.yml | 1 + .gitignore | 1 + ...isGraphExample.js => redisGraphExample.ts} | 5 +- examples/tsconfig.json | 20 + index.js | 13 - package.json | 9 +- src/{edge.js => edge.ts} | 11 +- src/graph.ts | 279 ++++++++++++++ src/index.ts | 11 + src/{label.js => label.ts} | 4 +- src/{node.js => node.ts} | 11 +- src/{path.js => path.ts} | 27 +- src/record.js | 80 ---- src/record.ts | 71 ++++ src/resultSet.js | 359 ----------------- src/resultSet.ts | 364 ++++++++++++++++++ src/{statistics.js => statistics.ts} | 38 +- test/pathBuilder.js | 53 ++- test/recordTest.js | 3 +- test/redisGraphAPITest.js | 6 +- tsconfig.json | 20 + 21 files changed, 853 insertions(+), 533 deletions(-) rename examples/{redisGraphExample.js => redisGraphExample.ts} (90%) create mode 100644 examples/tsconfig.json delete mode 100644 index.js rename src/{edge.js => edge.ts} (80%) create mode 100644 src/graph.ts create mode 100644 src/index.ts rename src/{label.js => label.ts} (89%) rename src/{node.js => node.ts} (78%) rename src/{path.js => path.ts} (80%) delete mode 100644 src/record.js create mode 100644 src/record.ts delete mode 100644 src/resultSet.js create mode 100644 src/resultSet.ts rename src/{statistics.js => statistics.ts} (82%) create mode 100644 tsconfig.json diff --git a/.circleci/config.yml b/.circleci/config.yml index bf3749fd7c..083a8d5ffd 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -45,6 +45,7 @@ jobs: # run tests! - run: sudo npm install -g istanbul codecov + - run: npx tsc - run: istanbul cover ./node_modules/mocha/bin/_mocha -- -R spec --exit - early_return_for_forked_pull_requests - run: codecov -t ${CODECOV_TOKEN} diff --git a/.gitignore b/.gitignore index 4e2197e5b7..4b4963a413 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ yarn.lock examples/node_modules coverage/ .DS_Store +dist/ diff --git a/examples/redisGraphExample.js b/examples/redisGraphExample.ts similarity index 90% rename from examples/redisGraphExample.js rename to examples/redisGraphExample.ts index 8df71c3f4f..7f6391ea91 100644 --- a/examples/redisGraphExample.js +++ b/examples/redisGraphExample.ts @@ -1,4 +1,5 @@ -const RedisGraph = require("redisgraph.js").Graph; + +import {Graph as RedisGraph} from 'redisgraph.js'; let graph = new RedisGraph("social"); @@ -18,7 +19,7 @@ try { let record = res.next(); console.log(record.get("a.name")); } - console.log(res.getStatistics().queryExecutionTime()); + console.log(res.getStatistics()?.queryExecutionTime()); // Match with parameters. let param = { age: 30 }; diff --git a/examples/tsconfig.json b/examples/tsconfig.json new file mode 100644 index 0000000000..417be15b60 --- /dev/null +++ b/examples/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "UMD", + "lib": ["es5"], + "strict": true, + "allowJs": true, + "inlineSourceMap": true, + "inlineSources": true, + "outDir": "./dist", + "moduleResolution": "node", + "downlevelIteration": true, + "rootDir": "./", + "baseUrl": "./", + "declaration": true, + + }, + "include": ["./"], + "exclude": ["dist"] +} diff --git a/index.js b/index.js deleted file mode 100644 index df7890a273..0000000000 --- a/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const Record = require("./src/record"), - Graph = require("./src/graph"), - ResultSet = require("./src/resultSet"), - Statistics = require("./src/statistics"), - Label = require("./src/label"); - -module.exports = { - Record: Record, - Graph: Graph, - ResultSet: ResultSet, - Statistics: Statistics, - Label: Label -}; diff --git a/package.json b/package.json index e097b1a2f6..8fe38d0124 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,13 @@ "redis": "^3.0.2" }, "devDependencies": { - "mocha": "^7.0.1" + "@types/deep-equal": "^1.0.1", + "@types/redis": "^2.8.28", + "mocha": "^7.0.1", + "typescript": "^4.1.3" }, "scripts": { - "test": "mocha --exit" + "test": "npx tsc && mocha --exit" }, - "main": "index.js" + "main": "dist/index.js" } diff --git a/src/edge.js b/src/edge.ts similarity index 80% rename from src/edge.js rename to src/edge.ts index ec9a52dc07..ddb7e1231e 100644 --- a/src/edge.js +++ b/src/edge.ts @@ -1,8 +1,10 @@ -"use strict"; +import {Node} from "./node"; /** * An edge connecting two nodes. */ -class Edge { +export class Edge { + + private id?: number; /** * Builds an Edge object. * @constructor @@ -11,7 +13,7 @@ class Edge { * @param {Node} destNode - Destination node of the edge. * @param {Map} properties - Properties map of the edge. */ - constructor(srcNode, relation, destNode, properties) { + constructor(private srcNode: Node, private relation: string, private destNode: Node, private properties: Map) { this.id = undefined; //edge's id - set by RedisGraph this.relation = relation; //edge's relationship type this.srcNode = srcNode; //edge's source node @@ -23,7 +25,7 @@ class Edge { * Sets the edge ID. * @param {int} id */ - setId(id) { + setId(id:number) { this.id = id; } @@ -35,4 +37,3 @@ class Edge { } } -module.exports = Edge; diff --git a/src/graph.ts b/src/graph.ts new file mode 100644 index 0000000000..6ee8bf3883 --- /dev/null +++ b/src/graph.ts @@ -0,0 +1,279 @@ +import * as redis from "redis"; +import { ClientOpts, RedisClient } from "redis"; +import * as util from "util"; +import { ResultSet } from "./resultSet"; + +function isString(value: any): value is string { + return typeof value === "string"; +} + +/** + * RedisGraph client + */ +export class Graph { + private _labels: string[] = []; // List of node labels. + private _relationshipTypes: string[] = []; // List of relation types. + private _properties: string[] = []; // List of properties. + + private _labelsPromise: Promise | undefined; // used as a synchronization mechanizom for labels retrival + private _propertyPromise: Promise | undefined; // used as a synchronization mechanizom for property names retrival + private _relationshipPromise: Promise | undefined; // used as a synchronization mechanizom for relationship types retrival + private _client: RedisClient; + private _sendCommand: + | ((command: string) => Promise) + | ((command: string, args?: any[]) => Promise); + /** + * Creates a client to a specific graph running on the specific host/post + * See: node_redis for more options on createClient + * + * @param {string} graphId the graph id + * @param {string | RedisClient} [host] Redis host or node_redis client + * @param {string | int} [port] Redis port + * @param {ClientOpts} [options] node_redis options + */ + constructor( + private _graphId: string, + host?: string | RedisClient, + port?: number, + options?: ClientOpts + ) { + 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._client = + host instanceof redis.RedisClient + ? host + : port + ? redis.createClient(port, host, options) + : redis.createClient(); + this._sendCommand = util + .promisify(this._client.send_command) + .bind(this._client); + } + /** + * Closes the client. + */ + close() { + this._client.quit(); + } + + /** + * Auxiliary function to extract string(s) data from procedures such as: + * db.labels, db.propertyKeys and db.relationshipTypes + * @param {ResultSet} resultSet - a procedure result set + * @returns {string[]} strings array. + */ + _extractStrings(resultSet: ResultSet): string[] { + var strings = []; + while (resultSet.hasNext()) { + strings.push(resultSet.next().getString(0)); + } + return strings; + } + + /** + * Transforms a parameter value to string. + * @param {object} paramValue + * @returns {string} the string representation of paramValue. + */ + paramToString(paramValue: null | string | any[]): string { + if (paramValue == null) return "null"; + if (isString(paramValue)) { + let strValue = ""; + paramValue = paramValue.replace(/[\\"']/g, "\\$&"); + if (paramValue[0] != '"') strValue += '"'; + strValue += paramValue; + if (!paramValue.endsWith('"') || paramValue.endsWith('\\"')) + 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: any) { + let paramsArray = ["CYPHER"]; + + 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 {string} query Cypher query + * @param {Map} [params] Parameters map + * @returns {ResultSet} a promise contains a result set + */ + async query(query: string, params?: object) { + 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 + * @returns {ResultSet} a promise contains the delete operation running time statistics + */ + async deleteGraph() { + var res = await this._sendCommand("graph.DELETE", [this._graphId]); + //clear internal graph state + this._labels = []; + this._relationshipTypes = []; + this._properties = []; + var resultSet = new ResultSet(this); + return resultSet.parseResponse(res); + } + + /** + * 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: string, args = new Array(), y = new Array()) { + 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 = undefined; + } 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) => { + return this._extractStrings(response); + }); + this._relationshipTypes = await this._relationshipPromise; + this._relationshipPromise = undefined; + } 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._properties = await this._propertyPromise; + this._propertyPromise = undefined; + } else { + await this._propertyPromise; + } + } + + /** + * Retrieves label by ID. + * @param {int} id internal ID of label. + * @returns {string} String label. + */ + getLabel(id: number): string { + return this._labels[id]; + } + + /** + * 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: number) { + await this.labels(); + return this._labels[id]; + } + + /** + * Retrieves relationship type by ID. + * @param {int} id internal ID of relationship type. + * @return String relationship type. + */ + getRelationship(id: number) { + return this._relationshipTypes[id]; + } + + /** + * 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: number): Promise { + await this.relationshipTypes(); + return this._relationshipTypes[id]; + } + + /** + * Retrieves property name by ID. + * @param {int} id internal ID of property. + * @returns {string} String property. + */ + getProperty(id: number): string { + return this._properties[id]; + } + + /** + * 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: number): Promise { + await this.propertyKeys(); + return this._properties[id]; + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000000..f34241abeb --- /dev/null +++ b/src/index.ts @@ -0,0 +1,11 @@ +export * from './record'; +export * from './graph'; +export * from './resultSet'; +export * from './statistics'; +export * from './label'; +export * from './node'; +export * from './path'; +export * from './edge'; + + + diff --git a/src/label.js b/src/label.ts similarity index 89% rename from src/label.js rename to src/label.ts index af8a41e8fe..1c9e48e198 100644 --- a/src/label.js +++ b/src/label.ts @@ -2,7 +2,7 @@ /** * Different Statistics labels */ -var Label = Object.freeze({ +export const Label = Object.freeze({ LABELS_ADDED: "Labels added", NODES_CREATED: "Nodes created", NODES_DELETED: "Nodes deleted", @@ -15,4 +15,4 @@ var Label = Object.freeze({ QUERY_INTERNAL_EXECUTION_TIME: "Query internal execution time" }); -module.exports = Label; + diff --git a/src/node.js b/src/node.ts similarity index 78% rename from src/node.js rename to src/node.ts index 8b40ba856e..b25913d5d0 100644 --- a/src/node.js +++ b/src/node.ts @@ -2,14 +2,16 @@ /** * A node within the garph. */ -class Node { +export class Node { + + private id?: number; /** * Builds a node object. * @constructor * @param {string} label - node label. * @param {Map} properties - properties map. */ - constructor(label, properties) { + constructor(public label: string, public properties: Map) { 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) @@ -19,16 +21,15 @@ class Node { * Sets the node id. * @param {int} id */ - setId(id) { + setId(id: number) { this.id = id; } /** * @returns {string} The string representation of the node. */ - toString() { + toString(): string { return JSON.stringify(this); } } -module.exports = Node; diff --git a/src/path.js b/src/path.ts similarity index 80% rename from src/path.js rename to src/path.ts index 1dde85936d..ba30bae64f 100644 --- a/src/path.js +++ b/src/path.ts @@ -1,11 +1,15 @@ "use strict"; -class Path { + +import { Edge } from "edge"; +import { Node } from "node"; + +export class Path { /** * @constructor * @param {Node[]} nodes - path's node list. * @param {Edge[]} edges - path's edge list. */ - constructor(nodes, edges) { + constructor(public nodes: Node[], public edges: Edge[]) { this.nodes = nodes; this.edges = edges; } @@ -14,7 +18,7 @@ class Path { * Returns the path's nodes as list. * @returns {Node[]} path's nodes. */ - get Nodes() { + get Nodes(): Node[] { return this.nodes; } @@ -22,7 +26,7 @@ class Path { * Returns the path's edges as list. * @returns {Edge[]} paths' edges. */ - get Edges() { + get Edges(): Edge[] { return this.edges; } @@ -31,7 +35,7 @@ class Path { * @param {int} index * @returns {Node} node in the given index. */ - getNode(index) { + getNode(index: number) { return this.nodes[index]; } @@ -40,7 +44,7 @@ class Path { * @param {int} index * @returns {Edge} edge in a given index. */ - getEdge(index) { + getEdge(index: number) { return this.edges[index]; } @@ -48,7 +52,7 @@ class Path { * Returns the path's first node. * @returns {Node} first node. */ - get firstNode() { + get firstNode(): Node { return this.nodes[0]; } @@ -56,7 +60,7 @@ class Path { * Returns the last node of the path. * @returns {Node} last node. */ - get lastNode() { + get lastNode():Node { return this.nodes[this.nodes.length - 1]; } @@ -64,7 +68,7 @@ class Path { * Returns the amount of nodes in th path. * @returns {int} amount of nodes. */ - get nodeCount() { + get nodeCount(): number { return this.nodes.length; } @@ -72,7 +76,7 @@ class Path { * Returns the amount of edges in the path. * @returns {int} amount of edges. */ - get edgeCount() { + get edgeCount(): number { return this.edges.length; } @@ -80,9 +84,8 @@ class Path { * Returns the path string representation. * @returns {string} path string representation. */ - toString() { + toString(): string { return JSON.stringify(this); } } -module.exports = Path; diff --git a/src/record.js b/src/record.js deleted file mode 100644 index ac8f0a7657..0000000000 --- a/src/record.js +++ /dev/null @@ -1,80 +0,0 @@ -"use strict"; -/** - * 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") { - index = this._header.indexOf(key); - } - 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); - } - - let value = this._values[index]; - if (value !== undefined && value !== null) { - return value.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; - } -} - -module.exports = Record; diff --git a/src/record.ts b/src/record.ts new file mode 100644 index 0000000000..f49ea89067 --- /dev/null +++ b/src/record.ts @@ -0,0 +1,71 @@ +"use strict"; +/** + * Hold a query record + */ +export class Record { + /** + * Builds a Record object + * @constructor + * @param {string[]} header + * @param {object[]} values + */ + constructor(private _header: string[], private _values: any[]) {} + + /** + * Returns a value of the given schema key or in the given position. + * @param {string | int} key + * @returns {object} Requested value. + */ + get(key: string | number) { + const index: number = + typeof key === "string" ? this._header.indexOf(key) : key; + 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: string | number) { + const index: number = + typeof key === "string" ? this._header.indexOf(key) : key; + + let value = this._values[index]; + if (value !== undefined && value !== null) { + return value.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: string) { + 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 deleted file mode 100644 index 8ec5675dd8..0000000000 --- a/src/resultSet.js +++ /dev/null @@ -1,359 +0,0 @@ -"use strict"; -const Statistics = require("./statistics"), - 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, -}; - -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_MAP: 10, -}; - -/** - * Hold a query result - */ -class ResultSet { - /** - * Builds an empty ResultSet object. - * @constructor - * @param {Graph} graph - */ - 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 - } - - /** - * 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; - } - - /** - * Parse a raw response body into header an records. - * @async - * @param {object[]} resp raw response - */ - async parseResults(resp) { - this.parseHeader(resp[0]); - await this.parseRecords(resp); - } - - /** - * 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]; - } - } - - /** - * 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); - - 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); - } - } - - /** - * 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; - } - - /** - * 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; - } - - /** - * 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; - } - - /** - * 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; - } - - /** - * 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); - } - - /** - * Parse a raw map representation into Map object. - * @async - * @param {object[]} rawMap raw map representation - * @returns {Map} Map object. - */ - async parseMap(rawMap) { - let m = {}; - for (var i = 0; i < rawMap.length; i+=2) { - var key = rawMap[i]; - m[key] = await this.parseScalar(rawMap[i+1]); - } - - return m; - } - - /** - * 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_MAP: - scalar = await this.parseMap(value); - break; - - case ResultSetValueTypes.VALUE_UNKNOWN: - console.log("Unknown scalar type\n"); - break; - } - return scalar; - } - - /** - * @returns {string[] }ResultSet's header. - */ - getHeader() { - return this._typelessHeader; - } - - /** - * @returns {boolean} If the ResultSet object can return additional records. - */ - hasNext() { - return this._position < this._resultsCount; - } - - /** - * @returns {Record} The current record. - */ - next() { - return this._results[this._position++]; - } - - /** - * @returns {Statistics} ResultsSet's statistics. - */ - getStatistics() { - return this._statistics; - } - - /** - * @returns {int} Result set size. - */ - size() { - return this._resultsCount; - } -} - -module.exports = ResultSet; diff --git a/src/resultSet.ts b/src/resultSet.ts new file mode 100644 index 0000000000..47c282b1ca --- /dev/null +++ b/src/resultSet.ts @@ -0,0 +1,364 @@ +import { Edge } from "./edge"; +import { Graph } from "./graph"; +import { Node } from "./node"; +import { Path } from "./path"; +import { Record } from "./record"; +import { ReplyError } from "redis"; +import { Statistics } from "./statistics"; + +const ResultSetColumnTypes = { + 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_MAP: 10, +}; + +/** + * Hold a query result + */ +export class ResultSet { + /** + * Builds an empty ResultSet object. + * @constructor + * @param {Graph} graph + */ + private _position = 0; //allowing iterator like behevior + private _resultsCount = 0; //total number of records in this result set + private _header: any[] = []; //reponse schema columns labels + private _results: any[] = []; //result records + private _statistics?: Statistics; + + private _typelessHeader: string[]; + constructor(private _graph: 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 + this._typelessHeader = []; + } + + /** + * 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: any[]) { + 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; + } + + /** + * Parse a raw response body into header an records. + * @async + * @param {object[]} resp raw response + */ + async parseResults(resp: any[]) { + this.parseHeader(resp[0]); + await this.parseRecords(resp); + } + + /** + * 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: any[]) { + // 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]; + } + } + + /** + * 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: any[]) { + let result_set = rawResultSet[1]; + this._results = new Array(result_set.length); + + 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); + } + } + + /** + * 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: any[]) { + // [[name, value, value type] X N] + let properties: any = {}; + 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; + } + + /** + * Parse raw node representation into a Node object. + * @async + * @param {object[]} cell raw node representation. + * @returns {Node} Node object. + */ + async parseNode(cell: any[]) { + // 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; + } + + /** + * Parse a raw edge representation into an Edge object. + * @async + * @param {object[]} cell raw edge representation + * @returns {Edge} Edge object. + */ + async parseEdge(cell: any[]): Promise { + // 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; + } + + /** + * 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: any[]): Promise { + for (var i = 0; i < rawArray.length; i++) { + rawArray[i] = await this.parseScalar(rawArray[i]); + } + return rawArray; + } + + /** + * Parse a raw path representation into Path object. + * @async + * @param {object[]} rawPath raw path representation + * @returns {Path} Path object. + */ + async parsePath(rawPath: any[]): Promise { + let nodes = await this.parseScalar(rawPath[0]); + let edges = await this.parseScalar(rawPath[1]); + return new Path(nodes, edges); + } + + /** + * Parse a raw map representation into Map object. + * @async + * @param {object[]} rawMap raw map representation + * @returns {Map} Map object. + */ + async parseMap(rawMap: any[]): Promise { + let m: any = {}; + for (var i = 0; i < rawMap.length; i += 2) { + var key = rawMap[i]; + m[key] = await this.parseScalar(rawMap[i + 1]); + } + + return m; + } + + /** + * 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: any[]): Promise { + 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_MAP: + scalar = await this.parseMap(value); + break; + + case ResultSetValueTypes.VALUE_UNKNOWN: + console.log("Unknown scalar type\n"); + break; + } + return scalar; + } + + /** + * @returns {string[] }ResultSet's header. + */ + getHeader() { + return this._typelessHeader; + } + + /** + * @returns {boolean} If the ResultSet object can return additional records. + */ + hasNext() { + return this._position < this._resultsCount; + } + + /** + * @returns {Record} The current record. + */ + next() { + return this._results[this._position++]; + } + + /** + * @returns {Statistics} ResultsSet's statistics. + */ + getStatistics() { + return this._statistics; + } + + /** + * @returns {int} Result set size. + */ + size() { + return this._resultsCount; + } +} diff --git a/src/statistics.js b/src/statistics.ts similarity index 82% rename from src/statistics.js rename to src/statistics.ts index 98c4c786be..036b59ba25 100644 --- a/src/statistics.js +++ b/src/statistics.ts @@ -1,21 +1,24 @@ "use strict"; -const Label = require("./label"); -class Statistics { +import { Label } from "./label"; + + +export class Statistics { /** * Builds a query statistics object out of raw data. * @constructor * @param {object[]} raw - raw data. */ - constructor(raw) { - this._raw = raw; + constructor(private _raw: any[]) { } + + private _statistics: any; /** * Returns a statistics value according to the statistics label. * @param {Label} label - Statistics label. */ - getStringValue(label) { + getStringValue(label: string) { return this.getStatistics()[label]; } @@ -39,7 +42,7 @@ class Statistics { * @param {Label} label * @returns {int} The actual value if exists, 0 otherwise. */ - getIntValue(label) { + getIntValue(label: string): number { let value = this.getStringValue(label); return value ? parseInt(value) : 0; } @@ -49,7 +52,7 @@ class Statistics { * @param {Label} label * @returns {float} The actual value if exists, 0 otherwise. */ - getFloatValue(label) { + getFloatValue(label: string): number { let value = this.getStringValue(label); return value ? parseFloat(value) : 0; } @@ -57,72 +60,71 @@ class Statistics { /** * @returns {int} The amount of nodes created by th query. */ - nodesCreated() { + nodesCreated(): number { return this.getIntValue(Label.NODES_CREATED); } /** * @returns {int} The amount of nodes deleted by the query. */ - nodesDeleted() { + nodesDeleted(): number { return this.getIntValue(Label.NODES_DELETED); } /** * @returns {int} The amount of labels created by the query. */ - labelsAdded() { + labelsAdded(): number { return this.getIntValue(Label.LABELS_ADDED); } /** * @returns {int} The amount of relationships deleted by the query. */ - relationshipsDeleted() { + relationshipsDeleted(): number { return this.getIntValue(Label.RELATIONSHIPS_DELETED); } /** * @returns {int} The amount of relationships created by the query. */ - relationshipsCreated() { + relationshipsCreated(): number { return this.getIntValue(Label.RELATIONSHIPS_CREATED); } /** * @returns {int} The amount of properties set by the query. */ - propertiesSet() { + propertiesSet(): number { return this.getIntValue(Label.PROPERTIES_SET); } /** * @returns {int} The amount of indices created by the query. */ - indicesCreated() { + indicesCreated(): number { return this.getIntValue(Label.INDICES_CREATED); } /** * @returns {int} The amount of indices deleted by the query. */ - indicesDeleted() { + indicesDeleted(): number { return this.getIntValue(Label.INDICES_DELETED); } /** * @returns {boolean} The execution plan was cached on RedisGraph. */ - cachedExecution() { + cachedExecution(): boolean { return this.getIntValue(Label.CACHED_EXECUTION) == 1; } /** * @returns {float} The query execution time in ms. */ - queryExecutionTime() { + queryExecutionTime(): number { return this.getFloatValue(Label.QUERY_INTERNAL_EXECUTION_TIME); } } -module.exports = Statistics; diff --git a/test/pathBuilder.js b/test/pathBuilder.js index aaaf07030d..4e00e1ed67 100644 --- a/test/pathBuilder.js +++ b/test/pathBuilder.js @@ -1,37 +1,34 @@ -"use strict"; -const Node = require("../src/node"), - Edge = require("../src/edge"), - Path = require("../src/path"); +const { Node, Edge, Path } = require("../dist/index"); 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/recordTest.js b/test/recordTest.js index d0140fa4fb..5c47669861 100644 --- a/test/recordTest.js +++ b/test/recordTest.js @@ -1,6 +1,7 @@ "use strict"; const assert = require("assert"), - Record = require("../src/record"); + RedisGraph = require("../dist/index"), + Record = RedisGraph.Record; describe("Record Test", () => { describe('getString()', () => { diff --git a/test/redisGraphAPITest.js b/test/redisGraphAPITest.js index 5db5a9f5bd..bdb050ae6a 100644 --- a/test/redisGraphAPITest.js +++ b/test/redisGraphAPITest.js @@ -1,11 +1,7 @@ "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"), + { Label, Graph: RedisGraph, Node, Edge, Path } = require("../dist/index"), PathBuilder = require("./pathBuilder"), deepEqual = require("deep-equal"); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..5730da2442 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "UMD", + "lib": ["es5"], + "strict": true, + "allowJs": true, + "inlineSourceMap": true, + "inlineSources": true, + "outDir": "dist", + "moduleResolution": "node", + "downlevelIteration": true, + "rootDir": "src", + "baseUrl": "src", + "declaration": true, + + }, + "include": ["src/**/*"], + "exclude": ["dist"] +}