Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix #2189 - add graph --compact support #2305

Merged
merged 7 commits into from Nov 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
37 changes: 17 additions & 20 deletions packages/graph/README.md
Expand Up @@ -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' }]
```
13 changes: 4 additions & 9 deletions packages/graph/lib/commands/QUERY.spec.ts
Expand Up @@ -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);
});
30 changes: 21 additions & 9 deletions packages/graph/lib/commands/QUERY.ts
@@ -1,41 +1,53 @@
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<string>;

type Data = Array<Array<string | number | null>>;
type Data = Array<string | number | null | Data>;

type Metadata = Array<string>;

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]
Expand Down
22 changes: 0 additions & 22 deletions packages/graph/lib/commands/QUERY_RO.spec.ts

This file was deleted.

17 changes: 17 additions & 0 deletions 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);
});
@@ -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';

Expand All @@ -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
);
}

Expand Down
62 changes: 62 additions & 0 deletions 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']
);
});
});
87 changes: 76 additions & 11 deletions packages/graph/lib/commands/index.ts
Expand Up @@ -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';

Expand All @@ -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<QueryParam>;

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;
}
}

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}`)
}
}