From 3fa7717a7d23aaaf10e284cc3c42f5fe736c0aad Mon Sep 17 00:00:00 2001 From: Leibale Date: Mon, 10 Jul 2023 12:20:40 -0400 Subject: [PATCH] graph --- .../graph/lib/commands/CONFIG_GET.spec.ts | 2 +- .../graph/lib/commands/CONFIG_SET.spec.ts | 2 +- packages/graph/lib/commands/CONFIG_SET.ts | 2 +- packages/graph/lib/commands/DELETE.spec.ts | 12 +- packages/graph/lib/commands/DELETE.ts | 2 +- packages/graph/lib/commands/EXPLAIN.spec.ts | 6 +- packages/graph/lib/commands/LIST.spec.ts | 2 +- packages/graph/lib/commands/PROFILE.spec.ts | 6 +- packages/graph/lib/commands/QUERY.spec.ts | 60 +- packages/graph/lib/commands/QUERY.ts | 132 +++-- packages/graph/lib/commands/RO_QUERY.spec.ts | 30 +- packages/graph/lib/commands/RO_QUERY.ts | 30 +- packages/graph/lib/commands/SLOWLOG.spec.ts | 10 +- packages/graph/lib/commands/SLOWLOG.ts | 46 +- packages/graph/lib/commands/index.spec.ts | 62 -- packages/graph/lib/commands/index.ts | 141 +---- packages/graph/lib/graph.spec.ts | 236 ++++---- packages/graph/lib/graph.ts | 552 +++++++++--------- packages/graph/lib/test-utils.ts | 20 +- packages/graph/package.json | 6 +- 20 files changed, 648 insertions(+), 711 deletions(-) delete mode 100644 packages/graph/lib/commands/index.spec.ts diff --git a/packages/graph/lib/commands/CONFIG_GET.spec.ts b/packages/graph/lib/commands/CONFIG_GET.spec.ts index c002bbdf0e..c9bf44503b 100644 --- a/packages/graph/lib/commands/CONFIG_GET.spec.ts +++ b/packages/graph/lib/commands/CONFIG_GET.spec.ts @@ -2,7 +2,7 @@ import { strict as assert } from 'assert'; import testUtils, { GLOBAL } from '../test-utils'; import CONFIG_GET from './CONFIG_GET'; -describe('CONFIG GET', () => { +describe('GRAPH.CONFIG GET', () => { it('transformArguments', () => { assert.deepEqual( CONFIG_GET.transformArguments('TIMEOUT'), diff --git a/packages/graph/lib/commands/CONFIG_SET.spec.ts b/packages/graph/lib/commands/CONFIG_SET.spec.ts index 02eed85cd9..aa44be8717 100644 --- a/packages/graph/lib/commands/CONFIG_SET.spec.ts +++ b/packages/graph/lib/commands/CONFIG_SET.spec.ts @@ -2,7 +2,7 @@ import { strict as assert } from 'assert'; import testUtils, { GLOBAL } from '../test-utils'; import CONFIG_SET from './CONFIG_SET'; -describe('CONFIG SET', () => { +describe('GRAPH.CONFIG SET', () => { it('transformArguments', () => { assert.deepEqual( CONFIG_SET.transformArguments('TIMEOUT', 0), diff --git a/packages/graph/lib/commands/CONFIG_SET.ts b/packages/graph/lib/commands/CONFIG_SET.ts index 715f754ce3..ba23ac2f1a 100644 --- a/packages/graph/lib/commands/CONFIG_SET.ts +++ b/packages/graph/lib/commands/CONFIG_SET.ts @@ -11,5 +11,5 @@ export default { value.toString() ]; }, - transformReply: undefined as unknown as () => SimpleStringReply + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/graph/lib/commands/DELETE.spec.ts b/packages/graph/lib/commands/DELETE.spec.ts index 5b9fd2ceb6..86cec5c8d2 100644 --- a/packages/graph/lib/commands/DELETE.spec.ts +++ b/packages/graph/lib/commands/DELETE.spec.ts @@ -2,7 +2,7 @@ import { strict as assert } from 'assert'; import testUtils, { GLOBAL } from '../test-utils'; import DELETE from './DELETE'; -describe('', () => { +describe('GRAPH.DELETE', () => { it('transformArguments', () => { assert.deepEqual( DELETE.transformArguments('key'), @@ -11,11 +11,11 @@ describe('', () => { }); testUtils.testWithClient('client.graph.delete', async client => { - await client.graph.query('key', 'RETURN 1'); + const [, reply] = await Promise.all([ + client.graph.query('key', 'RETURN 1'), + client.graph.delete('key') + ]); - assert.equal( - typeof await client.graph.delete('key'), - 'string' - ); + assert.equal(reply, 'OK'); }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/graph/lib/commands/DELETE.ts b/packages/graph/lib/commands/DELETE.ts index 16c2566af0..f5772a5f6e 100644 --- a/packages/graph/lib/commands/DELETE.ts +++ b/packages/graph/lib/commands/DELETE.ts @@ -6,5 +6,5 @@ export default { transformArguments(key: RedisArgument) { return ['GRAPH.DELETE', key]; }, - transformReply: undefined as unknown as () => SimpleStringReply + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/graph/lib/commands/EXPLAIN.spec.ts b/packages/graph/lib/commands/EXPLAIN.spec.ts index 9e532307a9..1fc824ad36 100644 --- a/packages/graph/lib/commands/EXPLAIN.spec.ts +++ b/packages/graph/lib/commands/EXPLAIN.spec.ts @@ -2,7 +2,7 @@ import { strict as assert } from 'assert'; import testUtils, { GLOBAL } from '../test-utils'; import EXPLAIN from './EXPLAIN'; -describe('EXPLAIN', () => { +describe('GRAPH.EXPLAIN', () => { it('transformArguments', () => { assert.deepEqual( EXPLAIN.transformArguments('key', 'RETURN 0'), @@ -16,6 +16,8 @@ describe('EXPLAIN', () => { client.graph.explain('key', 'RETURN 0') ]); assert.ok(Array.isArray(reply)); - assert.ok(!reply.find(x => typeof x !== 'string')); + for (const item of reply) { + assert.equal(typeof item, 'string'); + } }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/graph/lib/commands/LIST.spec.ts b/packages/graph/lib/commands/LIST.spec.ts index 3621adf124..697b990612 100644 --- a/packages/graph/lib/commands/LIST.spec.ts +++ b/packages/graph/lib/commands/LIST.spec.ts @@ -2,7 +2,7 @@ import { strict as assert } from 'assert'; import testUtils, { GLOBAL } from '../test-utils'; import LIST from './LIST'; -describe('LIST', () => { +describe('GRAPH.LIST', () => { it('transformArguments', () => { assert.deepEqual( LIST.transformArguments(), diff --git a/packages/graph/lib/commands/PROFILE.spec.ts b/packages/graph/lib/commands/PROFILE.spec.ts index 8d967a1344..ec950d505e 100644 --- a/packages/graph/lib/commands/PROFILE.spec.ts +++ b/packages/graph/lib/commands/PROFILE.spec.ts @@ -2,7 +2,7 @@ import { strict as assert } from 'assert'; import testUtils, { GLOBAL } from '../test-utils'; import PROFILE from './PROFILE'; -describe('PROFILE', () => { +describe('GRAPH.PROFILE', () => { it('transformArguments', () => { assert.deepEqual( PROFILE.transformArguments('key', 'RETURN 0'), @@ -13,6 +13,8 @@ describe('PROFILE', () => { testUtils.testWithClient('client.graph.profile', async client => { const reply = await client.graph.profile('key', 'RETURN 0'); assert.ok(Array.isArray(reply)); - assert.ok(!reply.find(x => typeof x !== 'string')); + for (const item of reply) { + assert.equal(typeof item, 'string'); + } }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/graph/lib/commands/QUERY.spec.ts b/packages/graph/lib/commands/QUERY.spec.ts index accbc5cc4c..389374b5d3 100644 --- a/packages/graph/lib/commands/QUERY.spec.ts +++ b/packages/graph/lib/commands/QUERY.spec.ts @@ -2,14 +2,60 @@ import { strict as assert } from 'assert'; import testUtils, { GLOBAL } from '../test-utils'; import QUERY from './QUERY'; -describe('QUERY', () => { - it('transformArguments', () => { - assert.deepEqual( - QUERY.transformArguments('key', 'query'), - ['GRAPH.QUERY', 'key', 'query'] - ); +describe('GRAPH.QUERY', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + QUERY.transformArguments('key', 'query'), + ['GRAPH.QUERY', 'key', 'query'] + ); + }); + + describe('params', () => { + it('all types', () => { + assert.deepEqual( + QUERY.transformArguments('key', 'query', { + params: { + null: null, + string: '"\\', + number: 0, + boolean: false, + array: [0], + object: {a: 0} + } + }), + ['GRAPH.QUERY', 'key', 'CYPHER null=null string="\\"\\\\" number=0 boolean=false array=[0] object={a:0} query'] + ); + }); + + it('TypeError', () => { + assert.throws(() => { + QUERY.transformArguments('key', 'query', { + params: { + a: Buffer.from('a') + } + }) + }, TypeError); + }); + }); + + it('TIMEOUT', () => { + assert.deepEqual( + QUERY.transformArguments('key', 'query', { + TIMEOUT: 1 + }), + ['GRAPH.QUERY', 'key', 'query', 'TIMEOUT', '1'] + ); + }); + + it('compact', () => { + assert.deepEqual( + QUERY.transformArguments('key', 'query', undefined, true), + ['GRAPH.QUERY', 'key', 'query', '--compact'] + ); + }); }); - + testUtils.testWithClient('client.graph.query', async client => { const { data } = await client.graph.query('key', 'RETURN 0'); assert.deepEqual(data, [[0]]); diff --git a/packages/graph/lib/commands/QUERY.ts b/packages/graph/lib/commands/QUERY.ts index 1d6a4229fa..b86382a49d 100644 --- a/packages/graph/lib/commands/QUERY.ts +++ b/packages/graph/lib/commands/QUERY.ts @@ -1,55 +1,103 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands/index'; -import { pushQueryArguments, QueryOptionsBackwardCompatible } from '.'; +import { RedisArgument, Command, ArrayReply, BlobStringReply, NumberReply, NullReply, TuplesReply } from '@redis/client/dist/lib/RESP/types'; -export const FIRST_KEY_INDEX = 1; +type Headers = ArrayReply; -export function transformArguments( - graph: RedisCommandArgument, - query: RedisCommandArgument, - options?: QueryOptionsBackwardCompatible, - compact?: boolean -): RedisCommandArguments { - return pushQueryArguments( - ['GRAPH.QUERY'], - graph, - query, - options, - compact - ); -} +// TODO: cannot use `ArrayReply` due to circular reference +type Data = Array; -type Headers = Array; +type Metadata = ArrayReply; -type Data = Array; - -type Metadata = Array; - -type QueryRawReply = [ +type QueryRawReply = TuplesReply<[ headers: Headers, data: Data, metadata: Metadata ] | [ metadata: Metadata -]; +]>; -export type QueryReply = { - headers: undefined; - data: undefined; - metadata: Metadata; -} | { - headers: Headers; - data: Data; - metadata: Metadata; +type QueryParam = null | string | number | boolean | QueryParams | Array; + +type QueryParams = { + [key: string]: QueryParam; }; -export function transformReply(reply: QueryRawReply): QueryReply { - return reply.length === 1 ? { - headers: undefined, - data: undefined, - metadata: reply[0] - } : { - headers: reply[0], - data: reply[1], - metadata: reply[2] - }; +export interface QueryOptions { + params?: QueryParams; + TIMEOUT?: number; } + +export function transformQueryArguments( + command: RedisArgument, + graph: RedisArgument, + query: RedisArgument, + options?: QueryOptions, + compact?: boolean +) { + const args = [ + command, + graph, + options?.params ? + `CYPHER ${queryParamsToString(options.params)} ${query}` : + query + ]; + + if (options?.TIMEOUT !== undefined) { + args.push('TIMEOUT', options.TIMEOUT.toString()); + } + + if (compact) { + args.push('--compact'); + } + + return args; +} + +function queryParamsToString(params: QueryParams) { + return Object.entries(params) + .map(([key, value]) => `${key}=${queryParamToString(value)}`) + .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}`) + } +} + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments: transformQueryArguments.bind(undefined, 'GRAPH.QUERY'), + transformReply(reply: QueryRawReply) { + return reply.length === 1 ? { + headers: undefined, + data: undefined, + metadata: reply[0] + } : { + headers: reply[0], + data: reply[1], + metadata: reply[2] + }; + } +} as const satisfies Command; diff --git a/packages/graph/lib/commands/RO_QUERY.spec.ts b/packages/graph/lib/commands/RO_QUERY.spec.ts index 1d76b1bd65..b85c33be63 100644 --- a/packages/graph/lib/commands/RO_QUERY.spec.ts +++ b/packages/graph/lib/commands/RO_QUERY.spec.ts @@ -1,20 +1,20 @@ import { strict as assert } from 'assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './RO_QUERY'; +import RO_QUERY from './RO_QUERY'; -describe('RO_QUERY', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'query'), - ['GRAPH.RO_QUERY', 'key', 'query'] - ); - }); +describe('GRAPH.RO_QUERY', () => { + it('transformArguments', () => { + assert.deepEqual( + RO_QUERY.transformArguments('key', 'query'), + ['GRAPH.RO_QUERY', 'key', 'query'] + ); + }); - testUtils.testWithClient('client.graph.roQuery', async client => { - const [, { data }] = await Promise.all([ - client.graph.query('key', 'RETURN 0'), // make sure to create a graph first - client.graph.roQuery('key', 'RETURN 0') - ]); - assert.deepEqual(data, [[0]]); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.graph.roQuery', async client => { + const [, { data }] = await Promise.all([ + client.graph.query('key', 'RETURN 0'), // make sure to create a graph first + 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/RO_QUERY.ts b/packages/graph/lib/commands/RO_QUERY.ts index d4dda9dee2..5987f511b7 100644 --- a/packages/graph/lib/commands/RO_QUERY.ts +++ b/packages/graph/lib/commands/RO_QUERY.ts @@ -1,23 +1,9 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { pushQueryArguments, QueryOptionsBackwardCompatible } from '.'; +import { Command } from '@redis/client/dist/lib/RESP/types'; +import QUERY, { transformQueryArguments } from './QUERY'; -export { FIRST_KEY_INDEX } from './QUERY'; - -export const IS_READ_ONLY = true; - -export function transformArguments( - graph: RedisCommandArgument, - query: RedisCommandArgument, - options?: QueryOptionsBackwardCompatible, - compact?: boolean -): RedisCommandArguments { - return pushQueryArguments( - ['GRAPH.RO_QUERY'], - graph, - query, - options, - compact - ); -} - -export { transformReply } from './QUERY'; +export default { + FIRST_KEY_INDEX: QUERY.FIRST_KEY_INDEX, + IS_READ_ONLY: true, + transformArguments: transformQueryArguments.bind(undefined, 'GRAPH.RO_QUERY'), + transformReply: QUERY.transformReply +} as const satisfies Command; diff --git a/packages/graph/lib/commands/SLOWLOG.spec.ts b/packages/graph/lib/commands/SLOWLOG.spec.ts index 00ec90e945..5f8d110752 100644 --- a/packages/graph/lib/commands/SLOWLOG.spec.ts +++ b/packages/graph/lib/commands/SLOWLOG.spec.ts @@ -1,8 +1,8 @@ import { strict as assert } from 'assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SLOWLOG'; +import SLOWLOG from './SLOWLOG'; -describe('SLOWLOG', () => { +describe('GRAPH.SLOWLOG', () => { it('transformArguments', () => { assert.deepEqual( transformArguments('key'), @@ -11,8 +11,10 @@ describe('SLOWLOG', () => { }); testUtils.testWithClient('client.graph.slowLog', async client => { - await client.graph.query('key', 'RETURN 1'); - const reply = await client.graph.slowLog('key'); + const [, reply] = await Promise.all([ + client.graph.query('key', 'RETURN 1'), + client.graph.slowLog('key') + ]); assert.equal(reply.length, 1); }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/graph/lib/commands/SLOWLOG.ts b/packages/graph/lib/commands/SLOWLOG.ts index 6ae87af89b..b0271ccae6 100644 --- a/packages/graph/lib/commands/SLOWLOG.ts +++ b/packages/graph/lib/commands/SLOWLOG.ts @@ -1,30 +1,24 @@ -export const IS_READ_ONLY = true; +import { RedisArgument, ArrayReply, TuplesReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -export const FIRST_KEY_INDEX = 1; +type SlowLogRawReply = ArrayReply>; -export function transformArguments(key: string) { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['GRAPH.SLOWLOG', key]; -} - -type SlowLogRawReply = Array<[ - timestamp: string, - command: string, - query: string, - took: string -]>; - -type SlowLogReply = Array<{ - timestamp: Date; - command: string; - query: string; - took: number; -}>; - -export function transformReply(logs: SlowLogRawReply): SlowLogReply { - return logs.map(([timestamp, command, query, took]) => ({ - timestamp: new Date(Number(timestamp) * 1000), - command, - query, - took: Number(took) + }, + transformReply(reply: SlowLogRawReply) { + return reply.map(([timestamp, command, query, took]) => ({ + timestamp: Number(timestamp), + command, + query, + took: Number(took) })); -} + } +} as const satisfies Command; diff --git a/packages/graph/lib/commands/index.spec.ts b/packages/graph/lib/commands/index.spec.ts deleted file mode 100644 index a688c49dd3..0000000000 --- a/packages/graph/lib/commands/index.spec.ts +++ /dev/null @@ -1,62 +0,0 @@ -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 2acf9089ee..cba3305a3f 100644 --- a/packages/graph/lib/commands/index.ts +++ b/packages/graph/lib/commands/index.ts @@ -1,114 +1,31 @@ -import * as CONFIG_GET from './CONFIG_GET'; -import * as CONFIG_SET from './CONFIG_SET';; -import * as DELETE from './DELETE'; -import * as EXPLAIN from './EXPLAIN'; -import * as LIST from './LIST'; -import * as PROFILE from './PROFILE'; -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'; +import type { RedisCommands } from '@redis/client/dist/lib/RESP/types'; +import CONFIG_GET from './CONFIG_GET'; +import CONFIG_SET from './CONFIG_SET';; +import DELETE from './DELETE'; +import EXPLAIN from './EXPLAIN'; +import LIST from './LIST'; +import PROFILE from './PROFILE'; +import QUERY from './QUERY'; +import RO_QUERY from './RO_QUERY'; +// import SLOWLOG from './SLOWLOG'; export default { - CONFIG_GET, - configGet: CONFIG_GET, - CONFIG_SET, - configSet: CONFIG_SET, - DELETE, - delete: DELETE, - EXPLAIN, - explain: EXPLAIN, - LIST, - list: LIST, - PROFILE, - profile: PROFILE, - 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, - options?: QueryOptionsBackwardCompatible, - compact?: boolean -): RedisCommandArguments { - args.push(graph); - - 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; -} - -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}`) - } -} + CONFIG_GET, + configGet: CONFIG_GET, + CONFIG_SET, + configSet: CONFIG_SET, + DELETE, + delete: DELETE, + EXPLAIN, + explain: EXPLAIN, + LIST, + list: LIST, + PROFILE, + profile: PROFILE, + QUERY, + query: QUERY, + RO_QUERY, + roQuery: RO_QUERY, + // SLOWLOG, + // slowLog: SLOWLOG +} as const satisfies RedisCommands; diff --git a/packages/graph/lib/graph.spec.ts b/packages/graph/lib/graph.spec.ts index 495c6d17a8..13b8e6145b 100644 --- a/packages/graph/lib/graph.spec.ts +++ b/packages/graph/lib/graph.spec.ts @@ -3,146 +3,146 @@ 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.query('RETURN null AS key'); + testUtils.testWithClient('null', async client => { + const graph = new Graph(client as any, 'graph'), + { data } = await graph.query('RETURN null AS key'); - assert.deepEqual( - data, - [{ key: null }] - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual( + data, + [{ key: null }] + ); + }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('string', async client => { - const graph = new Graph(client as any, 'graph'), - { data } = await graph.query('RETURN "string" AS key'); + testUtils.testWithClient('string', async client => { + const graph = new Graph(client as any, 'graph'), + { data } = await graph.query('RETURN "string" AS key'); - assert.deepEqual( - data, - [{ key: 'string' }] - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual( + data, + [{ key: 'string' }] + ); + }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('integer', async client => { - const graph = new Graph(client as any, 'graph'), - { data } = await graph.query('RETURN 0 AS key'); + testUtils.testWithClient('integer', async client => { + const graph = new Graph(client as any, 'graph'), + { data } = await graph.query('RETURN 0 AS key'); - assert.deepEqual( - data, - [{ key: 0 }] - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual( + data, + [{ key: 0 }] + ); + }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('boolean', async client => { - const graph = new Graph(client as any, 'graph'), - { data } = await graph.query('RETURN false AS key'); + testUtils.testWithClient('boolean', async client => { + const graph = new Graph(client as any, 'graph'), + { data } = await graph.query('RETURN false AS key'); - assert.deepEqual( - data, - [{ key: false }] - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual( + data, + [{ key: false }] + ); + }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('double', async client => { - const graph = new Graph(client as any, 'graph'), - { data } = await graph.query('RETURN 0.1 AS key'); + testUtils.testWithClient('double', async client => { + const graph = new Graph(client as any, 'graph'), + { data } = await graph.query('RETURN 0.1 AS key'); - assert.deepEqual( - data, - [{ key: 0.1 }] - ); - }, GLOBAL.SERVERS.OPEN); + 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.query('RETURN [null] AS key'); + testUtils.testWithClient('array', async client => { + const graph = new Graph(client as any, 'graph'), + { data } = await graph.query('RETURN [null] AS key'); - assert.deepEqual( - data, - [{ key: [null] }] - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual( + data, + [{ key: [null] }] + ); + }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('edge', async client => { - const graph = new Graph(client as any, 'graph'); + 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, {}); - } + // 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); + }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('node', async client => { - const graph = new Graph(client as any, 'graph'); + 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); + // 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') - ]); + 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)); + 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.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); + 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.query('RETURN { key: "value" } AS map'); + testUtils.testWithClient('map', async client => { + const graph = new Graph(client as any, 'graph'), + { data } = await graph.query('RETURN { key: "value" } AS map'); - assert.deepEqual(data, [{ - map: { - key: 'value' - } - }]); - }, GLOBAL.SERVERS.OPEN); + 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.query('RETURN point({ latitude: 1, longitude: 2 }) AS point'); + testUtils.testWithClient('point', async client => { + const graph = new Graph(client as any, 'graph'), + { data } = await graph.query('RETURN point({ latitude: 1, longitude: 2 }) AS point'); - assert.deepEqual(data, [{ - point: { - latitude: 1, - longitude: 2 - } - }]); - }, GLOBAL.SERVERS.OPEN); + 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 index 5baff1dae2..8866052bda 100644 --- a/packages/graph/lib/graph.ts +++ b/packages/graph/lib/graph.ts @@ -1,359 +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'; +import { RedisArgument, RedisFunctions, RedisScripts } from '@redis/client/dist/lib/RESP/types'; +import QUERY, { QueryOptions } from './commands/QUERY'; interface GraphMetadata { - labels: Array; - relationshipTypes: Array; - propertyKeys: Array; + 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 + 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 + id: number, + ...value: GraphRawValue ]>; type GraphEdgeRawValue = [ - GraphValueTypes.EDGE, - [ - id: number, - relationshipTypeId: number, - sourceId: number, - destinationId: number, - properties: GraphEntityRawProperties - ] + GraphValueTypes.EDGE, + [ + id: number, + relationshipTypeId: number, + sourceId: number, + destinationId: number, + properties: GraphEntityRawProperties + ] ]; type GraphNodeRawValue = [ - GraphValueTypes.NODE, - [ - id: number, - labelIds: Array, - properties: GraphEntityRawProperties - ] + GraphValueTypes.NODE, + [ + id: number, + labelIds: Array, + properties: GraphEntityRawProperties + ] ]; type GraphPathRawValue = [ - GraphValueTypes.PATH, - [ - nodes: [ - GraphValueTypes.ARRAY, - Array - ], - edges: [ - GraphValueTypes.ARRAY, - Array - ] + GraphValueTypes.PATH, + [ + nodes: [ + GraphValueTypes.ARRAY, + Array + ], + edges: [ + GraphValueTypes.ARRAY, + Array ] + ] ]; type GraphMapRawValue = [ - GraphValueTypes.MAP, - Array + GraphValueTypes.MAP, + Array ]; type GraphRawValue = [ - GraphValueTypes.NULL, - null + GraphValueTypes.NULL, + null ] | [ - GraphValueTypes.STRING, - string + GraphValueTypes.STRING, + string ] | [ - GraphValueTypes.INTEGER, - number + GraphValueTypes.INTEGER, + number ] | [ - GraphValueTypes.BOOLEAN, - string + GraphValueTypes.BOOLEAN, + string ] | [ - GraphValueTypes.DOUBLE, - string + GraphValueTypes.DOUBLE, + string ] | [ - GraphValueTypes.ARRAY, - Array + GraphValueTypes.ARRAY, + Array ] | GraphEdgeRawValue | GraphNodeRawValue | GraphPathRawValue | GraphMapRawValue | [ - GraphValueTypes.POINT, - [ - latitude: string, - longitude: string - ] + GraphValueTypes.POINT, + [ + latitude: string, + longitude: string + ] ]; type GraphEntityProperties = Record; interface GraphEdge { - id: number; - relationshipType: string; - sourceId: number; - destinationId: number; - properties: GraphEntityProperties; + id: number; + relationshipType: string; + sourceId: number; + destinationId: number; + properties: GraphEntityProperties; } interface GraphNode { - id: number; - labels: Array; - properties: GraphEntityProperties; + id: number; + labels: Array; + properties: GraphEntityProperties; } interface GraphPath { - nodes: Array; - edges: Array; + nodes: Array; + edges: Array; } type GraphMap = { - [key: string]: GraphValue; + [key: string]: GraphValue; }; type GraphValue = null | string | number | boolean | Array | { } | GraphEdge | GraphNode | GraphPath | GraphMap | { - latitude: string; - longitude: string; + latitude: string; + longitude: string; }; -type GraphReply = Omit & { - data?: Array; +type GraphReply = { + data?: Array; }; type GraphClientType = RedisClientType<{ - graph: { - query: typeof import('./commands/QUERY'), - roQuery: typeof import('./commands/RO_QUERY') - } + graph: { + query: typeof QUERY, + roQuery: typeof import('./commands/RO_QUERY').default + } }, RedisFunctions, RedisScripts>; export default class Graph { - #client: GraphClientType; - #name: string; - #metadata?: GraphMetadata; + #client: GraphClientType; + #name: string; + #metadata?: GraphMetadata; - constructor( - client: GraphClientType, - name: string - ) { - this.#client = client; - this.#name = name; - } + 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 query( + query: RedisArgument, + 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 - ) - ); - } + async roQuery( + query: RedisArgument, + options?: QueryOptions + ) { + return this.#parseReply( + await this.#client.graph.roQuery( + this.#name, + query, + options, + true + ) + ); + } - #setMetadataPromise?: Promise; + #setMetadataPromise?: Promise; - #updateMetadata(): Promise { - this.#setMetadataPromise ??= this.#setMetadata() - .finally(() => this.#setMetadataPromise = undefined); - return this.#setMetadataPromise; - } + #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()') - ]); + // 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]>) - }; + 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; - } + return this.#metadata; + } - #cleanMetadataArray(arr: Array<[string]>): Array { - return arr.map(([value]) => value); - } + #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); - } + #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}]`); + // 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; + } + + // TODO: reply type + async #parseReply(reply: any): 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; - } - async #parseReply(reply: QueryReply): Promise> { - if (!reply.data) return reply; + case GraphValueTypes.BOOLEAN: + return value === 'true'; - 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); - } + case GraphValueTypes.DOUBLE: + return parseFloat(value); - return data as unknown as T; - }) - }; + case GraphValueTypes.ARRAY: + return value.map(x => this.#parseValue(x, promises)); - if (promises.length) await Promise.all(promises); + case GraphValueTypes.EDGE: + return this.#parseEdge(value, 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; - } - } + case GraphValueTypes.NODE: + return this.#parseNode(value, promises); + case GraphValueTypes.PATH: return { - id, - labels, - properties: this.#parseProperties(properties, promises) + nodes: value[0][1].map(([, node]) => this.#parseNode(node, promises)), + edges: value[1][1].map(([, edge]) => this.#parseEdge(edge, 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; - } + 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 parsed; + 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/test-utils.ts b/packages/graph/lib/test-utils.ts index 56c0af56a2..e00b03fc69 100644 --- a/packages/graph/lib/test-utils.ts +++ b/packages/graph/lib/test-utils.ts @@ -2,19 +2,19 @@ import TestUtils from '@redis/test-utils'; import RedisGraph from '.'; export default new TestUtils({ - dockerImageName: 'redislabs/redisgraph', - dockerImageVersionArgument: 'redisgraph-version' + dockerImageName: 'redislabs/redisgraph', + dockerImageVersionArgument: 'redisgraph-version' }); export const GLOBAL = { - SERVERS: { - OPEN: { - serverArguments: ['--loadmodule /usr/lib/redis/modules/redisgraph.so'], - clientOptions: { - modules: { - graph: RedisGraph - } - } + SERVERS: { + OPEN: { + serverArguments: ['--loadmodule /usr/lib/redis/modules/redisgraph.so'], + clientOptions: { + modules: { + graph: RedisGraph } + } } + } }; diff --git a/packages/graph/package.json b/packages/graph/package.json index 17ae40d3b5..9f819a0f79 100644 --- a/packages/graph/package.json +++ b/packages/graph/package.json @@ -5,10 +5,12 @@ "main": "./dist/index.js", "types": "./dist/index.d.ts", "files": [ - "dist/" + "dist/", + "!dist/tsconfig.tsbuildinfo" ], "scripts": { - "test": "nyc -r text-summary -r lcov mocha -r source-map-support/register -r ts-node/register './lib/**/*.spec.ts'" }, + "test": "nyc -r text-summary -r lcov mocha -r source-map-support/register -r ts-node/register './lib/**/*.spec.ts'" + }, "peerDependencies": { "@redis/client": "*" },