Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • graphpolaris/microservices/query-service
1 result
Show changes
Commits on Source (2)
import type { AnyStatement } from 'ts-common/src/model/query/logic/general';
import { StringFilterTypes, type AnyStatement } from 'ts-common/src/model/query/logic/general';
import type { QueryCacheData } from './model';
import { log } from 'ts-common/src/logger/logger';
......@@ -32,7 +32,7 @@ export function extractLogicCypher(logicQuery: AnyStatement, cacheData: QueryCac
case 'object':
if (Array.isArray(logicQuery)) {
let op = logicQuery[0].replace('_', '').toLowerCase();
const right = logicQuery?.[2];
let right = logicQuery?.[2];
const { logic: left, where: whereLogic } = extractLogicCypher(logicQuery[1], cacheData);
switch (op) {
......@@ -46,6 +46,34 @@ export function extractLogicCypher(logicQuery: AnyStatement, cacheData: QueryCac
case 'like':
op = '=~';
break;
case 'in':
case 'present in list':
op = 'IN';
if (typeof right === 'string') {
return {
logic: `(${left} IN [${right
.replace(/"/g, '')
.split(',')
.map((r: string) => `"${r.trim()}"`)
.join(', ')}])`,
where: whereLogic,
};
}
break;
case 'not in':
case 'not in list':
op = 'NOT IN';
if (typeof right === 'string') {
return {
logic: `(NOT ${left} IN [${right
.replace(/"/g, '')
.split(',')
.map((r: string) => `"${r.trim()}"`)
.join(', ')}])`,
where: whereLogic,
};
}
break;
case 'isempty':
return { logic: `(${left} IS NULL OR ${left} = "")`, where: whereLogic };
case 'isnotempty':
......
import { query2Cypher } from './queryConverter';
import type { BackendQueryFormat } from 'ts-common';
import { StringFilterTypes, type BackendQueryFormat } from 'ts-common';
import { expect, test, describe, it } from 'bun:test';
function fixCypherSpaces(cypher?: string | null): string {
......@@ -61,7 +61,7 @@ describe('query2Cypher', () => {
RETURN * LIMIT 5000`;
const expectedCypherCount = `MATCH path1 = ((p1:Person)-[:DIRECTED*1..1]->(m1:Movie))
MATCH path2 = ((p1:Person)-[:IN_GENRE*1..1]->(g1:Genre))
RETURN COUNT(p1) as p1_count, COUNT(m1) as m1_count, COUNT(g1) as g1_count`;
RETURN COUNT(DISTINCT p1) as p1_count, COUNT(DISTINCT m1) as m1_count, COUNT(DISTINCT g1) as g1_count`;
expect(fixCypherSpaces(cypher.query)).toBe(fixCypherSpaces(expectedCypher));
expect(fixCypherSpaces(cypher.countQuery)).toBe(fixCypherSpaces(expectedCypherCount));
......@@ -663,4 +663,54 @@ describe('query2Cypher', () => {
'MATCH path_0 = (()-[id_1739982191754:HAS*1..1]-()) RETURN COUNT(DISTINCT id_1739982191754) as id_1739982191754_count';
expect(fixCypherSpaces(cypher.countQuery)).toEqual(fixCypherSpaces(expectedCypherCount));
});
it('should return correctly on a query with IN logic', () => {
const query: BackendQueryFormat = {
saveStateID: 'test',
limit: 500,
logic: [StringFilterTypes.IN, '@p1.name', '"John, Doe"'],
query: [
{
id: 'path1',
node: {
label: 'Person',
id: 'p1',
},
},
],
return: ['*'],
};
const cypher = query2Cypher(query);
const expectedCypher = `MATCH path1 = ((p1:Person))
WHERE (p1.name IN ["John", "Doe"])
RETURN * LIMIT 500`;
expect(fixCypherSpaces(cypher.query)).toBe(fixCypherSpaces(expectedCypher));
});
it('should return correctly on a query with NOT IN logic', () => {
const query: BackendQueryFormat = {
saveStateID: 'test',
limit: 500,
logic: [StringFilterTypes.NOT_IN, '@p1.name', '"John, Doe"'],
query: [
{
id: 'path1',
node: {
label: 'Person',
id: 'p1',
},
},
],
return: ['*'],
};
const cypher = query2Cypher(query);
const expectedCypher = `MATCH path1 = ((p1:Person))
WHERE (NOT p1.name IN ["John", "Doe"])
RETURN * LIMIT 500`;
expect(fixCypherSpaces(cypher.query)).toBe(fixCypherSpaces(expectedCypher));
});
});
......@@ -13,7 +13,7 @@ import {
type RecordShape,
} from 'neo4j-driver';
import { log } from '../../logger';
import type { CountQueryResultFromBackend, EdgeQueryResult, NodeQueryResult } from 'ts-common';
import type { CountQueryResultFromBackend, EdgeQueryResult, NodeAttributes, NodeQueryResult } from 'ts-common';
import type { GraphQueryResultFromBackend } from 'ts-common';
export function parseCypherQuery(result: RecordShape[], returnType: 'nodelink' | 'table' = 'nodelink'): GraphQueryResultFromBackend {
......@@ -42,7 +42,7 @@ export function parseCypherQuery(result: RecordShape[], returnType: 'nodelink' |
}
export function parseCountCypherQuery(result: RecordShape[]): CountQueryResultFromBackend {
try {
const countResult: CountQueryResultFromBackend = {};
const countResult: CountQueryResultFromBackend = { updatedAt: Date.now() };
for (let i = 0; i < result.length; i++) {
const r = result[i];
for (let j = 0; j < r.keys.length; j++) {
......@@ -114,23 +114,27 @@ function parseNodeLinkEntity(
}
}
function parseAttributes(attributes: any): NodeAttributes {
return Object.fromEntries(
Object.entries(attributes)?.map(([k, v]) => {
if (Integer.isInteger(v)) {
return [k, v.toNumber()];
} else if (typeof v === 'string') {
return [k, v];
} else if (v instanceof Object) {
return [k, v.toString()];
}
return [k, v];
}),
);
}
function parseNode(node: Node): NodeQueryResult {
return {
_id: node.identity.toString(),
label: node.labels[0], // TODO: only using first label
attributes: Object.fromEntries(
Object.entries(node.properties)?.map(([k, v]) => {
if (Integer.isInteger(v)) {
return [k, v.toNumber()];
} else if (typeof v === 'string') {
return [k, v];
} else if (v instanceof Object) {
return [k, v.toString()];
}
return [k, v];
}),
),
attributes: parseAttributes(node.properties),
};
}
......@@ -140,7 +144,7 @@ function parseEdge(edge: Relationship): EdgeQueryResult {
label: edge.type,
from: edge.start.toString(),
to: edge.end.toString(),
attributes: { ...edge.properties, type: edge.type },
attributes: { ...parseAttributes(edge.properties), type: edge.type },
};
}
......