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 type { QueryCacheData } from './model';
import { log } from 'ts-common/src/logger/logger'; import { log } from 'ts-common/src/logger/logger';
...@@ -32,7 +32,7 @@ export function extractLogicCypher(logicQuery: AnyStatement, cacheData: QueryCac ...@@ -32,7 +32,7 @@ export function extractLogicCypher(logicQuery: AnyStatement, cacheData: QueryCac
case 'object': case 'object':
if (Array.isArray(logicQuery)) { if (Array.isArray(logicQuery)) {
let op = logicQuery[0].replace('_', '').toLowerCase(); let op = logicQuery[0].replace('_', '').toLowerCase();
const right = logicQuery?.[2]; let right = logicQuery?.[2];
const { logic: left, where: whereLogic } = extractLogicCypher(logicQuery[1], cacheData); const { logic: left, where: whereLogic } = extractLogicCypher(logicQuery[1], cacheData);
switch (op) { switch (op) {
...@@ -46,6 +46,34 @@ export function extractLogicCypher(logicQuery: AnyStatement, cacheData: QueryCac ...@@ -46,6 +46,34 @@ export function extractLogicCypher(logicQuery: AnyStatement, cacheData: QueryCac
case 'like': case 'like':
op = '=~'; op = '=~';
break; 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': case 'isempty':
return { logic: `(${left} IS NULL OR ${left} = "")`, where: whereLogic }; return { logic: `(${left} IS NULL OR ${left} = "")`, where: whereLogic };
case 'isnotempty': case 'isnotempty':
......
import { query2Cypher } from './queryConverter'; 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'; import { expect, test, describe, it } from 'bun:test';
function fixCypherSpaces(cypher?: string | null): string { function fixCypherSpaces(cypher?: string | null): string {
...@@ -61,7 +61,7 @@ describe('query2Cypher', () => { ...@@ -61,7 +61,7 @@ describe('query2Cypher', () => {
RETURN * LIMIT 5000`; RETURN * LIMIT 5000`;
const expectedCypherCount = `MATCH path1 = ((p1:Person)-[:DIRECTED*1..1]->(m1:Movie)) const expectedCypherCount = `MATCH path1 = ((p1:Person)-[:DIRECTED*1..1]->(m1:Movie))
MATCH path2 = ((p1:Person)-[:IN_GENRE*1..1]->(g1:Genre)) 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.query)).toBe(fixCypherSpaces(expectedCypher));
expect(fixCypherSpaces(cypher.countQuery)).toBe(fixCypherSpaces(expectedCypherCount)); expect(fixCypherSpaces(cypher.countQuery)).toBe(fixCypherSpaces(expectedCypherCount));
...@@ -663,4 +663,54 @@ describe('query2Cypher', () => { ...@@ -663,4 +663,54 @@ describe('query2Cypher', () => {
'MATCH path_0 = (()-[id_1739982191754:HAS*1..1]-()) RETURN COUNT(DISTINCT id_1739982191754) as id_1739982191754_count'; 'MATCH path_0 = (()-[id_1739982191754:HAS*1..1]-()) RETURN COUNT(DISTINCT id_1739982191754) as id_1739982191754_count';
expect(fixCypherSpaces(cypher.countQuery)).toEqual(fixCypherSpaces(expectedCypherCount)); 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 { ...@@ -13,7 +13,7 @@ import {
type RecordShape, type RecordShape,
} from 'neo4j-driver'; } from 'neo4j-driver';
import { log } from '../../logger'; 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'; import type { GraphQueryResultFromBackend } from 'ts-common';
export function parseCypherQuery(result: RecordShape[], returnType: 'nodelink' | 'table' = 'nodelink'): GraphQueryResultFromBackend { export function parseCypherQuery(result: RecordShape[], returnType: 'nodelink' | 'table' = 'nodelink'): GraphQueryResultFromBackend {
...@@ -42,7 +42,7 @@ export function parseCypherQuery(result: RecordShape[], returnType: 'nodelink' | ...@@ -42,7 +42,7 @@ export function parseCypherQuery(result: RecordShape[], returnType: 'nodelink' |
} }
export function parseCountCypherQuery(result: RecordShape[]): CountQueryResultFromBackend { export function parseCountCypherQuery(result: RecordShape[]): CountQueryResultFromBackend {
try { try {
const countResult: CountQueryResultFromBackend = {}; const countResult: CountQueryResultFromBackend = { updatedAt: Date.now() };
for (let i = 0; i < result.length; i++) { for (let i = 0; i < result.length; i++) {
const r = result[i]; const r = result[i];
for (let j = 0; j < r.keys.length; j++) { for (let j = 0; j < r.keys.length; j++) {
...@@ -114,23 +114,27 @@ function parseNodeLinkEntity( ...@@ -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 { function parseNode(node: Node): NodeQueryResult {
return { return {
_id: node.identity.toString(), _id: node.identity.toString(),
label: node.labels[0], // TODO: only using first label label: node.labels[0], // TODO: only using first label
attributes: Object.fromEntries( attributes: parseAttributes(node.properties),
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];
}),
),
}; };
} }
...@@ -140,7 +144,7 @@ function parseEdge(edge: Relationship): EdgeQueryResult { ...@@ -140,7 +144,7 @@ function parseEdge(edge: Relationship): EdgeQueryResult {
label: edge.type, label: edge.type,
from: edge.start.toString(), from: edge.start.toString(),
to: edge.end.toString(), to: edge.end.toString(),
attributes: { ...edge.properties, type: edge.type }, attributes: { ...parseAttributes(edge.properties), type: edge.type },
}; };
} }
......