Skip to content

Commit 1c6d74f

Browse files
authored
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
1 parent 64f86d6 commit 1c6d74f

File tree

12 files changed

+713
-77
lines changed

12 files changed

+713
-77
lines changed

packages/graph/README.md

+17-20
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,31 @@
22

33
Example usage:
44
```javascript
5-
import { createClient } from 'redis';
5+
import { createClient, Graph } from 'redis';
66

77
const client = createClient();
88
client.on('error', (err) => console.log('Redis Client Error', err));
99

1010
await client.connect();
1111

12-
await client.graph.query(
13-
'graph',
14-
"CREATE (:Rider { name: 'Buzz Aldrin' })-[:rides]->(:Team { name: 'Apollo' })"
15-
);
12+
const graph = new Graph(client, 'graph');
1613

17-
const result = await client.graph.query(
18-
'graph',
19-
`MATCH (r:Rider)-[:rides]->(t:Team) WHERE t.name = 'Apollo' RETURN r.name, t.name`
14+
await graph.query(
15+
'CREATE (:Rider { name: $riderName })-[:rides]->(:Team { name: $teamName })',
16+
{
17+
params: {
18+
riderName: 'Buzz Aldrin',
19+
teamName: 'Apollo'
20+
}
21+
}
2022
);
2123

22-
console.log(result);
23-
```
24+
const result = await graph.roQuery(
25+
'MATCH (r:Rider)-[:rides]->(t:Team { name: $name }) RETURN r.name AS name',
26+
{
27+
name: 'Apollo'
28+
}
29+
);
2430

25-
Output from console log:
26-
```json
27-
{
28-
headers: [ 'r.name', 't.name' ],
29-
data: [ [ 'Buzz Aldrin', 'Apollo' ] ],
30-
metadata: [
31-
'Cached execution: 0',
32-
'Query internal execution time: 0.431700 milliseconds'
33-
]
34-
}
31+
console.log(result.data); // [{ name: 'Buzz Aldrin' }]
3532
```

packages/graph/lib/commands/QUERY.spec.ts

+4-9
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,13 @@ import { transformArguments } from './QUERY';
55
describe('QUERY', () => {
66
it('transformArguments', () => {
77
assert.deepEqual(
8-
transformArguments('key', '*', 100),
9-
['GRAPH.QUERY', 'key', '*', '100']
8+
transformArguments('key', 'query'),
9+
['GRAPH.QUERY', 'key', 'query']
1010
);
1111
});
1212

1313
testUtils.testWithClient('client.graph.query', async client => {
14-
await client.graph.query('key',
15-
"CREATE (r:human {name:'roi', age:34}), (a:human {name:'amit', age:32}), (r)-[:knows]->(a)"
16-
);
17-
const reply = await client.graph.query('key',
18-
"MATCH (r:human)-[:knows]->(a:human) RETURN r.age, r.name"
19-
);
20-
assert.equal(reply.data.length, 1);
14+
const { data } = await client.graph.query('key', 'RETURN 0');
15+
assert.deepEqual(data, [[0]]);
2116
}, GLOBAL.SERVERS.OPEN);
2217
});

packages/graph/lib/commands/QUERY.ts

+21-9
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,53 @@
11
import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands/index';
2-
import { pushQueryArguments } from '.';
2+
import { pushQueryArguments, QueryOptionsBackwardCompatible } from '.';
33

44
export const FIRST_KEY_INDEX = 1;
55

66
export function transformArguments(
77
graph: RedisCommandArgument,
88
query: RedisCommandArgument,
9-
timeout?: number
9+
options?: QueryOptionsBackwardCompatible,
10+
compact?: boolean
1011
): RedisCommandArguments {
1112
return pushQueryArguments(
1213
['GRAPH.QUERY'],
1314
graph,
1415
query,
15-
timeout
16+
options,
17+
compact
1618
);
1719
}
1820

1921
type Headers = Array<string>;
2022

21-
type Data = Array<Array<string | number | null>>;
23+
type Data = Array<string | number | null | Data>;
2224

2325
type Metadata = Array<string>;
2426

