From e24972d9f997dc08b22599bdd74337db1fef42b4 Mon Sep 17 00:00:00 2001 From: leibale Date: Wed, 26 Oct 2022 14:06:14 -0400 Subject: [PATCH 1/5] fix #2189 - add graph --compact support --- packages/graph/README.md | 31 +- packages/graph/lib/commands/QUERY.spec.ts | 13 +- packages/graph/lib/commands/QUERY.ts | 30 +- packages/graph/lib/commands/QUERY_RO.spec.ts | 22 -- packages/graph/lib/commands/RO_QUERY.spec.ts | 17 + .../lib/commands/{QUERY_RO.ts => RO_QUERY.ts} | 8 +- packages/graph/lib/commands/index.spec.ts | 62 +++ packages/graph/lib/commands/index.ts | 87 ++++- packages/graph/lib/graph.spec.ts | 159 ++++++++ packages/graph/lib/graph.ts | 354 ++++++++++++++++++ packages/graph/lib/index.ts | 1 + 11 files changed, 711 insertions(+), 73 deletions(-) delete mode 100644 packages/graph/lib/commands/QUERY_RO.spec.ts create mode 100644 packages/graph/lib/commands/RO_QUERY.spec.ts rename packages/graph/lib/commands/{QUERY_RO.ts => RO_QUERY.ts} (72%) create mode 100644 packages/graph/lib/commands/index.spec.ts create mode 100644 packages/graph/lib/graph.spec.ts create mode 100644 packages/graph/lib/graph.ts diff --git a/packages/graph/README.md b/packages/graph/README.md index 595e0226b25..7074f0859f6 100644 --- a/packages/graph/README.md +++ b/packages/graph/README.md @@ -2,34 +2,27 @@ Example usage: ```javascript -import { createClient } from 'redis'; +import { createClient, Graph } from 'redis'; const client = createClient(); client.on('error', (err) => console.log('Redis Client Error', err)); await client.connect(); -await client.graph.query( - 'graph', - "CREATE (:Rider { name: 'Buzz Aldrin' })-[:rides]->(:Team { name: 'Apollo' })" -); +const graph = new Graph('graph', client); -const result = await client.graph.query( +await graph.query( 'graph', - `MATCH (r:Rider)-[:rides]->(t:Team) WHERE t.name = 'Apollo' RETURN r.name, t.name` + 'CREATE (:Rider { name: "Buzz Aldrin" })-[:rides]->(:Team { name: "Apollo" })' ); -console.log(result); -``` +const result = await graph.roQuery( + 'MATCH (r:Rider)-[:rides]->(t:Team { name: "Apollo" }) RETURN r.name AS riderName, t.name AS teamName' +); -Output from console log: -```json -{ - headers: [ 'r.name', 't.name' ], - data: [ [ 'Buzz Aldrin', 'Apollo' ] ], - metadata: [ - 'Cached execution: 0', - 'Query internal execution time: 0.431700 milliseconds' - ] -} +console.log(result.data); +// [{ +// riderName: 'Buzz Aldrin', +// teamName: 'Apollo' +// }] ``` diff --git a/packages/graph/lib/commands/QUERY.spec.ts b/packages/graph/lib/commands/QUERY.spec.ts index 44492d75d27..c8a9a20372b 100644 --- a/packages/graph/lib/commands/QUERY.spec.ts +++ b/packages/graph/lib/commands/QUERY.spec.ts @@ -5,18 +5,13 @@ import { transformArguments } from './QUERY'; describe('QUERY', () => { it('transformArguments', () => { assert.deepEqual( - transformArguments('key', '*', 100), - ['GRAPH.QUERY', 'key', '*', '100'] + transformArguments('key', 'query'), + ['GRAPH.QUERY', 'key', 'query'] ); }); testUtils.testWithClient('client.graph.query', async client => { - await client.graph.query('key', - "CREATE (r:human {name:'roi', age:34}), (a:human {name:'amit', age:32}), (r)-[:knows]->(a)" - ); - const reply = await client.graph.query('key', - "MATCH (r:human)-[:knows]->(a:human) RETURN r.age, r.name" - ); - assert.equal(reply.data.length, 1); + const { data } = await client.graph.query('key', 'RETURN 0'); + assert.deepEqual(data, [[0]]); }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/graph/lib/commands/QUERY.ts b/packages/graph/lib/commands/QUERY.ts index 408443186d5..741cc6a3601 100644 --- a/packages/graph/lib/commands/QUERY.ts +++ b/packages/graph/lib/commands/QUERY.ts @@ -1,24 +1,26 @@ import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands/index'; -import { pushQueryArguments } from '.'; +import { pushQueryArguments, QueryOptionsBackwardCompatible } from '.'; export const FIRST_KEY_INDEX = 1; export function transformArguments( graph: RedisCommandArgument, query: RedisCommandArgument, - timeout?: number + options?: QueryOptionsBackwardCompatible, + compact?: boolean ): RedisCommandArguments { return pushQueryArguments( ['GRAPH.QUERY'], graph, query, - timeout + options, + compact ); } type Headers = Array; -type Data = Array>; +type Data = Array; type Metadata = Array; @@ -26,16 +28,26 @@ type QueryRawReply = [ headers: Headers, data: Data, metadata: Metadata +] | [ + metadata: Metadata ]; -interface QueryReply { - headers: Headers, - data: Data, - metadata: Metadata +export type QueryReply = { + headers: undefined; + data: undefined; + metadata: Metadata; +} | { + headers: Headers; + data: Data; + metadata: Metadata; }; export function transformReply(reply: QueryRawReply): QueryReply { - return { + return reply.length === 1 ? { + headers: undefined, + data: undefined, + metadata: reply[0] + } : { headers: reply[0], data: reply[1], metadata: reply[2] diff --git a/packages/graph/lib/commands/QUERY_RO.spec.ts b/packages/graph/lib/commands/QUERY_RO.spec.ts deleted file mode 100644 index 78814603aca..00000000000 --- a/packages/graph/lib/commands/QUERY_RO.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { strict as assert } from 'assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './QUERY_RO'; - -describe('QUERY_RO', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', '*', 100), - ['GRAPH.RO_QUERY', 'key', '*', '100'] - ); - }); - - testUtils.testWithClient('client.graph.queryRo', async client => { - await client.graph.query('key', - "CREATE (r:human {name:'roi', age:34}), (a:human {name:'amit', age:32}), (r)-[:knows]->(a)" - ); - const reply = await client.graph.queryRo('key', - "MATCH (r:human)-[:knows]->(a:human) RETURN r.age, r.name" - ); - assert.equal(reply.data.length, 1); - }, GLOBAL.SERVERS.OPEN); -}); \ No newline at end of file diff --git a/packages/graph/lib/commands/RO_QUERY.spec.ts b/packages/graph/lib/commands/RO_QUERY.spec.ts new file mode 100644 index 00000000000..0fbaeb69537 --- /dev/null +++ b/packages/graph/lib/commands/RO_QUERY.spec.ts @@ -0,0 +1,17 @@ +import { strict as assert } from 'assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import { transformArguments } from './RO_QUERY'; + +describe('RO_QUERY', () => { + it('transformArguments', () => { + assert.deepEqual( + transformArguments('key', 'query'), + ['GRAPH.RO_QUERY', 'key', 'query'] + ); + }); + + testUtils.testWithClient('client.graph.roQuery', async client => { + const { data } = await client.graph.roQuery('key', 'RETURN 0'); + assert.deepEqual(data, [[0]]); + }, GLOBAL.SERVERS.OPEN); +}); \ No newline at end of file diff --git a/packages/graph/lib/commands/QUERY_RO.ts b/packages/graph/lib/commands/RO_QUERY.ts similarity index 72% rename from packages/graph/lib/commands/QUERY_RO.ts rename to packages/graph/lib/commands/RO_QUERY.ts index 2090f593c72..d4dda9dee27 100644 --- a/packages/graph/lib/commands/QUERY_RO.ts +++ b/packages/graph/lib/commands/RO_QUERY.ts @@ -1,5 +1,5 @@ import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { pushQueryArguments } from '.'; +import { pushQueryArguments, QueryOptionsBackwardCompatible } from '.'; export { FIRST_KEY_INDEX } from './QUERY'; @@ -8,13 +8,15 @@ export const IS_READ_ONLY = true; export function transformArguments( graph: RedisCommandArgument, query: RedisCommandArgument, - timeout?: number + options?: QueryOptionsBackwardCompatible, + compact?: boolean ): RedisCommandArguments { return pushQueryArguments( ['GRAPH.RO_QUERY'], graph, query, - timeout + options, + compact ); } diff --git a/packages/graph/lib/commands/index.spec.ts b/packages/graph/lib/commands/index.spec.ts new file mode 100644 index 00000000000..a1cb92db9ae --- /dev/null +++ b/packages/graph/lib/commands/index.spec.ts @@ -0,0 +1,62 @@ +import { strict as assert } from 'assert'; +import { pushQueryArguments } from '.'; + +describe('pushQueryArguments', () => { + it('simple', () => { + assert.deepEqual( + pushQueryArguments(['GRAPH.QUERY'], 'graph', 'query'), + ['GRAPH.QUERY', 'graph', 'query'] + ); + }); + + describe('params', () => { + it('all types', () => { + assert.deepEqual( + pushQueryArguments(['GRAPH.QUERY'], 'graph', 'query', { + params: { + null: null, + string: '"', + number: 0, + boolean: false, + array: [0], + object: {a: 0} + } + }), + ['GRAPH.QUERY', 'graph', 'CYPHER null=null string="\\"" number=0 boolean=false array=[0] object={a:0} query'] + ); + }); + + it('TypeError', () => { + assert.throws(() => { + pushQueryArguments(['GRAPH.QUERY'], 'graph', 'query', { + params: { + a: undefined as any + } + }) + }, TypeError); + }); + }); + + it('TIMEOUT backward compatible', () => { + assert.deepEqual( + pushQueryArguments(['GRAPH.QUERY'], 'graph', 'query', 1), + ['GRAPH.QUERY', 'graph', 'query', 'TIMEOUT', '1'] + ); + }); + + it('TIMEOUT', () => { + assert.deepEqual( + pushQueryArguments(['GRAPH.QUERY'], 'graph', 'query', { + TIMEOUT: 1 + }), + ['GRAPH.QUERY', 'graph', 'query', 'TIMEOUT', '1'] + ); + }); + + it('compact', () => { + assert.deepEqual( + pushQueryArguments(['GRAPH.QUERY'], 'graph', 'query', undefined, true), + ['GRAPH.QUERY', 'graph', 'query', '--compact'] + ); + }); +}); diff --git a/packages/graph/lib/commands/index.ts b/packages/graph/lib/commands/index.ts index afc025e68cf..56d19702098 100644 --- a/packages/graph/lib/commands/index.ts +++ b/packages/graph/lib/commands/index.ts @@ -4,8 +4,8 @@ import * as DELETE from './DELETE'; import * as EXPLAIN from './EXPLAIN'; import * as LIST from './LIST'; import * as PROFILE from './PROFILE'; -import * as QUERY_RO from './QUERY_RO'; import * as QUERY from './QUERY'; +import * as RO_QUERY from './RO_QUERY'; import * as SLOWLOG from './SLOWLOG'; import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; @@ -22,28 +22,93 @@ export default { list: LIST, PROFILE, profile: PROFILE, - QUERY_RO, - queryRo: QUERY_RO, QUERY, query: QUERY, + RO_QUERY, + roQuery: RO_QUERY, SLOWLOG, slowLog: SLOWLOG }; +type QueryParam = null | string | number | boolean | QueryParams | Array; + +type QueryParams = { + [key: string]: QueryParam; +}; + +export interface QueryOptions { + params?: QueryParams; + TIMEOUT?: number; +} + +export type QueryOptionsBackwardCompatible = QueryOptions | number; + export function pushQueryArguments( args: RedisCommandArguments, graph: RedisCommandArgument, query: RedisCommandArgument, - timeout?: number + options?: QueryOptionsBackwardCompatible, + compact?: boolean ): RedisCommandArguments { - args.push( - graph, - query - ); + args.push(graph); - if (timeout !== undefined) { - args.push(timeout.toString()); + if (typeof options === 'number') { + args.push(query); + pushTimeout(args, options); + } else { + args.push( + options?.params ? + `CYPHER ${queryParamsToString(options.params)} ${query}` : + query + ); + + if (options?.TIMEOUT !== undefined) { + pushTimeout(args, options.TIMEOUT); + } + } + + if (compact) { + args.push('--compact'); } return args; -} \ No newline at end of file +} + +function pushTimeout(args: RedisCommandArguments, timeout: number): void { + args.push('TIMEOUT', timeout.toString()); +} + +function queryParamsToString(params: QueryParams): string { + const parts = []; + for (const [key, value] of Object.entries(params)) { + parts.push(`${key}=${queryParamToString(value)}`); + } + return parts.join(' '); +} + +function queryParamToString(param: QueryParam): string { + if (param === null) { + return 'null'; + } + + switch (typeof param) { + case 'string': + return `"${param.replace(/"/g, '\\"')}"`; + + case 'number': + case 'boolean': + return param.toString(); + } + + if (Array.isArray(param)) { + return `[${param.map(queryParamToString).join(',')}]`; + } else if (typeof param === 'object') { + const body = []; + for (const [key, value] of Object.entries(param)) { + body.push(`${key}:${queryParamToString(value)}`); + } + return `{${body.join(',')}}`; + } else { + throw new TypeError(`Unexpected param type ${typeof param} ${param}`) + } +} diff --git a/packages/graph/lib/graph.spec.ts b/packages/graph/lib/graph.spec.ts new file mode 100644 index 00000000000..e4a12c4281a --- /dev/null +++ b/packages/graph/lib/graph.spec.ts @@ -0,0 +1,159 @@ +import { strict as assert } from 'assert'; +import testUtils, { GLOBAL } from './test-utils'; +import Graph from './graph'; + +describe('Graph', () => { + testUtils.testWithClient('Graph', async client => { + const graph = new Graph(client as any, 'a'); + + await client.graph.query('a', 'CREATE (:person {name: "leibale", array: [1,2,"a"]})-[:knows]->(:person {name: "amit"})'); + console.log(JSON.stringify( + (await graph.query('MATCH path = (p :person)-[:knows]->(:person) RETURN p, path') as any)[0] + )); + }, GLOBAL.SERVERS.OPEN); + + describe('#parseValue', () => { + testUtils.testWithClient('null', async client => { + const graph = new Graph(client, 'graph'), + { data } = await graph.roQuery('RETURN null AS key'); + + assert.deepEqual( + data, + [{ key: null }] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('string', async client => { + const graph = new Graph(client, 'graph'), + { data } = await graph.roQuery('RETURN "string" AS key'); + + assert.deepEqual( + data, + [{ key: 'string' }] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('integer', async client => { + const graph = new Graph(client, 'graph'), + { data } = await graph.roQuery('RETURN 0 AS key'); + + assert.deepEqual( + data, + [{ key: 0 }] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('boolean', async client => { + const graph = new Graph(client, 'graph'), + { data } = await graph.roQuery('RETURN false AS key'); + + assert.deepEqual( + data, + [{ key: false }] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('double', async client => { + const graph = new Graph(client, 'graph'), + { data } = await graph.roQuery('RETURN 0.1 AS key'); + + assert.deepEqual( + data, + [{ key: 0.1 }] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('array', async client => { + const graph = new Graph(client, 'graph'), + { data } = await graph.roQuery('RETURN [null] AS key'); + + assert.deepEqual( + data, + [{ key: [null] }] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('edge', async client => { + const graph = new Graph(client, 'graph'); + + // check with and without metadata cache + for (let i = 0; i < 2; i++) { + const { data } = await graph.query('CREATE ()-[edge :edge]->() RETURN edge'); + assert.ok(Array.isArray(data)); + assert.equal(data.length, 1); + assert.equal(typeof data[0].edge.id, 'number'); + assert.equal(data[0].edge.relationshipType, 'edge'); + assert.equal(typeof data[0].edge.sourceId, 'number'); + assert.equal(typeof data[0].edge.destinationId, 'number'); + assert.deepEqual(data[0].edge.properties, {}); + } + + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('node', async client => { + const graph = new Graph(client, 'graph'); + + // check with and without metadata cache + for (let i = 0; i < 2; i++) { + const { data } = await graph.query('CREATE (node :node { p: 0 }) RETURN node'); + assert.ok(Array.isArray(data)); + assert.equal(data.length, 1); + assert.equal(typeof data[0].node.id, 'number'); + assert.deepEqual(data[0].node.labels, ['node']); + assert.deepEqual(data[0].node.properties, { p: 0 }); + } + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('path', async client => { + const graph = new Graph(client, 'graph'), + [, { data }] = await Promise.all([ + await graph.query('CREATE ()-[:edge]->()'), + await graph.roQuery('MATCH path = ()-[:edge]->() RETURN path') + ]); + + assert.ok(Array.isArray(data)); + assert.equal(data.length, 1); + + assert.ok(Array.isArray(data[0].path.nodes)); + assert.equal(data[0].path.nodes.length, 2); + for (const node of data[0].path.nodes) { + assert.equal(typeof node.id, 'number'); + assert.deepEqual(node.labels, []); + assert.deepEqual(node.properties, {}); + } + + assert.ok(Array.isArray(data[0].path.edges)); + assert.equal(data[0].path.edges.length, 1); + for (const edge of data[0].path.edges) { + assert.equal(typeof edge.id, 'number'); + assert.equal(edge.relationshipType, 'edge'); + assert.equal(typeof edge.sourceId, 'number'); + assert.equal(typeof edge.destinationId, 'number'); + assert.deepEqual(edge.properties, {}); + } + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('map', async client => { + const graph = new Graph(client, 'graph'), + { data } = await graph.roQuery('RETURN { key: "value" } AS map'); + + assert.deepEqual(data, [{ + map: { + key: 'value' + } + }]); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('point', async client => { + const graph = new Graph(client, 'graph'), + { data } = await graph.roQuery('RETURN point({ latitude: 1, longitude: 2 }) AS point'); + + assert.deepEqual(data, [{ + point: { + latitude: 1, + longitude: 2 + } + }]); + }, GLOBAL.SERVERS.OPEN); + }); +}); diff --git a/packages/graph/lib/graph.ts b/packages/graph/lib/graph.ts new file mode 100644 index 00000000000..1f0c5bd99be --- /dev/null +++ b/packages/graph/lib/graph.ts @@ -0,0 +1,354 @@ +import { RedisClientType } from '@redis/client/dist/lib/client/index'; +import { RedisCommandArgument, RedisFunctions, RedisScripts } from '@redis/client/dist/lib/commands'; +import { QueryOptions } from './commands'; +import { QueryReply } from './commands/QUERY'; +import * as QUERY from './commands/QUERY'; + +interface GraphMetadata { + labels: Array; + relationshipTypes: Array; + propertyKeys: Array; +} + +// https://github.com/RedisGraph/RedisGraph/blob/master/src/resultset/formatters/resultset_formatter.h#L20 +enum GraphValueTypes { + UNKNOWN = 0, + NULL = 1, + STRING = 2, + INTEGER = 3, + BOOLEAN = 4, + DOUBLE = 5, + ARRAY = 6, + EDGE = 7, + NODE = 8, + PATH = 9, + MAP = 10, + POINT = 11 +} + +type GraphEntityRawProperties = Array<[ + id: number, + ...rest: GraphRawValue +]>; + +type GraphEntityProperties = Record; + +type GraphEdgeRawValue = [ + GraphValueTypes.EDGE, + [ + id: number, + relationshipTypeId: number, + sourceId: number, + destinationId: number, + properties: GraphEntityRawProperties + ] +]; + +type GraphNodeRawValue = [ + GraphValueTypes.NODE, + [ + id: number, + labelIds: Array, + properties: GraphEntityRawProperties + ] +]; + +type GraphPathRawValue = [ + GraphValueTypes.PATH, + [ + nodes: [ + GraphValueTypes.ARRAY, + Array + ], + edges: [ + GraphValueTypes.ARRAY, + Array + ] + ] +]; + +type GraphMapRawValue = [ + GraphValueTypes.MAP, + Array +]; + +type GraphRawValue = [ + GraphValueTypes.NULL, + null +] | [ + GraphValueTypes.STRING, + string +] | [ + GraphValueTypes.INTEGER, + number +] | [ + GraphValueTypes.BOOLEAN, + string +] | [ + GraphValueTypes.DOUBLE, + string +] | [ + GraphValueTypes.ARRAY, + Array +] | GraphEdgeRawValue | GraphNodeRawValue | GraphPathRawValue | GraphMapRawValue | [ + GraphValueTypes.POINT, + [ + latitude: string, + longitude: string + ] +]; + +interface GraphEdge { + id: number; + relationshipType: string; + sourceId: number; + destinationId: number; + properties: GraphEntityProperties; +} + +interface GraphNode { + id: number; + labels: Array; + properties: GraphEntityProperties; +} + +interface GraphPath { + nodes: Array; + edges: Array; +} + +type GraphMap = { + [key: string]: GraphValue; +}; + +type GraphValue = null | string | number | boolean | Array | { +} | GraphEdge | GraphNode | GraphPath | GraphMap | { + latitude: string; + longitude: string; +}; + +type GraphReply = Omit & { + data?: Array; +}; + +type GraphClientType = RedisClientType<{ + graph: { + query: typeof import('./commands/QUERY'), + roQuery: typeof import('./commands/RO_QUERY') + } +}, RedisFunctions, RedisScripts>; + +export default class Graph { + #client: GraphClientType; + #name: string; + #metadata?: GraphMetadata; + + constructor( + client: GraphClientType, + name: string + ) { + this.#client = client; + this.#name = name; + } + + async query( + query: RedisCommandArgument, + options?: QueryOptions + ) { + return this.#parseReply( + await this.#client.graph.query( + this.#name, + query, + options, + true + ) + ); + } + + async roQuery( + query: RedisCommandArgument, + options?: QueryOptions + ) { + return this.#parseReply( + await this.#client.graph.roQuery( + this.#name, + query, + options, + true + ) + ); + } + + #setMetadataPromise?: Promise; + + #updateMetadata(): Promise { + this.#setMetadataPromise ??= this.#setMetadata() + .finally(() => this.#setMetadataPromise = undefined); + return this.#setMetadataPromise; + } + + // DO NOT use directly, use #updateMetadata instead + async #setMetadata(): Promise { + const [labels, relationshipTypes, propertyKeys] = await Promise.all([ + this.#client.graph.roQuery(this.#name, 'CALL db.labels()'), + this.#client.graph.roQuery(this.#name, 'CALL db.relationshipTypes()'), + this.#client.graph.roQuery(this.#name, 'CALL db.propertyKeys()') + ]); + + this.#metadata = { + labels: this.#cleanMetadataArray(labels.data as Array<[string]>), + relationshipTypes: this.#cleanMetadataArray(relationshipTypes.data as Array<[string]>), + propertyKeys: this.#cleanMetadataArray(propertyKeys.data as Array<[string]>) + }; + + return this.#metadata; + } + + #cleanMetadataArray(arr: Array<[string]>): Array { + return arr.map(([value]) => value); + } + + #getMetadata(key: T, id: number): GraphMetadata[T][number] | Promise { + return this.#metadata?.[key][id] ?? this.#getMetadataAsync(key, id); + } + + // DO NOT use directly, use #getMetadata instead + async #getMetadataAsync(key: T, id: number): Promise { + const value = (await this.#updateMetadata())[key][id]; + if (value === undefined) throw new Error(`Cannot find value from ${key}[${id}]`); + return value; + } + + async #parseReply(reply: QueryReply): Promise> { + if (!reply.data) return reply; + + const promises: Array> = [], + parsed = { + metadata: reply.metadata, + data: reply.data!.map((row: any) => { + const data: Record = {}; + for (let i = 0; i < row.length; i++) { + data[reply.headers[i][1]] = this.#parseValue(row[i], promises); + } + + return data as unknown as T; + }) + }; + + if (promises.length) await Promise.all(promises); + + return parsed; + } + + #parseValue([valueType, value]: GraphRawValue, promises: Array>): GraphValue { + switch (valueType) { + case GraphValueTypes.NULL: + return null; + + case GraphValueTypes.STRING: + case GraphValueTypes.INTEGER: + return value; + + case GraphValueTypes.BOOLEAN: + return value === 'true'; + + case GraphValueTypes.DOUBLE: + return parseFloat(value); + + case GraphValueTypes.ARRAY: + return value.map(x => this.#parseValue(x, promises)); + + case GraphValueTypes.EDGE: + return this.#parseEdge(value, promises); + + case GraphValueTypes.NODE: + return this.#parseNode(value, promises); + + case GraphValueTypes.PATH: + return { + nodes: value[0][1].map(([, node]) => this.#parseNode(node, promises)), + edges: value[1][1].map(([, edge]) => this.#parseEdge(edge, promises)) + }; + + case GraphValueTypes.MAP: + const map: GraphMap = {}; + for (let i = 0; i < value.length; i++) { + map[value[i++] as string] = this.#parseValue(value[i] as GraphRawValue, promises); + } + + return map; + + case GraphValueTypes.POINT: + return { + latitude: parseFloat(value[0]), + longitude: parseFloat(value[1]) + }; + + default: + throw new Error(`unknown scalar type: ${valueType}`); + } + } + + #parseEdge([ + id, + relationshipTypeId, + sourceId, + destinationId, + properties + ]: GraphEdgeRawValue[1], promises: Array>): GraphEdge { + const edge = { + id, + sourceId, + destinationId, + properties: this.#parseProperties(properties, promises) + } as GraphEdge; + + const relationshipType = this.#getMetadata('relationshipTypes', relationshipTypeId); + if (relationshipType instanceof Promise) { + promises.push( + relationshipType.then(value => edge.relationshipType = value) + ); + } else { + edge.relationshipType = relationshipType; + } + + return edge; + } + + #parseNode([ + id, + labelIds, + properties + ]: GraphNodeRawValue[1], promises: Array>): GraphNode { + const labels = new Array(labelIds.length); + for (let i = 0; i < labelIds.length; i++) { + const value = this.#getMetadata('labels', labelIds[i]); + if (value instanceof Promise) { + promises.push(value.then(value => labels[i] = value)); + } else { + labels[i] = value; + } + } + + return { + id, + labels, + properties: this.#parseProperties(properties, promises) + }; + } + + #parseProperties(raw: GraphEntityRawProperties, promises: Array>): GraphEntityProperties { + const parsed: GraphEntityProperties = {}; + for (const [id, type, value] of raw) { + const parsedValue = this.#parseValue([type, value] as GraphRawValue, promises), + key = this.#getMetadata('propertyKeys', id); + if (key instanceof Promise) { + promises.push(key.then(key => parsed[key] = parsedValue)); + } else { + parsed[key] = parsedValue; + } + } + + return parsed; + } +} diff --git a/packages/graph/lib/index.ts b/packages/graph/lib/index.ts index bc0e103e8c8..e9f15ab1fd9 100644 --- a/packages/graph/lib/index.ts +++ b/packages/graph/lib/index.ts @@ -1 +1,2 @@ export { default } from './commands'; +export { default as Graph } from './graph'; From b0f43c5593e970b50aed377b68785490774ba99e Mon Sep 17 00:00:00 2001 From: leibale Date: Wed, 26 Oct 2022 14:53:51 -0400 Subject: [PATCH 2/5] clean code --- packages/graph/lib/graph.spec.ts | 267 ++++++++++++++--------------- packages/graph/lib/graph.ts | 17 +- packages/test-utils/lib/dockers.ts | 6 +- packages/test-utils/lib/index.ts | 9 +- 4 files changed, 147 insertions(+), 152 deletions(-) diff --git a/packages/graph/lib/graph.spec.ts b/packages/graph/lib/graph.spec.ts index e4a12c4281a..eb3d80f02ba 100644 --- a/packages/graph/lib/graph.spec.ts +++ b/packages/graph/lib/graph.spec.ts @@ -3,157 +3,146 @@ import testUtils, { GLOBAL } from './test-utils'; import Graph from './graph'; describe('Graph', () => { - testUtils.testWithClient('Graph', async client => { - const graph = new Graph(client as any, 'a'); + testUtils.testWithClient('null', async client => { + const graph = new Graph(client, 'graph'), + { data } = await graph.roQuery('RETURN null AS key'); + + assert.deepEqual( + data, + [{ key: null }] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('string', async client => { + const graph = new Graph(client, 'graph'), + { data } = await graph.roQuery('RETURN "string" AS key'); - await client.graph.query('a', 'CREATE (:person {name: "leibale", array: [1,2,"a"]})-[:knows]->(:person {name: "amit"})'); - console.log(JSON.stringify( - (await graph.query('MATCH path = (p :person)-[:knows]->(:person) RETURN p, path') as any)[0] - )); + assert.deepEqual( + data, + [{ key: 'string' }] + ); }, GLOBAL.SERVERS.OPEN); - describe('#parseValue', () => { - testUtils.testWithClient('null', async client => { - const graph = new Graph(client, 'graph'), - { data } = await graph.roQuery('RETURN null AS key'); - - assert.deepEqual( - data, - [{ key: null }] - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('string', async client => { - const graph = new Graph(client, 'graph'), - { data } = await graph.roQuery('RETURN "string" AS key'); - - assert.deepEqual( - data, - [{ key: 'string' }] - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('integer', async client => { - const graph = new Graph(client, 'graph'), - { data } = await graph.roQuery('RETURN 0 AS key'); - - assert.deepEqual( - data, - [{ key: 0 }] - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('boolean', async client => { - const graph = new Graph(client, 'graph'), - { data } = await graph.roQuery('RETURN false AS key'); - - assert.deepEqual( - data, - [{ key: false }] - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('double', async client => { - const graph = new Graph(client, 'graph'), - { data } = await graph.roQuery('RETURN 0.1 AS key'); - - assert.deepEqual( - data, - [{ key: 0.1 }] - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('array', async client => { - const graph = new Graph(client, 'graph'), - { data } = await graph.roQuery('RETURN [null] AS key'); - - assert.deepEqual( - data, - [{ key: [null] }] - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('edge', async client => { - const graph = new Graph(client, 'graph'); - - // check with and without metadata cache - for (let i = 0; i < 2; i++) { - const { data } = await graph.query('CREATE ()-[edge :edge]->() RETURN edge'); - assert.ok(Array.isArray(data)); - assert.equal(data.length, 1); - assert.equal(typeof data[0].edge.id, 'number'); - assert.equal(data[0].edge.relationshipType, 'edge'); - assert.equal(typeof data[0].edge.sourceId, 'number'); - assert.equal(typeof data[0].edge.destinationId, 'number'); - assert.deepEqual(data[0].edge.properties, {}); - } + testUtils.testWithClient('integer', async client => { + const graph = new Graph(client, 'graph'), + { data } = await graph.roQuery('RETURN 0 AS key'); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual( + data, + [{ key: 0 }] + ); + }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('node', async client => { - const graph = new Graph(client, 'graph'); + testUtils.testWithClient('boolean', async client => { + const graph = new Graph(client, 'graph'), + { data } = await graph.roQuery('RETURN false AS key'); - // check with and without metadata cache - for (let i = 0; i < 2; i++) { - const { data } = await graph.query('CREATE (node :node { p: 0 }) RETURN node'); - assert.ok(Array.isArray(data)); - assert.equal(data.length, 1); - assert.equal(typeof data[0].node.id, 'number'); - assert.deepEqual(data[0].node.labels, ['node']); - assert.deepEqual(data[0].node.properties, { p: 0 }); - } - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual( + data, + [{ key: false }] + ); + }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('path', async client => { - const graph = new Graph(client, 'graph'), - [, { data }] = await Promise.all([ - await graph.query('CREATE ()-[:edge]->()'), - await graph.roQuery('MATCH path = ()-[:edge]->() RETURN path') - ]); + testUtils.testWithClient('double', async client => { + const graph = new Graph(client, 'graph'), + { data } = await graph.roQuery('RETURN 0.1 AS key'); + assert.deepEqual( + data, + [{ key: 0.1 }] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('array', async client => { + const graph = new Graph(client, 'graph'), + { data } = await graph.roQuery('RETURN [null] AS key'); + + assert.deepEqual( + data, + [{ key: [null] }] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('edge', async client => { + const graph = new Graph(client, 'graph'); + + // check with and without metadata cache + for (let i = 0; i < 2; i++) { + const { data } = await graph.query('CREATE ()-[edge :edge]->() RETURN edge'); assert.ok(Array.isArray(data)); assert.equal(data.length, 1); + assert.equal(typeof data[0].edge.id, 'number'); + assert.equal(data[0].edge.relationshipType, 'edge'); + assert.equal(typeof data[0].edge.sourceId, 'number'); + assert.equal(typeof data[0].edge.destinationId, 'number'); + assert.deepEqual(data[0].edge.properties, {}); + } - assert.ok(Array.isArray(data[0].path.nodes)); - assert.equal(data[0].path.nodes.length, 2); - for (const node of data[0].path.nodes) { - assert.equal(typeof node.id, 'number'); - assert.deepEqual(node.labels, []); - assert.deepEqual(node.properties, {}); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('node', async client => { + const graph = new Graph(client, 'graph'); + + // check with and without metadata cache + for (let i = 0; i < 2; i++) { + const { data } = await graph.query('CREATE (node :node { p: 0 }) RETURN node'); + assert.ok(Array.isArray(data)); + assert.equal(data.length, 1); + assert.equal(typeof data[0].node.id, 'number'); + assert.deepEqual(data[0].node.labels, ['node']); + assert.deepEqual(data[0].node.properties, { p: 0 }); + } + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('path', async client => { + const graph = new Graph(client, 'graph'), + [, { data }] = await Promise.all([ + await graph.query('CREATE ()-[:edge]->()'), + await graph.roQuery('MATCH path = ()-[:edge]->() RETURN path') + ]); + + assert.ok(Array.isArray(data)); + assert.equal(data.length, 1); + + assert.ok(Array.isArray(data[0].path.nodes)); + assert.equal(data[0].path.nodes.length, 2); + for (const node of data[0].path.nodes) { + assert.equal(typeof node.id, 'number'); + assert.deepEqual(node.labels, []); + assert.deepEqual(node.properties, {}); + } + + assert.ok(Array.isArray(data[0].path.edges)); + assert.equal(data[0].path.edges.length, 1); + for (const edge of data[0].path.edges) { + assert.equal(typeof edge.id, 'number'); + assert.equal(edge.relationshipType, 'edge'); + assert.equal(typeof edge.sourceId, 'number'); + assert.equal(typeof edge.destinationId, 'number'); + assert.deepEqual(edge.properties, {}); + } + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('map', async client => { + const graph = new Graph(client, 'graph'), + { data } = await graph.roQuery('RETURN { key: "value" } AS map'); + + assert.deepEqual(data, [{ + map: { + key: 'value' } + }]); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('point', async client => { + const graph = new Graph(client, 'graph'), + { data } = await graph.roQuery('RETURN point({ latitude: 1, longitude: 2 }) AS point'); - assert.ok(Array.isArray(data[0].path.edges)); - assert.equal(data[0].path.edges.length, 1); - for (const edge of data[0].path.edges) { - assert.equal(typeof edge.id, 'number'); - assert.equal(edge.relationshipType, 'edge'); - assert.equal(typeof edge.sourceId, 'number'); - assert.equal(typeof edge.destinationId, 'number'); - assert.deepEqual(edge.properties, {}); + assert.deepEqual(data, [{ + point: { + latitude: 1, + longitude: 2 } - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('map', async client => { - const graph = new Graph(client, 'graph'), - { data } = await graph.roQuery('RETURN { key: "value" } AS map'); - - assert.deepEqual(data, [{ - map: { - key: 'value' - } - }]); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('point', async client => { - const graph = new Graph(client, 'graph'), - { data } = await graph.roQuery('RETURN point({ latitude: 1, longitude: 2 }) AS point'); - - assert.deepEqual(data, [{ - point: { - latitude: 1, - longitude: 2 - } - }]); - }, GLOBAL.SERVERS.OPEN); - }); + }]); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/graph/lib/graph.ts b/packages/graph/lib/graph.ts index 1f0c5bd99be..5baff1dae29 100644 --- a/packages/graph/lib/graph.ts +++ b/packages/graph/lib/graph.ts @@ -2,7 +2,6 @@ import { RedisClientType } from '@redis/client/dist/lib/client/index'; import { RedisCommandArgument, RedisFunctions, RedisScripts } from '@redis/client/dist/lib/commands'; import { QueryOptions } from './commands'; import { QueryReply } from './commands/QUERY'; -import * as QUERY from './commands/QUERY'; interface GraphMetadata { labels: Array; @@ -28,11 +27,9 @@ enum GraphValueTypes { type GraphEntityRawProperties = Array<[ id: number, - ...rest: GraphRawValue + ...value: GraphRawValue ]>; -type GraphEntityProperties = Record; - type GraphEdgeRawValue = [ GraphValueTypes.EDGE, [ @@ -98,6 +95,8 @@ type GraphRawValue = [ ] ]; +type GraphEntityProperties = Record; + interface GraphEdge { id: number; relationshipType: string; @@ -208,12 +207,18 @@ export default class Graph { return arr.map(([value]) => value); } - #getMetadata(key: T, id: number): GraphMetadata[T][number] | Promise { + #getMetadata( + key: T, + id: number + ): GraphMetadata[T][number] | Promise { return this.#metadata?.[key][id] ?? this.#getMetadataAsync(key, id); } // DO NOT use directly, use #getMetadata instead - async #getMetadataAsync(key: T, id: number): Promise { + async #getMetadataAsync( + key: T, + id: number + ): Promise { const value = (await this.#updateMetadata())[key][id]; if (value === undefined) throw new Error(`Cannot find value from ${key}[${id}]`); return value; diff --git a/packages/test-utils/lib/dockers.ts b/packages/test-utils/lib/dockers.ts index d6da977d93f..8f0be95b094 100644 --- a/packages/test-utils/lib/dockers.ts +++ b/packages/test-utils/lib/dockers.ts @@ -1,8 +1,8 @@ import { createConnection } from 'net'; import { once } from 'events'; -import { RedisModules, RedisFunctions, RedisScripts } from '@redis/client/lib/commands'; -import RedisClient, { RedisClientType } from '@redis/client/lib/client'; -import { promiseTimeout } from '@redis/client/lib/utils'; +import { RedisModules, RedisFunctions, RedisScripts } from '@redis/client/dist/lib/commands'; +import RedisClient, { RedisClientType } from '@redis/client/dist/lib/client'; +import { promiseTimeout } from '@redis/client/dist/lib/utils'; import * as path from 'path'; import { promisify } from 'util'; import { exec } from 'child_process'; diff --git a/packages/test-utils/lib/index.ts b/packages/test-utils/lib/index.ts index 1e814c29746..0a76944c017 100644 --- a/packages/test-utils/lib/index.ts +++ b/packages/test-utils/lib/index.ts @@ -1,6 +1,7 @@ -import { RedisModules, RedisFunctions, RedisScripts } from '@redis/client/lib/commands'; -import RedisClient, { RedisClientOptions, RedisClientType } from '@redis/client/lib/client'; -import RedisCluster, { RedisClusterOptions, RedisClusterType } from '@redis/client/lib/cluster'; +import { RedisModules, RedisFunctions, RedisScripts } from '@redis/client/dist/lib/commands'; +import RedisClient, { RedisClientOptions, RedisClientType } from '@redis/client/dist/lib/client'; +import RedisCluster, { RedisClusterOptions, RedisClusterType } from '@redis/client/dist/lib/cluster'; +import { RedisSocketCommonOptions } from '@redis/client/dist/lib/client/socket'; import { RedisServerDockerConfig, spawnRedisServer, spawnRedisCluster } from './dockers'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; @@ -21,7 +22,7 @@ interface ClientTestOptions< S extends RedisScripts > extends CommonTestOptions { serverArguments: Array; - clientOptions?: Partial>; + clientOptions?: Partial, 'socket'> & { socket: RedisSocketCommonOptions }>; disableClientSetup?: boolean; } From 131f45def3df61063094c8f28e25ea16cf6f02ad Mon Sep 17 00:00:00 2001 From: leibale Date: Wed, 26 Oct 2022 15:44:12 -0400 Subject: [PATCH 3/5] fix graph string param escaping --- packages/graph/lib/commands/index.spec.ts | 4 ++-- packages/graph/lib/commands/index.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/graph/lib/commands/index.spec.ts b/packages/graph/lib/commands/index.spec.ts index a1cb92db9ae..a688c49dd39 100644 --- a/packages/graph/lib/commands/index.spec.ts +++ b/packages/graph/lib/commands/index.spec.ts @@ -15,14 +15,14 @@ describe('pushQueryArguments', () => { pushQueryArguments(['GRAPH.QUERY'], 'graph', 'query', { params: { null: null, - string: '"', + string: '"\\', number: 0, boolean: false, array: [0], object: {a: 0} } }), - ['GRAPH.QUERY', 'graph', 'CYPHER null=null string="\\"" number=0 boolean=false array=[0] object={a:0} query'] + ['GRAPH.QUERY', 'graph', 'CYPHER null=null string="\\"\\\\" number=0 boolean=false array=[0] object={a:0} query'] ); }); diff --git a/packages/graph/lib/commands/index.ts b/packages/graph/lib/commands/index.ts index 56d19702098..2acf9089ee6 100644 --- a/packages/graph/lib/commands/index.ts +++ b/packages/graph/lib/commands/index.ts @@ -93,7 +93,7 @@ function queryParamToString(param: QueryParam): string { switch (typeof param) { case 'string': - return `"${param.replace(/"/g, '\\"')}"`; + return `"${param.replace(/["\\]/g, '\\$&')}"`; case 'number': case 'boolean': From 7d3ded239e07e26b5da8833e1230e6055f0aa282 Mon Sep 17 00:00:00 2001 From: leibale Date: Wed, 26 Oct 2022 16:07:18 -0400 Subject: [PATCH 4/5] fix "is not assignable to parameter of type 'GraphClientType'" --- packages/graph/lib/graph.spec.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/graph/lib/graph.spec.ts b/packages/graph/lib/graph.spec.ts index eb3d80f02ba..51912356d3a 100644 --- a/packages/graph/lib/graph.spec.ts +++ b/packages/graph/lib/graph.spec.ts @@ -4,7 +4,7 @@ import Graph from './graph'; describe('Graph', () => { testUtils.testWithClient('null', async client => { - const graph = new Graph(client, 'graph'), + const graph = new Graph(client as any, 'graph'), { data } = await graph.roQuery('RETURN null AS key'); assert.deepEqual( @@ -14,7 +14,7 @@ describe('Graph', () => { }, GLOBAL.SERVERS.OPEN); testUtils.testWithClient('string', async client => { - const graph = new Graph(client, 'graph'), + const graph = new Graph(client as any, 'graph'), { data } = await graph.roQuery('RETURN "string" AS key'); assert.deepEqual( @@ -24,7 +24,7 @@ describe('Graph', () => { }, GLOBAL.SERVERS.OPEN); testUtils.testWithClient('integer', async client => { - const graph = new Graph(client, 'graph'), + const graph = new Graph(client as any, 'graph'), { data } = await graph.roQuery('RETURN 0 AS key'); assert.deepEqual( @@ -34,7 +34,7 @@ describe('Graph', () => { }, GLOBAL.SERVERS.OPEN); testUtils.testWithClient('boolean', async client => { - const graph = new Graph(client, 'graph'), + const graph = new Graph(client as any, 'graph'), { data } = await graph.roQuery('RETURN false AS key'); assert.deepEqual( @@ -44,7 +44,7 @@ describe('Graph', () => { }, GLOBAL.SERVERS.OPEN); testUtils.testWithClient('double', async client => { - const graph = new Graph(client, 'graph'), + const graph = new Graph(client as any, 'graph'), { data } = await graph.roQuery('RETURN 0.1 AS key'); assert.deepEqual( @@ -54,7 +54,7 @@ describe('Graph', () => { }, GLOBAL.SERVERS.OPEN); testUtils.testWithClient('array', async client => { - const graph = new Graph(client, 'graph'), + const graph = new Graph(client as any, 'graph'), { data } = await graph.roQuery('RETURN [null] AS key'); assert.deepEqual( @@ -64,7 +64,7 @@ describe('Graph', () => { }, GLOBAL.SERVERS.OPEN); testUtils.testWithClient('edge', async client => { - const graph = new Graph(client, 'graph'); + const graph = new Graph(client as any, 'graph'); // check with and without metadata cache for (let i = 0; i < 2; i++) { @@ -81,7 +81,7 @@ describe('Graph', () => { }, GLOBAL.SERVERS.OPEN); testUtils.testWithClient('node', async client => { - const graph = new Graph(client, 'graph'); + const graph = new Graph(client as any, 'graph'); // check with and without metadata cache for (let i = 0; i < 2; i++) { @@ -95,7 +95,7 @@ describe('Graph', () => { }, GLOBAL.SERVERS.OPEN); testUtils.testWithClient('path', async client => { - const graph = new Graph(client, 'graph'), + const graph = new Graph(client as any, 'graph'), [, { data }] = await Promise.all([ await graph.query('CREATE ()-[:edge]->()'), await graph.roQuery('MATCH path = ()-[:edge]->() RETURN path') @@ -124,7 +124,7 @@ describe('Graph', () => { }, GLOBAL.SERVERS.OPEN); testUtils.testWithClient('map', async client => { - const graph = new Graph(client, 'graph'), + const graph = new Graph(client as any, 'graph'), { data } = await graph.roQuery('RETURN { key: "value" } AS map'); assert.deepEqual(data, [{ @@ -135,7 +135,7 @@ describe('Graph', () => { }, GLOBAL.SERVERS.OPEN); testUtils.testWithClient('point', async client => { - const graph = new Graph(client, 'graph'), + const graph = new Graph(client as any, 'graph'), { data } = await graph.roQuery('RETURN point({ latitude: 1, longitude: 2 }) AS point'); assert.deepEqual(data, [{ From 5ecc1d62c88fd23488d1b211f0555e468de138b3 Mon Sep 17 00:00:00 2001 From: leibale Date: Thu, 27 Oct 2022 14:27:21 -0400 Subject: [PATCH 5/5] fix README --- packages/graph/README.md | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/graph/README.md b/packages/graph/README.md index 7074f0859f6..3beb08f0ecd 100644 --- a/packages/graph/README.md +++ b/packages/graph/README.md @@ -9,20 +9,24 @@ client.on('error', (err) => console.log('Redis Client Error', err)); await client.connect(); -const graph = new Graph('graph', client); +const graph = new Graph(client, 'graph'); await graph.query( - 'graph', - 'CREATE (:Rider { name: "Buzz Aldrin" })-[:rides]->(:Team { name: "Apollo" })' + 'CREATE (:Rider { name: $riderName })-[:rides]->(:Team { name: $teamName })', + { + params: { + riderName: 'Buzz Aldrin', + teamName: 'Apollo' + } + } ); const result = await graph.roQuery( - 'MATCH (r:Rider)-[:rides]->(t:Team { name: "Apollo" }) RETURN r.name AS riderName, t.name AS teamName' + 'MATCH (r:Rider)-[:rides]->(t:Team { name: $name }) RETURN r.name AS name', + { + name: 'Apollo' + } ); -console.log(result.data); -// [{ -// riderName: 'Buzz Aldrin', -// teamName: 'Apollo' -// }] +console.log(result.data); // [{ name: 'Buzz Aldrin' }] ```