diff --git a/.gitignore b/.gitignore index 9d71eeb8..38e5b070 100644 --- a/.gitignore +++ b/.gitignore @@ -133,3 +133,5 @@ package-lock.json docs/build vite.config.ts yarn.lock + +.early.coverage \ No newline at end of file diff --git a/@types/react-codemirror.d.ts b/@types/react-codemirror.d.ts new file mode 100644 index 00000000..3f6e48bd --- /dev/null +++ b/@types/react-codemirror.d.ts @@ -0,0 +1,5 @@ +declare module '@neo4j-cypher/react-codemirror' { + import { ComponentType } from 'react'; + + export const CypherEditor: ComponentType; + } \ No newline at end of file diff --git a/package.json b/package.json index 7e28d5b6..31c07125 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ "preview": "vite preview" }, "dependencies": { + "@neo4j-cypher/react-codemirror": "^1.0.4", + "@neo4j-devtools/word-color": "^0.0.8", "@neo4j-ndl/base": "^3.0.14", "@neo4j-ndl/react": "^3.0.24", "@neo4j-nvl/react": "^0.3.6", diff --git a/src/App.tsx b/src/App.tsx index c31f145f..58b324f1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -19,6 +19,7 @@ import messagesData from './templates/shared/assets/ChatbotMessages.json'; import ConnectionModal from './templates/shared/components/ConnectionModal'; import Header from './templates/shared/components/Header'; import User from './templates/shared/components/User'; +import CypherBlock from './templates/shared/components/CypherBlock'; import { FileContextProvider } from './context/connectionFile'; @@ -59,6 +60,21 @@ function App() { /> } /> } /> + + + + } + /> } /> diff --git a/src/landingPage/categories/Component.tsx b/src/landingPage/categories/Component.tsx index b8cfa301..82c7bb02 100644 --- a/src/landingPage/categories/Component.tsx +++ b/src/landingPage/categories/Component.tsx @@ -60,6 +60,16 @@ export default function Component() { }/src/templates/shared/components/Card.tsx`, previewLink: '/cards-preview', }, + { + title: 'CypherBlock', + description: + 'xxx', + image: colorMode === 'dark' ? CardImgDark : CardImgLight, + sourceCode: `https://github.com/neo4j-labs/neo4j-needle-starterkit/blob/${ + import.meta.env.PACKAGE_VERSION + }/src/templates/shared/components/Card.tsx`, + previewLink: '/cypherblock-preview', + } ]; return ( diff --git a/src/templates/foundation/Content.tsx b/src/templates/foundation/Content.tsx index 1c7dd213..bea127df 100644 --- a/src/templates/foundation/Content.tsx +++ b/src/templates/foundation/Content.tsx @@ -3,6 +3,7 @@ import { Button, Label, Typography } from '@neo4j-ndl/react'; import { setDriver, disconnect } from '../shared/utils/Driver'; import ConnectionModal from '../shared/components/ConnectionModal'; +import { Driver } from 'neo4j-driver'; export default function Content() { const [init, setInit] = useState(false); @@ -14,8 +15,8 @@ export default function Content() { let session = localStorage.getItem('needleStarterKit-neo4j.connection'); if (session) { let neo4jConnection = JSON.parse(session); - setDriver(neo4jConnection.uri, neo4jConnection.user, neo4jConnection.password).then((isSuccessful: boolean) => { - setConnectionStatus(isSuccessful); + setDriver(neo4jConnection.uri, neo4jConnection.user, neo4jConnection.password).then((driver: false | Driver) => { + setConnectionStatus(driver instanceof Driver); }); } setInit(true); diff --git a/src/templates/shared/components/ConnectionModal.tsx b/src/templates/shared/components/ConnectionModal.tsx index 81eae88a..7916acf3 100644 --- a/src/templates/shared/components/ConnectionModal.tsx +++ b/src/templates/shared/components/ConnectionModal.tsx @@ -1,6 +1,7 @@ import { Button, Dialog, TextInput, Select, Banner, Dropzone } from '@neo4j-ndl/react'; import { useState } from 'react'; import { setDriver } from '../utils/Driver'; +import { Driver } from 'neo4j-driver'; interface Message { type: 'success' | 'info' | 'warning' | 'danger' | 'neutral'; @@ -78,13 +79,16 @@ export default function ConnectionModal({ function submitConnection() { const connectionURI = `${protocol}://${URI}${URI.split(':')[1] ? '' : `:${port}`}`; setDriver(connectionURI, username, password).then((isSuccessful) => { - setConnectionStatus(isSuccessful); - isSuccessful - ? setOpenConnection(false) - : setMessage({ - type: 'danger', - content: 'Connection failed, please check the developer console logs for more informations', - }); + setConnectionStatus(isSuccessful instanceof Driver); + if (isSuccessful instanceof Driver) { + setOpenConnection(false); + setMessage({ type: 'success', content: `Connected to ${connectionURI}` }); + } else { + setMessage({ + type: 'danger', + content: 'Connection failed, please check the developer console logs for more informations', + }); + } }); } @@ -93,10 +97,10 @@ export default function ConnectionModal({ setOpenConnection(false)} > (themeUtils.colorMode); + + const [openConnection, setOpenConnection] = useState(false); + const [connectionStatus, setConnectionStatus] = useState(false); + + const cypherEditorProps = { + lineNumbers: false, + lint: true, + autocomplete: true, + theme: themeMode, + onValueChanged: (value: string) => setQuery(value), + autofocus: true, + }; + const nvl = useRef(null); + + const [nodes, setNodes] = useState([]); + const [rels, setRels] = useState([]); + + const [query, setQuery] = useState(props.initialQuery ?? 'MATCH (a) \nRETURN a'); + const [queryLimit, setqueryLimit] = useState(false); + const [isMultiQuery, setIsMultiQuery] = useState(false); + + const mouseEventCallbacks: MouseEventCallbacks = { + onHover: (_element: Node | Relationship, _hitTargets: HitTargets, _evt: MouseEvent) => null, + onRelationshipRightClick: (_rel: Relationship, _hitTargets: HitTargets, _evt: MouseEvent) => null, + onNodeClick: (_node: Node, _hitTargets: HitTargets, _evt: MouseEvent) => null, + onNodeRightClick: (_node: Node, _hitTargets: HitTargets, _evt: MouseEvent) => null, + onNodeDoubleClick: (_node: Node, _hitTargets: HitTargets, _evt: MouseEvent) => null, + onRelationshipClick: (_rel: Relationship, _hitTargets: HitTargets, _evt: MouseEvent) => null, + onRelationshipDoubleClick: (_rel: Relationship, _hitTargets: HitTargets, _evt: MouseEvent) => null, + onCanvasClick: (_evt: MouseEvent) => null, + onCanvasDoubleClick: (_evt: MouseEvent) => null, + onCanvasRightClick: (_evt: MouseEvent) => null, + onDrag: (_nodes: Node[]) => null, + onPan: (_panning: { x: number; y: number }, _evt: MouseEvent) => null, + onZoom: (_zoomLevel: number) => null, + }; + + const fitNodes = () => { + nvl.current?.fit(nodes.map((n) => n.id)); + }; + const resetZoom = () => { + nvl.current?.resetZoom(); + }; + + function setDriverFromProps() { + let driver: Driver = {} as Driver; + if (props.neo4jConnection && typeof props.neo4jConnection === 'string') { + const connectionDetailsString = localStorage.getItem(props.neo4jConnection) ?? ''; + const connectionDetails: { uri: string; user: string; password: string } = JSON.parse(connectionDetailsString); + return setDriver(connectionDetails.uri, connectionDetails.user, connectionDetails.password).then( + (d: Driver | boolean) => { + if (d instanceof Driver) { + return d; + } + throw new Error('Failed to set driver'); + } + ); + } else if (typeof props.neo4jConnection === 'object' && props.neo4jConnection.constructor.name === 'Driver2') { + driver = props.neo4jConnection; + } + return Promise.resolve(driver); + } + + function submitQuery(query: string) { + const limit = !queryLimit ? props.limitResultSet : false; + !queryLimit + ? toast.danger(`You have not set a LIMIT in your return. We have applied a default of ${props.limitResultSet}`, { + isCloseable: true, + shouldAutoClose: true, + }) + : null; + isMultiQuery + ? toast.danger('Multi-query detected, only single queries are supported. Execution has been cancelled.', { + isCloseable: true, + shouldAutoClose: true, + }) + : null; + if (!isMultiQuery) { + setDriverFromProps().then((driver) => { + runQuery(query, driver, limit).then((nvlGraph) => { + if ('error' in nvlGraph) { + toast.danger(`Query error: ${nvlGraph.error}`, { isCloseable: true, shouldAutoClose: true }); + } else { + if (nvlGraph.nodes.length === 0 && nvlGraph.relationships.length === 0) { + toast.neutral('Query returned no results', { isCloseable: true, shouldAutoClose: true }); + console.log('Query returned no results'); + console.log(nvlGraph); + } + setNodes(nvlGraph.nodes); + setRels(nvlGraph.relationships); + nvl.current?.addAndUpdateElementsInGraph(nvlGraph.nodes, nvlGraph.relationships); + nvl.current?.fit(nvlGraph.nodes.map((n) => n.id)); + } + }); + }); + } + } + + useEffect(() => { + const checkLimit = (query: string) => { + const limitPattern = /LIMIT\s+(\d+);?$/i; + const match = query.match(limitPattern); + if (match) { + return parseInt(match[1], 10); + } + return false; + }; + + const checkMultiQuery = (query: string) => { + const multiQueryPattern = /;(.+)/s; + return multiQueryPattern.test(query); + }; + + setIsMultiQuery(checkMultiQuery(query)); + + setqueryLimit(checkLimit(query)); + }, [query]); + + return ( +
+ +
+
+ + + Cypher Query + + + + {connectionStatus ? `Connected` : 'Not connected'} + + + + + + + submitQuery(query)} + > + + + + + {connectionStatus ? 'Run your query' : 'Connect to your database first'} + + + + + setOpenConnection(true)} + > + + + + {connectionStatus ? 'Switch connection' : 'Configure your connection'} + + + + void; }) => { + if (e.metaKey && e.key === 'Enter' && connectionStatus) { + e.preventDefault(); + submitQuery(query); + } + }} /> +
+ +
+ + + + + + + + + + {/* */} + + null} + mouseEventCallbacks={mouseEventCallbacks} + nvlOptions={{ + initialZoom: 0, + layout: 'd3Force', + relationshipThreshold: 1, + }} + className='' + /> +
+
+
+ +
+ ); +} diff --git a/src/templates/shared/utils/ChartUtils.ts b/src/templates/shared/utils/ChartUtils.ts new file mode 100644 index 00000000..da4049c0 --- /dev/null +++ b/src/templates/shared/utils/ChartUtils.ts @@ -0,0 +1,48 @@ + +export function valueIsArray(value) { + const className = value !== undefined && value.__proto__.constructor.name; + return className == 'Array'; + } + + export function valueIsNode(value) { + // const className = value.__proto__.constructor.name; + // return className == "Node"; + return value && value.labels && value.identity && value.properties; + } + + export function valueIsRelationship(value) { + // const className = value.__proto__.constructor.name; + // return className == "Relationship"; + return value && value.type && value.start && value.end && value.identity && value.properties; + } + + export function valueIsPath(value) { + // const className = value.__proto__.constructor.name; + // return className == "Path" + return value && value.start && value.end && value.segments && value.length; + } + + export function valueisPoint(value) { + // Look at the properties and identify the type. + return value && value.x && value.y && value.srid; + } + + export function valueIsObject(value) { + // TODO - this will not work in production builds. Need alternative. + const className = value.__proto__.constructor.name; + return className == 'Object'; + } + + export function toNumber(ref) { + if (ref === undefined || typeof ref === 'number') { + return ref; + } + let { low, high } = ref; + let res = high; + + for (let i = 0; i < 32; i++) { + res *= 2; + } + + return low + res; + } \ No newline at end of file diff --git a/src/templates/shared/utils/CypherDataTypes.ts b/src/templates/shared/utils/CypherDataTypes.ts new file mode 100644 index 00000000..d51ab667 --- /dev/null +++ b/src/templates/shared/utils/CypherDataTypes.ts @@ -0,0 +1,60 @@ +import type { + Date, + DateTime, + Duration, + Integer, + LocalDateTime, + LocalTime, + Point, + Time, +} from 'neo4j-driver'; + +/** + The neo4j driver type mapping - https://neo4j.com/docs/javascript-manual/current/cypher-workflow/#js-driver-type-mapping + + Star denotes custom driver class. + + Cypher(neo4j) - Driver type(js) + null - null + List - array + Map - Object + Boolean - boolean + Integer - Integer* + Float - number + String - string + ByteArray - Int8Array + Date - Date* + Time - Time* + LocalTime - LocalTime* + DateTime - DateTime* + LocalDateTime - LocalDateTime* + Duration - Duration* + Point - Point* + Node - Node* + Relationship - Relationship* + Path - Path* + */ + +export type CypherBasicPropertyType = + | null + | boolean + | number + | string + | Integer + | bigint + | Int8Array + | CypherTemporalType + | Point; + +export type CypherTemporalType = + | Date + | Time + | DateTime + | LocalTime + | LocalDateTime + | Duration; + +// Lists are also allowed as property types, as long as all items are the same basic type +export type CypherProperty = + | CypherBasicPropertyType + | CypherBasicPropertyType[]; \ No newline at end of file diff --git a/src/templates/shared/utils/Driver.tsx b/src/templates/shared/utils/Driver.tsx index cde565f3..eaabe427 100644 --- a/src/templates/shared/utils/Driver.tsx +++ b/src/templates/shared/utils/Driver.tsx @@ -1,6 +1,10 @@ /* eslint-disable no-console */ import neo4j, { Driver } from 'neo4j-driver'; import { nvlResultTransformer } from '@neo4j-nvl/base'; +import { calcWordColor } from '@neo4j-devtools/word-color'; +import { graphResultTransformer } from './GraphResultTransformer'; + +import { extractGraphEntitiesFromField } from './RecordUtils'; export let driver: Driver; @@ -12,7 +16,7 @@ export async function setDriver(connectionURI: string, username: string, passwor 'needleStarterKit-neo4j.connection', JSON.stringify({ uri: connectionURI, user: username, password: password }) ); - return true; + return driver; } catch (err) { console.error(`Connection error\n${err}\nCause: ${err as Error}`); return false; @@ -39,9 +43,9 @@ export async function runRAGQuery(sources: Array) { return { ...node, caption: properties.name ?? labels[0], + color: calcWordColor(properties.name ?? labels[0]), }; }); - console.log(nodes); const relationships = nvlGraph.relationships.map((rel) => { const or = nvlGraph.recordObjectMap.get(rel.id); return { @@ -52,6 +56,90 @@ export async function runRAGQuery(sources: Array) { return { nodes, relationships }; } +export async function runQuery(query: string, driver: Driver, limit: number | boolean) { + try { + // Customize the RETRIEVAL_QUERY to match your needs + let formattedQuery = `${query}`; + if (typeof limit === 'number') { + if (formattedQuery.trim().endsWith(';')) { + formattedQuery = `${formattedQuery.trim().slice(0, -1) } LIMIT ${limit};`; + } else { + formattedQuery += ` LIMIT ${limit}`; + } + } + runQueryAndExtractEntities(formattedQuery, driver, limit); + const nvlGraph = await driver.executeQuery(formattedQuery, {}, { resultTransformer: nvlResultTransformer }); + const nodes = nvlGraph.nodes.map((node) => { + const { properties, labels } = nvlGraph.recordObjectMap.get(node.id); + return { + ...node, + caption: properties.name ?? labels[0], + color: calcWordColor(properties.name ?? labels[0]), + }; + }); + const relationships = nvlGraph.relationships.map((rel) => { + const or = nvlGraph.recordObjectMap.get(rel.id); + return { + ...rel, + caption: or.type, + }; + }); + return { nodes, relationships }; + } catch (err) { + console.error(`Query error\n${err}\nCause: ${err as Error}`); + return { error: (err as Error).message }; + } +} + +export async function runQueryAndExtractEntities(query: string, driver: Driver, limit: number | boolean) { +try{ + const resultTest = await driver.executeQuery(query, {}, {resultTransformer: graphResultTransformer}); + console.log("resultTest"); + console.log(resultTest.records); + console.log(resultTest.summary); + console.log(resultTest); + console.log(resultTest.nodes, resultTest.relationships); + const res = await driver.executeQuery(query, {}, { }).then((result) => { + const nodes = []; + const links = []; + const nodeLabels = {}; + const linkTypes = {}; + const nodePositions = {}; + const { records } = result; + for (let record of records) { + for (let key in record) { + extractGraphEntitiesFromField( + record[key], + nodes, + links, + nodeLabels, + linkTypes, + false, + 'size', + 10, + 'width', + 1, + 'color', + '#000000', + nodePositions + ); + } + } + console.log('nodes', nodes); + console.log('links', links); + console.log('nodeLabels', nodeLabels); + console.log('linkTypes', linkTypes); + console.log('nodePositions', nodePositions); + return { nodes, links, nodeLabels, linkTypes, nodePositions }; + } + ); + +} catch (err) { + console.error(`Query error\n${err}\nCause: ${err as Error}`); + return { error: (err as Error).message }; + } +} + /* Everything below this line is only for providing examples based on datasets available in Neo4j Sandbox (sandbox.neo4j.com). When using this code in your own project, you should remove the examples below and use your own queries. diff --git a/src/templates/shared/utils/ExtractNodesRels.ts b/src/templates/shared/utils/ExtractNodesRels.ts new file mode 100644 index 00000000..1ee6344b --- /dev/null +++ b/src/templates/shared/utils/ExtractNodesRels.ts @@ -0,0 +1,130 @@ +import type { + Integer, + Node, + Path, + Record, + RecordShape, + Relationship, +} from 'neo4j-driver'; +import { isNode, isPath, isRelationship } from 'neo4j-driver'; +import { CypherProperty } from './CypherDataTypes'; + +export type Properties = RecordShape; + +/** + * Result type containing deduplicated nodes and relationships extracted from Neo4j records. + */ +export type DeduplicatedBasicNodesAndRels = { + /** Array of unique nodes found in the records */ + nodes: Node[]; + /** Array of unique relationships found in the records */ + relationships: Relationship[]; + /** Whether the max node limit was reached during extraction */ + limitHit: boolean; +}; + +/** + * Extracts and deduplicates nodes and relationships from Neo4j query records. + * + * This function processes Neo4j records to find all nodes and relationships, + * removing duplicates based on their element IDs. It can handle various data + * structures including individual nodes/relationships, paths, arrays, and + * nested objects. + * + * @param records - Array of Neo4j records to process + * @param options - Configuration options for extraction + * @param options.nodeLimit - Maximum number of unique nodes to extract (optional) + * @param options.keepDanglingRels - Whether to keep relationships whose start/end nodes are missing (default: false) + * + * @returns The {@link DeduplicatedBasicNodesAndRels} containing unique nodes and relationships + */ +export const extractUniqueNodesAndRels = ( + records: Record[], + { + nodeLimit, + keepDanglingRels = false, + }: { nodeLimit?: number; keepDanglingRels?: boolean } = {}, +): DeduplicatedBasicNodesAndRels => { + let limitHit = false; + if (records.length === 0) { + return { nodes: [], relationships: [], limitHit: false }; + } + + const items = new Set(); + + for (const record of records) { + for (const key of record.keys) { + items.add(record.get(key)); + } + } + + const paths: Path[] = []; + + const nodeMap = new Map(); + function addNode(n: Node) { + if (!limitHit) { + const id = n.elementId.toString(); + if (!nodeMap.has(id)) { + nodeMap.set(id, n); + } + if (typeof nodeLimit === 'number' && nodeMap.size === nodeLimit) { + limitHit = true; + } + } + } + + const relMap = new Map(); + function addRel(r: Relationship) { + const id = r.elementId.toString(); + if (!relMap.has(id)) { + relMap.set(id, r); + } + } + + const findAllEntities = (item: unknown) => { + if (typeof item !== 'object' || !item) { + return; + } + + if (isRelationship(item)) { + addRel(item); + } else if (isNode(item)) { + addNode(item); + } else if (isPath(item)) { + paths.push(item); + } else if (Array.isArray(item)) { + item.forEach(findAllEntities); + } else { + Object.values(item).forEach(findAllEntities); + } + }; + + findAllEntities(Array.from(items)); + + for (const path of paths) { + addNode(path.start); + addNode(path.end); + for (const segment of path.segments) { + addNode(segment.start); + addNode(segment.end); + addRel(segment.relationship); + } + } + + const nodes = Array.from(nodeMap.values()); + + const relationships = Array.from(relMap.values()).filter((item) => { + if (keepDanglingRels) { + return true; + } + + // We'd get dangling relationships from + // match ()-[a:ACTED_IN]->() return a; + // or from hitting the node limit + const start = item.startNodeElementId.toString(); + const end = item.endNodeElementId.toString(); + return nodeMap.has(start) && nodeMap.has(end); + }); + + return { nodes, relationships, limitHit }; +}; \ No newline at end of file diff --git a/src/templates/shared/utils/GraphResultTransformer.ts b/src/templates/shared/utils/GraphResultTransformer.ts new file mode 100644 index 00000000..8fb3fb26 --- /dev/null +++ b/src/templates/shared/utils/GraphResultTransformer.ts @@ -0,0 +1,57 @@ +import { + Integer, + Record, + ResultSummary, + resultTransformers, +} from 'neo4j-driver'; + +import { + DeduplicatedBasicNodesAndRels, + extractUniqueNodesAndRels, +} from './ExtractNodesRels'; + +/** + * Result type for graph queries that includes deduplicated nodes and relationships + * along with the original records and query summary. + * + * See {@link DeduplicatedBasicNodesAndRels} for the type of the nodes and relationships. + */ +export type GraphResult = DeduplicatedBasicNodesAndRels & { + /** Original Neo4j records returned by the query */ + records: Record[]; + summary: ResultSummary; +}; + +/** + * A result transformer that processes Neo4j query results into a graph format + * with deduplicated nodes and relationships. + * + * This transformer extracts unique nodes and relationships from query records + * while preserving the original records and summary information. It's particularly + * useful for graph visualization and analysis where duplicate entities need to be + * consolidated. + * + * @example + * ```typescript + * const result: GraphResult = await driver.executeQuery( + * 'MATCH p=(name: $name)-[]->() RETURN p', + * { name: 'John' }, + * { resultTransformer: graphResultTransformer }, + * ); + * + * console.log(result.nodes, result.relationships); + * ``` + * + * @returns ResultTransformer producing {@link GraphResult} + */ +export const graphResultTransformer = + resultTransformers.mappedResultTransformer({ + map(record) { + return record; + }, + collect(records, summary): GraphResult { + const { nodes, relationships, limitHit } = + extractUniqueNodesAndRels(records); + return { nodes, relationships, limitHit, records, summary }; + }, + }); \ No newline at end of file diff --git a/src/templates/shared/utils/RecordUtils.ts b/src/templates/shared/utils/RecordUtils.ts new file mode 100644 index 00000000..5128dd1c --- /dev/null +++ b/src/templates/shared/utils/RecordUtils.ts @@ -0,0 +1,119 @@ +import { valueIsArray, valueIsNode, valueIsRelationship, valueIsPath, toNumber } from './ChartUtils'; +export function extractGraphEntitiesFromField( + value, + nodes: Record[], + links: Record[], + nodeLabels: Record, + linkTypes: Record, + frozen: boolean, + nodeSizeProperty: string, + defaultNodeSize: number, + relWidthProperty: string, + defaultRelWidth: number, + relColorProperty: string, + defaultRelColor: string, + nodePositions: Record[] + ) { + if (value == undefined) { + return; + } + if (valueIsArray(value)) { + value.forEach((v) => + extractGraphEntitiesFromField( + v, + nodes, + links, + nodeLabels, + linkTypes, + frozen, + nodeSizeProperty, + defaultNodeSize, + relWidthProperty, + defaultRelWidth, + relColorProperty, + defaultRelColor, + nodePositions + ) + ); + } else if (valueIsNode(value)) { + value.labels.forEach((l) => (nodeLabels[l] = true)); + nodes[value.identity.low] = { + id: value.identity.low, + labels: value.labels, + size: !Number.isNaN(value.properties[nodeSizeProperty]) + ? toNumber(value.properties[nodeSizeProperty]) + : defaultNodeSize, + properties: value.properties, + mainLabel: value.labels[value.labels.length - 1], + }; + if (frozen && nodePositions && nodePositions[value.identity.low]) { + nodes[value.identity.low].fx = nodePositions[value.identity.low][0]; + nodes[value.identity.low].fy = nodePositions[value.identity.low][1]; + } + } else if (valueIsRelationship(value)) { + if (links[`${value.start.low},${value.end.low}`] == undefined) { + links[`${value.start.low},${value.end.low}`] = []; + } + const addItem = (arr, item) => arr.find((x) => x.id === item.id) || arr.push(item); + addItem(links[`${value.start.low},${value.end.low}`], { + id: value.identity.low, + source: value.start.low, + target: value.end.low, + type: value.type, + width: + value.properties[relWidthProperty] !== undefined && !Number.isNaN(value.properties[relWidthProperty]) + ? toNumber(value.properties[relWidthProperty]) + : defaultRelWidth, + color: value.properties[relColorProperty] ? value.properties[relColorProperty] : defaultRelColor, + properties: value.properties, + }); + } else if (valueIsPath(value)) { + value.segments.map((segment) => { + extractGraphEntitiesFromField( + segment.start, + nodes, + links, + nodeLabels, + linkTypes, + frozen, + nodeSizeProperty, + defaultNodeSize, + relWidthProperty, + defaultRelWidth, + relColorProperty, + defaultRelColor, + nodePositions + ); + extractGraphEntitiesFromField( + segment.relationship, + nodes, + links, + nodeLabels, + linkTypes, + frozen, + nodeSizeProperty, + defaultNodeSize, + relWidthProperty, + defaultRelWidth, + relColorProperty, + defaultRelColor, + nodePositions + ); + extractGraphEntitiesFromField( + segment.end, + nodes, + links, + nodeLabels, + linkTypes, + frozen, + nodeSizeProperty, + defaultNodeSize, + relWidthProperty, + defaultRelWidth, + relColorProperty, + defaultRelColor, + nodePositions + ); + }); + } + } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index a7fc6fbf..24ae9b4e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,8 +18,10 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + + "typeRoots": ["./node_modules/@types", "./types"], }, - "include": ["src"], + "include": ["src", "@types"], "references": [{ "path": "./tsconfig.node.json" }] }