From 1c6d74ffcb776af858d4be9c60658ef75436addc Mon Sep 17 00:00:00 2001 From: Leibale Eidelman Date: Tue, 1 Nov 2022 15:45:35 -0400 Subject: [PATCH] fix #2189 - add graph --compact support (#2305) * fix #2189 - add graph --compact support * clean code * fix graph string param escaping * fix "is not assignable to parameter of type 'GraphClientType'" * fix README --- packages/graph/README.md | 37 +- 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 | 148 ++++++++ packages/graph/lib/graph.ts | 359 ++++++++++++++++++ packages/graph/lib/index.ts | 1 + packages/test-utils/lib/dockers.ts | 6 +- 12 files changed, 713 insertions(+), 77 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..3beb08f0ecd 100644 --- a/packages/graph/README.md +++ b/packages/graph/README.md @@ -2,34 +2,31 @@ 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(client, 'graph'); -const result = await client.graph.query( - 'graph', - `MATCH (r:Rider)-[:rides]->(t:Team) WHERE t.name = 'Apollo' RETURN r.name, t.name` +await graph.query( + 'CREATE (:Rider { name: $riderName })-[:rides]->(:Team { name: $teamName })', + { + params: { + riderName: 'Buzz Aldrin', + teamName: 'Apollo' + } + } ); -console.log(result); -``` +const result = await graph.roQuery( + 'MATCH (r:Rider)-[:rides]->(t:Team { name: $name }) RETURN r.name AS name', + { + name: 'Apollo' + } +); -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); // [{ name: 'Buzz Aldrin' }] ``` 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..a688c49dd39 --- /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..2acf9089ee6 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..51912356d3a --- /dev/null +++ b/packages/graph/lib/graph.spec.ts @@ -0,0 +1,148 @@ +import { strict as assert } from 'assert'; +import testUtils, { GLOBAL } from './test-utils'; +import Graph from './graph'; + +describe('Graph', () => { + testUtils.testWithClient('null', async client => { + const graph = new Graph(client as any, '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 as any, '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 as any, '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 as any, '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 as any, '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 as any, '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 as any, '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 as any, '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 as any, '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 as any, '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 as any, '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..5baff1dae29 --- /dev/null +++ b/packages/graph/lib/graph.ts @@ -0,0 +1,359 @@ +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'; + +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, + ...value: GraphRawValue +]>; + +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 + ] +]; + +type GraphEntityProperties = Record; + +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'; 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';