2527
type QueryRawReply = [
2628
headers: Headers,
2729
data: Data,
2830
metadata: Metadata
31+
] | [
32+
metadata: Metadata
2933
];
3034

31-
interface QueryReply {
32-
headers: Headers,
33-
data: Data,
34-
metadata: Metadata
35+
export type QueryReply = {
36+
headers: undefined;
37+
data: undefined;
38+
metadata: Metadata;
39+
} | {
40+
headers: Headers;
41+
data: Data;
42+
metadata: Metadata;
3543
};
3644

3745
export function transformReply(reply: QueryRawReply): QueryReply {
38-
return {
46+
return reply.length === 1 ? {
47+
headers: undefined,
48+
data: undefined,
49+
metadata: reply[0]
50+
} : {
3951
headers: reply[0],
4052
data: reply[1],
4153
metadata: reply[2]

packages/graph/lib/commands/QUERY_RO.spec.ts

-22
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { strict as assert } from 'assert';
2+
import testUtils, { GLOBAL } from '../test-utils';
3+
import { transformArguments } from './RO_QUERY';
4+
5+
describe('RO_QUERY', () => {
6+
it('transformArguments', () => {
7+
assert.deepEqual(
8+
transformArguments('key', 'query'),
9+
['GRAPH.RO_QUERY', 'key', 'query']
10+
);
11+
});
12+
13+
testUtils.testWithClient('client.graph.roQuery', async client => {
14+
const { data } = await client.graph.roQuery('key', 'RETURN 0');
15+
assert.deepEqual(data, [[0]]);
16+
}, GLOBAL.SERVERS.OPEN);
17+
});

packages/graph/lib/commands/QUERY_RO.ts renamed to packages/graph/lib/commands/RO_QUERY.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands';
2-
import { pushQueryArguments } from '.';
2+
import { pushQueryArguments, QueryOptionsBackwardCompatible } from '.';
33

44
export { FIRST_KEY_INDEX } from './QUERY';
55

@@ -8,13 +8,15 @@ export const IS_READ_ONLY = true;
88
export function transformArguments(
99
graph: RedisCommandArgument,
1010
query: RedisCommandArgument,
11-
timeout?: number
11+
options?: QueryOptionsBackwardCompatible,
12+
compact?: boolean
1213
): RedisCommandArguments {
1314
return pushQueryArguments(
1415
['GRAPH.RO_QUERY'],
1516
graph,
1617
query,
17-
timeout
18+
options,
19+
compact
1820
);
1921
}
2022

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { strict as assert } from 'assert';
2+
import { pushQueryArguments } from '.';
3+
4+
describe('pushQueryArguments', () => {
5+
it('simple', () => {
6+
assert.deepEqual(
7+
pushQueryArguments(['GRAPH.QUERY'], 'graph', 'query'),
8+
['GRAPH.QUERY', 'graph', 'query']
9+
);
10+
});
11+
12+
describe('params', () => {
13+
it('all types', () => {
14+
assert.deepEqual(
15+
pushQueryArguments(['GRAPH.QUERY'], 'graph', 'query', {
16+
params: {
17+
null: null,
18+
string: '"\\',
19+
number: 0,
20+
boolean: false,
21+
array: [0],
22+
object: {a: 0}
23+
}
24+
}),
25+
['GRAPH.QUERY', 'graph', 'CYPHER null=null string="\\"\\\\" number=0 boolean=false array=[0] object={a:0} query']
26+
);
27+
});
28+
29+
it('TypeError', () => {
30+
assert.throws(() => {
31+
pushQueryArguments(['GRAPH.QUERY'], 'graph', 'query', {
32+
params: {
33+
a: undefined as any
34+
}
35+
})
36+
}, TypeError);
37+
});
38+
});
39+
40+
it('TIMEOUT backward compatible', () => {
41+
assert.deepEqual(
42+
pushQueryArguments(['GRAPH.QUERY'], 'graph', 'query', 1),
43+
['GRAPH.QUERY', 'graph', 'query', 'TIMEOUT', '1']
44+
);
45+
});
46+
47+
it('TIMEOUT', () => {
48+
assert.deepEqual(
49+
pushQueryArguments(['GRAPH.QUERY'], 'graph', 'query', {
50+
TIMEOUT: 1
51+
}),
52+
['GRAPH.QUERY', 'graph', 'query', 'TIMEOUT', '1']
53+
);
54+
});
55+
56+
it('compact', () => {
57+
assert.deepEqual(
58+
pushQueryArguments(['GRAPH.QUERY'], 'graph', 'query', undefined, true),
59+
['GRAPH.QUERY', 'graph', 'query', '--compact']
60+
);
61+
});
62+
});

packages/graph/lib/commands/index.ts

+76-11
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import * as DELETE from './DELETE';
44
import * as EXPLAIN from './EXPLAIN';
55
import * as LIST from './LIST';
66
import * as PROFILE from './PROFILE';
7-
import * as QUERY_RO from './QUERY_RO';
87
import * as QUERY from './QUERY';
8+
import * as RO_QUERY from './RO_QUERY';
99
import * as SLOWLOG from './SLOWLOG';
1010
import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands';
1111

@@ -22,28 +22,93 @@ export default {
2222
list: LIST,
2323
PROFILE,
2424
profile: PROFILE,
25-
QUERY_RO,
26-
queryRo: QUERY_RO,
2725
QUERY,
2826
query: QUERY,
27+
RO_QUERY,
28+
roQuery: RO_QUERY,
2929
SLOWLOG,
3030
slowLog: SLOWLOG
3131
};
3232

33+
type QueryParam = null | string | number | boolean | QueryParams | Array<QueryParam>;
34+
35+
type QueryParams = {
36+
[key: string]: QueryParam;
37+
};
38+
39+
export interface QueryOptions {
40+
params?: QueryParams;
41+
TIMEOUT?: number;
42+
}
43+
44+
export type QueryOptionsBackwardCompatible = QueryOptions | number;
45+
3346
export function pushQueryArguments(
3447
args: RedisCommandArguments,
3548
graph: RedisCommandArgument,
3649
query: RedisCommandArgument,
37-
timeout?: number
50+
options?: QueryOptionsBackwardCompatible,
51+
compact?: boolean
3852
): RedisCommandArguments {
39-
args.push(
40-
graph,
41-
query
42-
);
53+
args.push(graph);
4354

44-
if (timeout !== undefined) {
45-
args.push(timeout.toString());
55+
if (typeof options === 'number') {
56+
args.push(query);
57+
pushTimeout(args, options);
58+
} else {
59+
args.push(
60+
options?.params ?
61+
`CYPHER ${queryParamsToString(options.params)} ${query}` :
62+
query
63+
);
64+
65+
if (options?.TIMEOUT !== undefined) {
66+
pushTimeout(args, options.TIMEOUT);
67+
}
68+
}
69+
70+
if (compact) {
71+
args.push('--compact');
4672
}
4773

4874
return args;
49-
}
75+
}
76+
77+
function pushTimeout(args: RedisCommandArguments, timeout: number): void {
78+
args.push('TIMEOUT', timeout.toString());
79+
}
80+
81+
function queryParamsToString(params: QueryParams): string {
82+
const parts = [];
83+
for (const [key, value] of Object.entries(params)) {
84+
parts.push(`${key}=${queryParamToString(value)}`);
85+
}
86+
return parts.join(' ');
87+
}
88+
89+
function queryParamToString(param: QueryParam): string {
90+
if (param === null) {
91+
return 'null';
92+
}
93+
94+
switch (typeof param) {
95+
case 'string':
96+
return `"${param.replace(/["\\]/g, '\\$&')}"`;
97+
98+
case 'number':
99+
case 'boolean':
100+
return param.toString();
101+
}
102+
103+
if (Array.isArray(param)) {
104+
return `[${param.map(queryParamToString).join(',')}]`;
105+
} else if (typeof param === 'object') {
106+
const body = [];
107+
for (const [key, value] of Object.entries(param)) {
108+
body.push(`${key}:${queryParamToString(value)}`);
109+
}
110+
return `{${body.join(',')}}`;
111+
} else {
112+
throw new TypeError(`Unexpected param type ${typeof param} ${param}`)
113+
}
114+
}

0 commit comments

Comments
 (0)