From 71893f87b4bdb7018a0bccc65ce5181bf243373c Mon Sep 17 00:00:00 2001 From: Dave Cockrell Date: Sat, 27 Apr 2024 13:06:39 +0100 Subject: [PATCH] feat(rdfInstanceViewer): Add literals Switched out libraries from rdf-parse to n3, this allows better parsing and writing of rdf Added logic to add and remove literals. Store all quads in an N3 store Fixed logic to work with new store and fixed tests The entire graph is generated via the RDF --- index.html | 2 +- package.json | 2 + public/favicon.svg | 2 +- public/vite.svg | 1 - src/Components/Diagram/Diagram.tsx | 54 +- src/Components/Diagram/EdgeDialog.tsx | 2 +- src/Components/Diagram/LiteralDialog.tsx | 65 +++ src/Components/Diagram/NodeDialog.tsx | 3 +- src/Components/Terminal/Terminal.tsx | 10 +- src/Core/FakeHttpGateway.ts | 20 +- src/TestTools/DataTypePropertyResponseStub.ts | 35 ++ src/TestTools/GetRdfInputStub.ts | 68 +++ src/TestTools/ObjectPropertyResponseStub.ts | 41 ++ src/constants/index.ts | 18 +- src/helpers/index.ts | 109 +++- src/lib/CustomNode/ClassInstanceNode.tsx | 15 +- src/lib/DataTypeProperty/DataTypeProperty.tsx | 28 + .../DataTypeProperty/data-type-property.css | 5 + .../ObjectProperty.tsx} | 9 +- src/rdfInstanceViewer/RdfInstancePresenter.ts | 300 +++++++--- .../RdfInstanceRepository.ts | 99 ++-- src/rdfInstanceViewer/RdfInstanceViewer.tsx | 25 +- src/rdfInstanceViewer/Types/index.ts | 20 + .../__tests__/RdfInstance.spec.ts | 543 ++++++++---------- yarn.lock | 10 +- 25 files changed, 976 insertions(+), 510 deletions(-) delete mode 100644 public/vite.svg create mode 100644 src/Components/Diagram/LiteralDialog.tsx create mode 100644 src/TestTools/DataTypePropertyResponseStub.ts create mode 100644 src/TestTools/ObjectPropertyResponseStub.ts create mode 100644 src/lib/DataTypeProperty/DataTypeProperty.tsx create mode 100644 src/lib/DataTypeProperty/data-type-property.css rename src/lib/{CustomEdge/CustomEdge.tsx => ObjectProperty/ObjectProperty.tsx} (61%) diff --git a/index.html b/index.html index fa2ed45..ce3ddb1 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - Telicent Ontology + Telicent Instance
diff --git a/package.json b/package.json index 9f15134..9b5c646 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@telicent-oss/ds": "^0.0.3", "@telicent-oss/ontologyservice": "^0.0.6", "@telicent-oss/rdfservice": "^0.0.6", + "@types/n3": "^1.16.4", "classnames": "^2.5.1", "dagre": "^0.8.5", "fontawesome": "^5.6.3", @@ -25,6 +26,7 @@ "mobx": "^6.12.1", "mobx-react": "^9.1.0", "monaco-editor": "^0.47.0", + "n3": "^1.17.3", "rdf-parse": "^2.3.3", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/public/favicon.svg b/public/favicon.svg index 32e93c1..46fd0f5 100644 --- a/public/favicon.svg +++ b/public/favicon.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/Components/Diagram/Diagram.tsx b/src/Components/Diagram/Diagram.tsx index 55ae799..99e9cde 100644 --- a/src/Components/Diagram/Diagram.tsx +++ b/src/Components/Diagram/Diagram.tsx @@ -7,24 +7,27 @@ import { RdfPanelProps } from '../../types' import 'reactflow/dist/style.css' import ClassInstanceNode from '../../lib/CustomNode/ClassInstanceNode' import { observer } from 'mobx-react' -import CustomEdge from '../../lib/CustomEdge/CustomEdge' -import getLayoutNodes from './Layout' +import ObjectProperty from '../../lib/ObjectProperty/ObjectProperty' import { NodeDialog } from './NodeDialog' import { EdgeDialog } from './EdgeDialog' +import { LiteralDialog } from './LiteralDialog' +import DataTypeProperty from '../../lib/DataTypeProperty/DataTypeProperty' const nodeTypes = { classInstanceNode: ClassInstanceNode, + dataTypeProperty: DataTypeProperty } const edgeTypes = { - relationshipEdge: CustomEdge + relationshipEdge: ObjectProperty } const DiagramComponent: FC = observer((props: RdfPanelProps) => { - const [nodes, setNodes, onNodesChange] = useNodesState(props.presenter.viewModel.nodes) + const [nodes, setNodes, onNodesChange] = useNodesState([]) const [edges, setEdges, onEdgesChange] = useEdgesState([]) const [open, setOpen] = useState(false) const [edgeDialogOpen, setEdgeDialogOpen] = useState(false) + const [literalDialogOpen, setLiteralDialogOpen] = useState(false) useEffect(() => { if (!props.presenter) { @@ -34,11 +37,9 @@ const DiagramComponent: FC = observer((props: RdfPanelProps) => { }, []) useEffect(() => { - const { nodes, edges } = getLayoutNodes(props.presenter.viewModel.nodes, props.presenter.viewModel.edges) - - setNodes(nodes) - setEdges(edges) - }, [props.presenter.viewModel.nodes, props.presenter.viewModel.edges, setEdges, setNodes]) + setNodes(props.presenter.diagram.nodes) + setEdges(props.presenter.diagram.edges) + }, [props.presenter.diagram.nodes, props.presenter.diagram.edges, setEdges, setNodes]) const onDragOver = useCallback((event: React.DragEvent) => { event.preventDefault() @@ -72,26 +73,21 @@ const DiagramComponent: FC = observer((props: RdfPanelProps) => { const onCloseDialog = () => { setOpen(false) setEdgeDialogOpen(false) + setLiteralDialogOpen(false) } + // NOTE: deleting a node will automatically trigger + // the edges delete too, this is a reactflow feature const onNodesDelete = (nodes: Array) => { - props.presenter.deleteNodeAndAssociatedEdges(nodes[0].id) + props.presenter.deleteNode(nodes[0].id) } const onEdgesDelete = (edges: Array) => { - console.log({ edges }) - if (edges.length === 0) return - const edge = edges[0] - if (!edge.label || !edge.target || !edge.source) { - console.warn("cannot delete edge, missing information") - return - } - - props.presenter.deleteEdge(edge.source, edge.target, edge.label as string) + props.presenter.deleteEdges(edges) } const onSubmitNode = (prefix: string, name: string): void => { - props.presenter.newNodeName = `${prefix}${name}` + props.presenter.newNodeName = `${prefix}:${name}` props.presenter.lastSelectedPrefix = prefix props.presenter.addNode() @@ -104,6 +100,18 @@ const DiagramComponent: FC = observer((props: RdfPanelProps) => { setEdgeDialogOpen(false) } + const onSubmitLiteral = (edgeType: string, attributeValue: string): void => { + props.presenter.addLiteral(edgeType, attributeValue) + setLiteralDialogOpen(false) + } + + + const onNodeContextMenu = (event: React.MouseEvent, node: Node): void => { + props.presenter.selectedNode = node.data.id + event.preventDefault() + setLiteralDialogOpen(true) + } + return (<> = observer((props: RdfPanelProps) => { onConnect={onConnect} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} + onNodeContextMenu={onNodeContextMenu} nodeTypes={nodeTypes} onDragOver={onDragOver} onDrop={onDrop} @@ -126,8 +135,9 @@ const DiagramComponent: FC = observer((props: RdfPanelProps) => { } /> - {open && } - {edgeDialogOpen && } + {open && } + {edgeDialogOpen && } + {literalDialogOpen && } ) diff --git a/src/Components/Diagram/EdgeDialog.tsx b/src/Components/Diagram/EdgeDialog.tsx index 546303f..da9b68b 100644 --- a/src/Components/Diagram/EdgeDialog.tsx +++ b/src/Components/Diagram/EdgeDialog.tsx @@ -31,7 +31,7 @@ export const EdgeDialog: FC = ({ options, onClose, title, onSub
- +
Submit diff --git a/src/Components/Diagram/LiteralDialog.tsx b/src/Components/Diagram/LiteralDialog.tsx new file mode 100644 index 0000000..7c838be --- /dev/null +++ b/src/Components/Diagram/LiteralDialog.tsx @@ -0,0 +1,65 @@ +import React, { FC, useEffect, useState } from 'react' +import { TeliAutocomplete, TeliButton, TeliTextField } from "@telicent-oss/ds" +import { DialogBox } from '../../lib/DialogBox/DialogBox' + +interface LiteralDialogProps { + onClose: () => void + onSubmit: (prefix: string, name: string) => void + options: Array + title: string + lastSelected: string +} + +export const LiteralDialog: FC = ({ options, onClose, title, onSubmit, lastSelected }) => { + const [selectedEdgeType, setSelectedEdgeType] = useState(lastSelected) + const [attributeValue, setAttributeValue] = useState("") + + useEffect(() => { + const handleKeyPress = (event: KeyboardEvent) => { + if (event.key === 'Enter') { + onHandleSubmit() + } + } + document.addEventListener('keypress', handleKeyPress) + return () => { + document.removeEventListener('keypress', handleKeyPress) + } + }, []) + + const onChangeName: React.ChangeEventHandler = (event) => { + event.preventDefault() + + setAttributeValue(event.target.value) + } + + const onChangeEdgeType = (event: React.SyntheticEvent, value: string | null) => { + event.preventDefault() + if (!value) { + console.warn("Invalid value", value) + return + } + setSelectedEdgeType(value) + } + + const onHandleSubmit = () => { + if (!selectedEdgeType || !attributeValue) { + console.warn("Literal must have valid inputs") + return + } + onSubmit(selectedEdgeType, attributeValue) + } + + return ( + +
+
+ + +
+
+ Submit +
+
+
+ ) +} diff --git a/src/Components/Diagram/NodeDialog.tsx b/src/Components/Diagram/NodeDialog.tsx index 2dd942c..888ca2f 100644 --- a/src/Components/Diagram/NodeDialog.tsx +++ b/src/Components/Diagram/NodeDialog.tsx @@ -9,6 +9,7 @@ interface NodeDialogProps { title: string lastSelectedPrefix: string } + export const NodeDialog: FC = ({ options, onClose, title, onSubmit, lastSelectedPrefix }) => { const [selectedPrefix, setSelectedPrefix] = useState(lastSelectedPrefix) const [name, setName] = useState(crypto.randomUUID()) @@ -54,7 +55,7 @@ export const NodeDialog: FC = ({ options, onClose, title, onSub
- +
Submit diff --git a/src/Components/Terminal/Terminal.tsx b/src/Components/Terminal/Terminal.tsx index 1b4ec59..98c46c0 100644 --- a/src/Components/Terminal/Terminal.tsx +++ b/src/Components/Terminal/Terminal.tsx @@ -1,9 +1,7 @@ import Editor, { Monaco } from "@monaco-editor/react"; -// import type monaco from "monaco-editor"; import rdfParser from "rdf-parse"; import { Readable } from "readable-stream"; import { useRef } from "react"; -// import { MarkerSeverity } from "monaco-editor"; import { RdfInstancePresenter } from "../../rdfInstanceViewer/RdfInstancePresenter"; import { withInjection } from "../../Core/Providers/injection"; import { RdfPanelProps } from "../../types"; @@ -14,14 +12,10 @@ import { themeRules } from "./themeRules"; const TerminalComponent = observer((props: RdfPanelProps) => { const { handleRdfInput, viewModel } = props.presenter; - // console.log({ viewModel }) const monacoRef = useRef(null); - // const monaco = useMonaco() - // const markers: monaco.editor.IMarkerData[] = []; - // console.log({ viewModel }) if (monacoRef?.current) { // @ts-expect-error Property from does not exist on type Readable - const input = Readable.from([viewModel.rdf]); + const input = Readable.from([props.presenter.rdfInstanceRepository.rdf]); // console.log({ rdf: viewModel.rdf }) rdfParser .parse(input, { contentType: "text/turtle" }) @@ -107,7 +101,7 @@ const TerminalComponent = observer((props: RdfPanelProps) => { height="93.5vh" language="turtle" onChange={handleRdfInput} - value={viewModel.rdf ?? ""} + value={props.presenter.rdfInstanceRepository.rdf ?? ""} options={{ selectOnLineNumbers: true, automaticLayout: true, diff --git a/src/Core/FakeHttpGateway.ts b/src/Core/FakeHttpGateway.ts index e755747..3a10ef8 100644 --- a/src/Core/FakeHttpGateway.ts +++ b/src/Core/FakeHttpGateway.ts @@ -2,22 +2,28 @@ import { z } from "zod"; import { injectable } from "inversify"; import OntologyService from "@telicent-oss/ontologyservice" import { ResponseSchema } from "../rdfInstanceViewer/Types"; +import { ObjectPropertiesResponseStub } from "../TestTools/ObjectPropertyResponseStub"; +import { DataTypePropertyResponseStub } from "../TestTools/DataTypePropertyResponseStub"; @injectable() export class FakeHttpGateway { private ontologyService = new OntologyService("fake-address", "fake-topic") get = async >( - _: string, + query: string, validationCallback: (data: unknown) => T ): Promise => { - await Promise.resolve(); + // console.log(query) // Create a stubbed response matching the structure expected by T - const stubbedResponse: unknown = { - results: { - bindings: [] - } - }; + let stubbedResponse: unknown + + if (query.includes("object_property")) { + stubbedResponse = ObjectPropertiesResponseStub + } + if (query.includes("data_type_property")) { + stubbedResponse = DataTypePropertyResponseStub + } + // await Promise.resolve(); // Validate the stubbed response with the provided callback const validatedResponse = validationCallback(stubbedResponse); diff --git a/src/TestTools/DataTypePropertyResponseStub.ts b/src/TestTools/DataTypePropertyResponseStub.ts new file mode 100644 index 0000000..9fb34d4 --- /dev/null +++ b/src/TestTools/DataTypePropertyResponseStub.ts @@ -0,0 +1,35 @@ +export const DataTypePropertyResponseStub = { + "head": { + "vars": [ + "data_type_property" + ] + }, + "results": { + "bindings": [ + { + "data_type_property": { + "type": "uri", + "value": "http://ies.data.gov.uk/ontology/ies4#idEmergencyContactTelNo" + } + }, + { + "data_type_property": { + "type": "uri", + "value": "http://ies.data.gov.uk/ontology/ies4#endsIn" + } + }, + { + "data_type_property": { + "type": "uri", + "value": "http://ies.data.gov.uk/ontology/ies4#issuerIdentificationNumber" + } + }, + { + "data_type_property": { + "type": "uri", + "value": "http://ies.data.gov.uk/ontology/ies4#hmlConfidence" + } + }, + ] + } +} diff --git a/src/TestTools/GetRdfInputStub.ts b/src/TestTools/GetRdfInputStub.ts index 9228e52..b03e204 100644 --- a/src/TestTools/GetRdfInputStub.ts +++ b/src/TestTools/GetRdfInputStub.ts @@ -36,3 +36,71 @@ data:4c48ac99-61fd-4fa5-81e2-aab8e7648618 a ies:Event . data:0b791546-4f5c-4d58-9b62-7b7608af6468 ies:EventParticipant data:4c48ac99-61fd-4fa5-81e2-aab8e7648618 . ` +export const GetRdfAandBClassesWithEdge = `@prefix : . +@prefix xsd: . +@prefix dc: . +@prefix rdf: . +@prefix rdfs: . +@prefix owl: . +@prefix telicent: . +@prefix data: . +@prefix ies: . + +data:nodeA rdf:type ies:nodeAType; + ies:edgeType data:nodeB. +data:nodeB rdf:type ies:nodeBType. +` + +export const GetRdfAClassOnly = `@prefix : . +@prefix xsd: . +@prefix dc: . +@prefix rdf: . +@prefix rdfs: . +@prefix owl: . +@prefix telicent: . +@prefix data: . +@prefix ies: . + +data:nodeA rdf:type ies:nodeAType. +` + +export const GetRdfAandBClassesOnly = `@prefix : . +@prefix xsd: . +@prefix dc: . +@prefix rdf: . +@prefix rdfs: . +@prefix owl: . +@prefix telicent: . +@prefix data: . +@prefix ies: . + +data:nodeA rdf:type ies:nodeAType. +data:nodeB rdf:type ies:nodeBType. +` + +export const GetPrefixRdfStub = `@prefix : . +@prefix xsd: . +@prefix dc: . +@prefix rdf: . +@prefix rdfs: . +@prefix owl: . +@prefix telicent: . +@prefix data: . +@prefix ies: . +` + +export const GetRdfAandBClassesWithEdgeAndLiteral = `@prefix : . +@prefix xsd: . +@prefix dc: . +@prefix rdf: . +@prefix rdfs: . +@prefix owl: . +@prefix telicent: . +@prefix data: . +@prefix ies: . + +data:nodeA rdf:type ies:nodeAType; + ies:edgeType data:nodeB. +data:nodeB rdf:type ies:nodeBType; + ies:representativeValue "Anderson". +` diff --git a/src/TestTools/ObjectPropertyResponseStub.ts b/src/TestTools/ObjectPropertyResponseStub.ts new file mode 100644 index 0000000..22433f3 --- /dev/null +++ b/src/TestTools/ObjectPropertyResponseStub.ts @@ -0,0 +1,41 @@ +export const ObjectPropertiesResponseStub = { + "head": { + "vars": [ + "object_property" + ] + }, + "results": { + "bindings": [ + { + "object_property": { + "type": "uri", + "value": "http://ies.data.gov.uk/ontology/ies4#isPartOf" + } + }, + { + "object_property": { + "type": "uri", + "value": "http://ies.data.gov.uk/ontology/ies4#enemyOf" + } + }, + { + "object_property": { + "type": "uri", + "value": "http://ies.data.gov.uk/ontology/ies4#excludedFrom" + } + }, + { + "object_property": { + "type": "uri", + "value": "http://ies.data.gov.uk/ontology/ies4#allHaveDisposition" + } + }, + { + "object_property": { + "type": "uri", + "value": "http://ies.data.gov.uk/ontology/ies4#inPeriod" + } + } + ] + } +} diff --git a/src/constants/index.ts b/src/constants/index.ts index 79048d9..f2c17d3 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,11 +1,3 @@ -export const INITIAL_RDF_PREFIXES: Record = { - data: "http://data.gov/data#", - ies: "http://ies.data.gov.uk/ontology/ies4#", - rdfs: "http://www.w3.org/2000/01/rdf-schema#", - telicent: "http://telicent.io/ontology/", - xsd: "http://www.w3.org/2001/XMLSchema#", -}; - export const HIERARCHY_QUERY = `SELECT ?sub ?super ?subType ?subComment ?subLabel WHERE { @@ -16,3 +8,13 @@ export const HIERARCHY_QUERY = `SELECT ?sub ?super ?subType ?subComment ?subLabe }`; export const rootHierarchyUri = "http://ies.data.gov.uk/ontology/ies4#ExchangedItem" + +export const OBJECT_PROPERTY_QUERY = `SELECT ?object_property +WHERE { + ?object_property a owl:ObjectProperty . +}` + +export const DATATYPE_PROPERTY_QUERY = `SELECT ?data_type_property +WHERE { + ?data_type_property a owl:DatatypeProperty . +}` diff --git a/src/helpers/index.ts b/src/helpers/index.ts index ac5f386..857dc76 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -6,6 +6,9 @@ import { z } from "zod"; import { Quad } from "@rdfjs/types"; import { Node, Edge, MarkerType } from "reactflow"; +import { Parser, Prefixes } from "n3"; +import { Readable } from "readable-stream"; + // TODO: Needs to be exposed in @telicent-oss/ontologyservice export const getAndCheckValidation = (data: unknown, schema: z.ZodType): T => { try { @@ -38,27 +41,43 @@ export const pascalToKebab = (pascalString: string): string => { return pascalString.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); } +export const formatDataTypeProperty = (dataTypePropertyType: Quad): Node => { + return { + type: 'dataTypeProperty', + position: { x: 0, y: 0 }, + id: `dataTypeProperty-${dataTypePropertyType.subject.value}`, + data: { + id: dataTypePropertyType.subject.value, + name: dataTypePropertyType.object.value, + edgeLabel: dataTypePropertyType.predicate.value, + shortName: getCapitalLetters(getWordAfterLastHash(dataTypePropertyType.object.value)), + className: pascalToKebab(getWordAfterLastHash(dataTypePropertyType.object.value)) + } + } +} + export const formatNode = (node: Quad): Node => { return { type: 'classInstanceNode', - id: node.subject.value, position: { x: 0, y: 0 }, + id: `classInstanceNode-${node.subject.value}`, data: { name: node.object.value, shortName: getCapitalLetters(getWordAfterLastHash(node.object.value)), - className: pascalToKebab(getWordAfterLastHash(node.object.value)) + className: pascalToKebab(getWordAfterLastHash(node.object.value)), + id: node.subject.value, } } } -export const formatEdge = (edge: Quad, label: string): Edge => { +export const formatEdge = (edge: Quad): Edge => { const newEdge = ({ - id: `${edge.object.value}-${edge.subject.value}`, - source: edge.object.value, - target: edge.subject.value, + id: `${edge.subject.value}--${edge.object.value}--${crypto.randomUUID()}`, + source: edge.subject.value, + target: edge.object.value, type: "relationshipEdge", - label, + label: edge.predicate.value, markerEnd: { type: MarkerType.ArrowClosed } @@ -82,3 +101,79 @@ export const addSpacesToPascalCase = (inputString: string): string => { return result; }; + +export const stripOutPrefixesAndEmptyLinesFromRdf = (rdfInput: string) => rdfInput.split("\n").filter(line => !line.includes("@prefix") || !Boolean(line)).join("\n") + +const isUsingAllNamedNodes = (q: Quad) => q.subject.termType === "NamedNode" && q.predicate.termType === "NamedNode" && q.object.termType === "NamedNode" +const isRdfType = (q: Quad) => q.predicate.value === "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" + +export const isObjectProperty = (q: Quad) => isUsingAllNamedNodes(q) && !isRdfType(q) +export const isDataTypeProperty = (q: Quad) => !isUsingAllNamedNodes(q) +export const isNode = (q: Quad) => isUsingAllNamedNodes(q) && isRdfType(q) + +export const connectNodesWithReactFlowId = (nodes: Array, objectProperties: Array, dataTypeProperties: Array) => { + const { edges } = objectProperties.reduce((accumulator, objectProperty) => { + // find normal nodes and get connection id's + const sourceNode = nodes.find(n => n.data.id === objectProperty.source); + const targetNode = nodes.find(n => n.data.id === objectProperty.target); + + if (!sourceNode || !targetNode) { + accumulator.edges.push(objectProperty); + return accumulator; + } + + const updatedObjectProperty = { + ...objectProperty, + source: sourceNode.id, + target: targetNode.id + }; + + accumulator.edges.push(updatedObjectProperty); + + // Additional logic here, for example, adding to dataTypePropertyNodes array + // once node is created create edge between the two nodes + const matchingDTP = dataTypeProperties.find(dtp => dtp.data.id === objectProperty.target) + if (!matchingDTP) return accumulator + + matchingDTP.id = crypto.randomUUID() + const dtpEgde: Edge = { + // TODO: make edge label generator so that we have a consistent naming schema + id: `${updatedObjectProperty.source}--${matchingDTP.id}`, + label: matchingDTP.data.edgeLabel, + markerEnd: { + type: MarkerType.ArrowClosed + }, + source: updatedObjectProperty.target, + target: matchingDTP.id, + type: "relationshipEdge" + } + + accumulator.edges.push(dtpEgde) + + return accumulator; + }, { + edges: new Array() + }); + + return edges +} + +export const findAllNodesById = (rdfInput: string, id: string) => new Promise((resolve) => { + // @ts-expect-error From does not exist on type Readable when it actually does + const input = Readable.from([rdfInput]) + const parser = new Parser({ format: 'Turtle' }) + const quadsToRemove: Array = [] + parser.parse(input, (_, quad) => { + if (quad && isRdfType(quad) && (quad.subject.value === id || quad.object.value === id)) { + quadsToRemove.push(quad) + } else { + resolve(quadsToRemove) + } + }) +}) + +export const rebuildLongUri = (prefixes: Prefixes, value: string) => { + const longUri = prefixes[value.replace(/:.*/, "")].value + const afterColon = value.replace(/.*:/, "") + return `${longUri}${afterColon}` +} diff --git a/src/lib/CustomNode/ClassInstanceNode.tsx b/src/lib/CustomNode/ClassInstanceNode.tsx index d3652ac..8c754ad 100644 --- a/src/lib/CustomNode/ClassInstanceNode.tsx +++ b/src/lib/CustomNode/ClassInstanceNode.tsx @@ -2,15 +2,16 @@ import "./custom-node.css" import { FC } from "react" import { Handle, NodeProps, Position } from "reactflow" -interface CustomNode extends Node { - data: { - className: string - shortName: string - name: string - } +interface ClassInstanceData { + className: string + shortName: string + name: string + id: string } -const ClassInstanceNode: FC> = (node) => { +type ClassInstance = NodeProps + +const ClassInstanceNode: FC = (node) => { // console.log({ node }) // TODO: work out how to update prefixes. If user removes // a prefix in the terminal it should be removed from diff --git a/src/lib/DataTypeProperty/DataTypeProperty.tsx b/src/lib/DataTypeProperty/DataTypeProperty.tsx new file mode 100644 index 0000000..9ab1fd4 --- /dev/null +++ b/src/lib/DataTypeProperty/DataTypeProperty.tsx @@ -0,0 +1,28 @@ +import "./data-type-property.css" +import { FC } from "react" +import { Handle, NodeProps, Position } from "reactflow" + +type DTPData = { + className: string + shortName: string + name: string + id: string +} + +type DataTypeProperty = NodeProps + +const ClassInstanceLiteral: FC = (node) => { + // TODO: work out how to update prefixes. If user removes + // a prefix in the terminal it should be removed from + // from the OntologyService + return ( + <> + +
+

"{node.data.name}"

+
+ + ) +} + +export default ClassInstanceLiteral diff --git a/src/lib/DataTypeProperty/data-type-property.css b/src/lib/DataTypeProperty/data-type-property.css new file mode 100644 index 0000000..61addd4 --- /dev/null +++ b/src/lib/DataTypeProperty/data-type-property.css @@ -0,0 +1,5 @@ +.data-type-property { + height: 50px; + width: 50px; + background: transparent; +} diff --git a/src/lib/CustomEdge/CustomEdge.tsx b/src/lib/ObjectProperty/ObjectProperty.tsx similarity index 61% rename from src/lib/CustomEdge/CustomEdge.tsx rename to src/lib/ObjectProperty/ObjectProperty.tsx index 5faccb9..f4f310d 100644 --- a/src/lib/CustomEdge/CustomEdge.tsx +++ b/src/lib/ObjectProperty/ObjectProperty.tsx @@ -2,17 +2,18 @@ import { FC } from "react" import { BaseEdge, EdgeLabelRenderer, EdgeProps, getBezierPath } from "reactflow" -const CustomEdge: FC = ({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, markerEnd, label }) => { +const ObjectProperty: FC = ({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, markerEnd, markerStart, label }) => { const [edgePath, labelX, labelY] = getBezierPath({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition }) return ( <> - +
= ({ id, sourceX, sourceY, targetX, targetY, sou ) } -export default CustomEdge +export default ObjectProperty diff --git a/src/rdfInstanceViewer/RdfInstancePresenter.ts b/src/rdfInstanceViewer/RdfInstancePresenter.ts index 665ad01..d9abfb5 100644 --- a/src/rdfInstanceViewer/RdfInstancePresenter.ts +++ b/src/rdfInstanceViewer/RdfInstancePresenter.ts @@ -1,10 +1,14 @@ -import { inject, injectable } from "inversify"; +import { inject, injectable, targetName } from "inversify"; import { Quad } from '@rdfjs/types' -import rdfParser from 'rdf-parse' +import { Parser, Prefixes, Writer, DataFactory, Store } from "n3"; import { action, computed, makeObservable, observable, runInAction } from "mobx"; import { Readable } from 'readable-stream' import { RdfInstanceRepository } from "./RdfInstanceRepository" -import { formatEdge, formatNode } from "../helpers"; +import { formatDataTypeProperty, formatEdge, formatNode, isNode, isDataTypeProperty, isObjectProperty, connectNodesWithReactFlowId, rebuildLongUri } from "../helpers"; +import getLayoutNodes from "../Components/Diagram/Layout"; +import { Edge } from "reactflow"; + +const { namedNode, literal, quad: quadFn } = DataFactory // MarkerSeverity: Copied from monaco-editor // due to import error while testing @@ -50,24 +54,25 @@ export class RdfInstancePresenter { newEdgeSource: string | null = null newEdgeTarget: string | null = null - lastSelectedPrefix: string | null = null + selectedNode: string | null = null + lastSelectedPrefix: string | null = null + lastSelectedLiteral: string | null = null constructor() { makeObservable(this, { handleRdfInput: action, viewModel: computed, + diagram: computed, addNode: action, newNodeType: observable, newNodeName: observable, - newEdgeSource: observable, - newEdgeTarget: observable, - newEdgeType: observable, resetNewNode: action, resetNewEdge: action, - deleteNodeAndAssociatedEdges: action, - deleteEdge: action, + deleteNode: action, + deleteEdges: action, formatRdfText: action, - reset: action + addChangedPrefixes: action, + reset: action, }) this.reset() } @@ -75,35 +80,65 @@ export class RdfInstancePresenter { reset() { this.resetNewEdge() this.resetNewNode() - this.lastSelectedPrefix = "data:" + this.lastSelectedPrefix = "data" + this.lastSelectedLiteral = "ies:representationValue" + this.selectedNode = null + } + + get diagram() { + // get nodes and edges (already formatted) then run them through the dagre layout then add them to the respective nodes a fields + const n = this.rdfInstanceRepository.nodes.map(formatNode) + const objectProperties = this.rdfInstanceRepository.objectProperties.map(formatEdge) + const dataTypeProperties = this.rdfInstanceRepository.dataTypeProperties.map(formatDataTypeProperty) + + const edgesWithNoLayout = connectNodesWithReactFlowId(n, objectProperties, dataTypeProperties).map(edge => ({ + ...edge, + label: this.rdfInstanceRepository.getUserFriendlyURI(edge.label!.toString()) + })) + + const { nodes, edges } = getLayoutNodes([...n, ...dataTypeProperties], edgesWithNoLayout) + + return { + nodes, + edges + } } get viewModel() { + console.log("computed") return { - rdf: this.rdfInstanceRepository.rdf, markers: this.rdfInstanceRepository.markers.map((marker) => { return marker }), - nodes: this.rdfInstanceRepository.nodes.map(formatNode), - edges: this.rdfInstanceRepository.edges.map((e) => { - const label = this.rdfInstanceRepository.getUserFriendlyURI(e.predicate.value) - return formatEdge(e, label) - }), relationships: this.rdfInstanceRepository.relationships.map((relationship => this.rdfInstanceRepository.getUserFriendlyURI(relationship))), - iesObjects: this.rdfInstanceRepository.iesObjects.map((iesObject) => iesObject), prefixes: this.rdfInstanceRepository.prefixes, - lastSelectedPrefix: this.lastSelectedPrefix ?? this.rdfInstanceRepository.prefixes[0] + dataTypes: this.rdfInstanceRepository.literals, + lastSelectedPrefix: this.lastSelectedPrefix ?? this.rdfInstanceRepository.prefixes[0], + lastSelectedLiteral: this.lastSelectedLiteral ?? this.rdfInstanceRepository.literals[0] } } addNode = () => { - // TODO: need id and type - if (!this.newNodeType || !this.newNodeName) return - this.rdfInstanceRepository.rdf = `${this.rdfInstanceRepository.rdf}\n${this.newNodeName} a ${this.rdfInstanceRepository.getUserFriendlyURI(this.newNodeType)} .` - this.handleRdfInput(this.rdfInstanceRepository.rdf) + if (!this.newNodeType || !this.newNodeName) { + console.warn("cancelling creation of node, missing name or type") + return + } + const rdf = `${this.rdfInstanceRepository.rdf}\n${this.newNodeName} a ${this.rdfInstanceRepository.getUserFriendlyURI(this.newNodeType)} .` + this.handleRdfInput(rdf) this.resetNewNode() } + addLiteral = (edgeType: string, attributeValue: string) => { + if (this.selectedNode === null) { + console.warn("Cannot add a literal without a source node") + return + } + // createLiteral quad + const rdf = `${this.rdfInstanceRepository.rdf}\n${this.rdfInstanceRepository.getUserFriendlyURI(this.selectedNode)} ${edgeType} "${attributeValue}" .` + this.handleRdfInput(rdf) + this.selectedNode = null + } + resetNewNode = () => { this.newNodeName = null this.newNodeType = null @@ -117,104 +152,191 @@ export class RdfInstancePresenter { addEdge = () => { if (!this.newEdgeType || !this.newEdgeSource || !this.newEdgeTarget) return - this.rdfInstanceRepository.rdf = `${this.rdfInstanceRepository.rdf}\n${this.rdfInstanceRepository.getUserFriendlyURI(this.newEdgeTarget)} ${this.newEdgeType} ${this.rdfInstanceRepository.getUserFriendlyURI(this.newEdgeSource)} .` - this.handleRdfInput(this.rdfInstanceRepository.rdf) + // Because nodes have random id's we have to find the data id to generate valid rdf + const sourceNode = this.diagram.nodes.find(n => n.id === this.newEdgeSource) + const targetNode = this.diagram.nodes.find(n => n.id === this.newEdgeTarget) + if (!sourceNode || !targetNode) { + console.warn("Unable to find connecting nodes") + return + } + const rdf = `${this.rdfInstanceRepository.convertPrefixesToRdf()}${this.rdfInstanceRepository.rdf}\n${this.rdfInstanceRepository.getUserFriendlyURI(sourceNode.data.id)} ${this.newEdgeType} ${this.rdfInstanceRepository.getUserFriendlyURI(targetNode.data.id)} .` + this.handleRdfInput(rdf) this.resetNewEdge() } - deleteNodeAndAssociatedEdges = (nodeId: string) => { - if (!this.rdfInstanceRepository.rdf) return - - const readableId = this.rdfInstanceRepository.getUserFriendlyURI(nodeId) - this.rdfInstanceRepository.rdf = this.rdfInstanceRepository.rdf.split("\n").filter((line) => !line.includes(readableId)).join("\n") - this.handleRdfInput(this.rdfInstanceRepository.rdf) - } + deleteNode = async (nodeId: string) => { - deleteEdge = (source: string, target: string, label: string) => { if (!this.rdfInstanceRepository.rdf) return - const rdf = `${this.rdfInstanceRepository.getUserFriendlyURI(target)} ${label} ${this.rdfInstanceRepository.getUserFriendlyURI(source)} .\n` - this.rdfInstanceRepository.rdf = this.rdfInstanceRepository.rdf.split("\n").filter((line) => - !Boolean(line.trim() === rdf.trim())).join("\n") - this.handleRdfInput(this.rdfInstanceRepository.rdf) - } - - onDataPartial = (nodeArr: Array, edgeArr: Array, objectArr: Array, quadArray: Array) => (triple: Quad) => { - quadArray.push(triple) - // Set objects - if (triple.object.termType === "Literal") { - objectArr.push(triple); - return; + const node = this.diagram.nodes.find(n => n.id === nodeId) + if (!node) { + console.warn("cannot find node to delete") + return } - - // Set Edges - if (this.rdfInstanceRepository.relationships.includes(triple.predicate.value)) { - edgeArr.push(triple); + const nodesToRemove = this.rdfInstanceRepository.nodes.filter(n => n.subject.value === node.data.id) + const dataTypesToRemove = this.rdfInstanceRepository.dataTypeProperties.filter(n => n.subject.value === node.data.id) + const quadsToRemove = nodesToRemove.concat(dataTypesToRemove) + if (quadsToRemove.length === 0) { + console.warn("cannot find node to delete") return } - // Set Nodes - nodeArr.push(triple); - } + // @ts-expect-error From does not exist on type Readable when it actually does + const input = Readable.from([this.rdfInstanceRepository.rdf]) + + const parser = new Parser({ format: 'Turtle' }) - formatRdfText = (quads: Array) => { - quads.forEach(quad => { - if (quad.predicate.value === "http://www.w3.org/1999/02/22-rdf-syntax-ns#type") { - this.rdfInstanceRepository.rdf += `${this.rdfInstanceRepository.getUserFriendlyURI(quad.subject.value)} a ${this.rdfInstanceRepository.getUserFriendlyURI(quad.object.value)} .\n` + parser.parse(input, (error, quad, prefixes) => { + if (error) { + this.onError(error as QuadError) return } - this.rdfInstanceRepository.rdf += `${this.rdfInstanceRepository.getUserFriendlyURI(quad.subject.value)} ${this.rdfInstanceRepository.getUserFriendlyURI(quad.predicate.value)} ${this.rdfInstanceRepository.getUserFriendlyURI(quad.object.value)} .\n` - }); + if (quad) { + quadsToRemove.forEach(quadToRemove => { + if (quadToRemove.equals(quad)) { + this.rdfInstanceRepository.store.removeQuad(quad.subject, quad.predicate, quad.object) + } + }) + } else { + runInAction(() => { + this.rdfInstanceRepository.nodes = this.rdfInstanceRepository.store.getQuads(null, null, null, null).filter(isNode) + this.rdfInstanceRepository.objectProperties = this.rdfInstanceRepository.store.getQuads(null, null, null, null).filter(isObjectProperty) + this.rdfInstanceRepository.dataTypeProperties = this.rdfInstanceRepository.store.getQuads(null, null, null, null).filter(isDataTypeProperty) + if (prefixes) { + this.addChangedPrefixes(prefixes) + } + this.formatRdfText(this.rdfInstanceRepository.store.getQuads(null, null, null, null), this.rdfInstanceRepository.prefixes) + }) + + } + }) } - onEndPartial = (nodes: Array, edges: Array, objects: Array, quads: Array) => () => { - runInAction(() => { - this.rdfInstanceRepository.nodes = [...nodes] - this.rdfInstanceRepository.edges = [...edges] - this.rdfInstanceRepository.iesObjects = [...objects] - this.rdfInstanceRepository.loadPrefixes() - this.formatRdfText(quads) + deleteEdges = (edges: Array) => { + if (!this.rdfInstanceRepository.rdf) { + // Should never hit here as prefixes are loaded + // to rdf on mount + console.warn("No rdf, cancelling delete") + return + } + // @ts-expect-error From does not exist on type Readable when it actually does + const input = Readable.from([this.rdfInstanceRepository.rdf]) + + const parser = new Parser({ format: 'Turtle' }) + + parser.parse(input, (error, quad, prefixes) => { + if (error) { + this.onError(error as QuadError) + return + } + if (quad) { + edges.forEach((edge => { + const [source, target] = edge.id.split("--") + const expandedUri = rebuildLongUri(this.rdfInstanceRepository.prefixes, edge.label!.toString()) + const quadToRemove = quadFn( + namedNode(source), + namedNode(expandedUri), + namedNode(target) + ) + if (quadToRemove.equals(quad)) { + this.rdfInstanceRepository.store.removeQuad(quad) + } + })) + } else { + runInAction(() => { + this.rdfInstanceRepository.nodes = this.rdfInstanceRepository.store.getQuads(null, null, null, null).filter(isNode) + this.rdfInstanceRepository.objectProperties = this.rdfInstanceRepository.store.getQuads(null, null, null, null).filter(isObjectProperty) + this.rdfInstanceRepository.dataTypeProperties = this.rdfInstanceRepository.store.getQuads(null, null, null, null).filter(isDataTypeProperty) + if (prefixes) { + this.addChangedPrefixes(prefixes) + } + this.formatRdfText(this.rdfInstanceRepository.store.getQuads(null, null, null, null), this.rdfInstanceRepository.prefixes) + }) + + } }) + + + this.formatRdfText(this.rdfInstanceRepository.store.getQuads(null, null, null, null), this.rdfInstanceRepository.prefixes) } onError = (err: QuadError) => { runInAction(() => { - this.rdfInstanceRepository.markers.push({ + this.rdfInstanceRepository.markers = [{ startColumn: err?.context.previousToken?.start ?? 0, endColumn: err?.context.previousToken?.end ?? 0, startLineNumber: err?.context.line, endLineNumber: err?.context.line, message: err.message, severity: MarkerSeverity.Error - }) + }] }) } - handleRdfInput = (rdfInput?: string) => { - if (!rdfInput) return + addChangedPrefixes = (prefixes: Prefixes) => { + for (let prefix in prefixes) { + // add if doesn't exist + const prefixWithColon = `${prefix}:` + + if (this.rdfInstanceRepository.prefixes[prefixWithColon] === undefined) { + this.rdfInstanceRepository.addPrefix(prefix, prefixes[prefix].toString()) + } + + // overwrite if prefix exists but has a differert value + else if (this.rdfInstanceRepository.prefixes && this.rdfInstanceRepository.prefixes[prefixWithColon].value !== prefixes[prefix].value) { + this.rdfInstanceRepository.addPrefix(prefix, prefixes[prefix].toString()) + } + } + this.rdfInstanceRepository.loadPrefixes() + } + + + handleRdfInput = (rdfInput: string | undefined) => { + this.rdfInstanceRepository.store = new Store() + if (!rdfInput) return null // @ts-expect-error From does not exist on type Readable when it actually does const input = Readable.from([rdfInput]) - const quads: Array = [] - const nodeQuads: Array = [] - const edgeQuads: Array = [] - const iesObjectQuads: Array = [] - - const onData = this.onDataPartial(nodeQuads, edgeQuads, iesObjectQuads, quads) - const onEnd = this.onEndPartial(nodeQuads, edgeQuads, iesObjectQuads, quads) - - rdfParser.parse(input, { contentType: "text/turtle" }).on("prefix", (prefix, namespace) => { - // TODO: check existing prefixes - // if doesn't exist add it - done - // have a feeling that this will cause problems when - // trying to remove prefixes - - // Add prefix - if (!Object.keys(this.viewModel.prefixes).includes(`${prefix}:`) || this.viewModel.prefixes[`${prefix}:`] !== namespace) { - this.rdfInstanceRepository.addPrefix(prefix, namespace.value) + + const parser = new Parser({ format: 'Turtle' }) + + parser.parse(input, (error, quad, prefixes) => { + if (error) { + this.onError(error as QuadError) + return } - }).on("data", onData) - .on("end", onEnd) - .on("error", this.onError) + if (quad) { + this.rdfInstanceRepository.store.addQuad(quad.subject, quad.predicate, quad.object) + } else { + runInAction(() => { + this.rdfInstanceRepository.nodes = this.rdfInstanceRepository.store.getQuads(null, null, null, null).filter(isNode) + this.rdfInstanceRepository.objectProperties = this.rdfInstanceRepository.store.getQuads(null, null, null, null).filter(isObjectProperty) + this.rdfInstanceRepository.dataTypeProperties = this.rdfInstanceRepository.store.getQuads(null, null, null, null).filter(isDataTypeProperty) + if (prefixes) { + this.addChangedPrefixes(prefixes) + } + }) + this.formatRdfText(this.rdfInstanceRepository.store.getQuads(null, null, null, null), this.rdfInstanceRepository.prefixes) + } + }) + } + + formatRdfText = (quads: Array, prefixes: Prefixes) => { + const writer = new Writer({ prefixes }); + quads.forEach((quad) => { + if (isDataTypeProperty(quad)) { + writer.addQuad( + namedNode(this.rdfInstanceRepository.getUserFriendlyURI(quad.subject.value)), namedNode(quad.predicate.value), literal(this.rdfInstanceRepository.getUserFriendlyURI(quad.object.value)) + ) + } else { + writer.addQuad( + namedNode(this.rdfInstanceRepository.getUserFriendlyURI(quad.subject.value)), namedNode(this.rdfInstanceRepository.getUserFriendlyURI(quad.predicate.value)), namedNode(this.rdfInstanceRepository.getUserFriendlyURI(quad.object.value)) + ) + } + }) + writer.end((_, result) => { + console.log({ result }) + this.rdfInstanceRepository.rdf = result + }); } } diff --git a/src/rdfInstanceViewer/RdfInstanceRepository.ts b/src/rdfInstanceViewer/RdfInstanceRepository.ts index a3a08a4..cfaa746 100644 --- a/src/rdfInstanceViewer/RdfInstanceRepository.ts +++ b/src/rdfInstanceViewer/RdfInstanceRepository.ts @@ -3,13 +3,25 @@ * Description: Where all of the rdf and error marker data will be stored */ +import { z } from "zod"; import { injectable, inject } from 'inversify' import type monaco from 'monaco-editor' -import { makeObservable, observable, action, runInAction } from 'mobx' +import { makeObservable, observable, action, runInAction, computed } from 'mobx' import { Types } from '../Core/Types' import { HttpGateway } from '../Core/HttpGateway' import { Quad } from '@rdfjs/types' +import { Prefixes, DataFactory, Store } from 'n3' +import { DATATYPE_PROPERTY_QUERY, OBJECT_PROPERTY_QUERY } from '../constants' +import { DataTypeResponseSchema, ObjectPropertyResponseSchema } from "./Types"; +import { getAndCheckValidation, isDataTypeProperty, isNode, isObjectProperty } from "../helpers"; +export const getAndCheckDataTypeResponse = (data: unknown): z.infer => + getAndCheckValidation>(data, DataTypeResponseSchema) + +export const getAndCheckObjectPropertyResponse = (data: unknown): z.infer => + getAndCheckValidation>(data, ObjectPropertyResponseSchema) + +const { namedNode } = DataFactory @injectable() export class RdfInstanceRepository { @@ -17,13 +29,15 @@ export class RdfInstanceRepository { //TODO: split out hierarchy logic into its own Repository? rdf: string | null = null - prefixes: Record = {} + store = new Store() + prefixes: Prefixes = {} + literals: Array = [] relationships: Array = [] selectedRelationship: string | null = null markers: Array = [] nodes: Array = [] - edges: Array = [] - iesObjects: Array = [] + objectProperties: Array = [] + dataTypeProperties: Array = [] constructor(@inject(Types.IDataGateway) dataGateway: HttpGateway) { @@ -32,41 +46,34 @@ export class RdfInstanceRepository { rdf: observable, selectedRelationship: observable, relationships: observable, + literals: observable, markers: observable, - nodes: observable, + nodes: observable.deep, + store: observable.deep, + objectProperties: observable.deep, + dataTypeProperties: observable.deep, addPrefix: action, + loadDataTypes: action, + loadPrefixes: action, + loadObjectProperties: action, reset: action }) this.reset() } reset = () => { - this.rdf = "" - this.relationships = [ - "http://ies.data.gov.uk/ontology/ies4#aCopyOf", - "http://ies.data.gov.uk/ontology/ies4#relationship", - "http://ies.data.gov.uk/ontology/ies4#isParticipantIn", - "http://ies.data.gov.uk/ontology/ies4#isParticipationOf", - "http://ies.data.gov.uk/ontology/ies4#EventParticipant", - "http://ies.data.gov.uk/ontology/ies4#isStateOf", - "http://ies.data.gov.uk/ontology/ies4#isPartOf", - "http://ies.data.gov.uk/ontology/ies4#PeriodOfTime", - "http://ies.data.gov.uk/ontology/ies4#ClassOfElement", - "http://ies.data.gov.uk/ontology/ies4#Element", - "http://ies.data.gov.uk/ontology/ies4#State", - "http://ies.data.gov.uk/ontology/ies4#attribute", - "http://ies.data.gov.uk/ontology/ies4#Event", - "http://ies.data.gov.uk/ontology/ies4#ExchangedItem", - "http://ies.data.gov.uk/ontology/ies4#Entity" - ] + this.addPrefix("data", "http://example.com/rdf/testdata#") + this.addPrefix("ies", "http://ies.data.gov.uk/ontology/ies4#") + this.prefixes = this.loadPrefixes() + this.loadObjectProperties() + this.loadDataTypes() this.markers = [] - this.nodes = [] - // TODO: set this up properly later this.selectedRelationship = this.relationships[0] - this.addPrefix("data", "http://example.com/rdf/testdata#") - this.loadPrefixes() + this.rdf = this.convertPrefixesToRdf() } + convertPrefixesToRdf = () => Object.entries(this.prefixes).map(([key, namespace]) => `@prefix ${key}: <${namespace.value}>.\n`).join("") + removeEdgeFromRdf = (input: Array, source: string, target: string) => { this.rdf = input.filter((i) => !(i.includes(source) && i.includes(target))).join() } @@ -81,19 +88,39 @@ export class RdfInstanceRepository { } loadPrefixes() { - const prefixes = this.dataGateway.getPrefixes() - let rdfPrefix = ""; + const prefixes = Object.entries(this.dataGateway.getPrefixes()).reduce((prefixes, prefix) => { + const [key, value] = prefix + // strip colon because n3 Prefix already understands this + prefixes[key.replace(":", "")] = namedNode(value) + return prefixes + }, {} as Prefixes) - for (const key in prefixes) { - rdfPrefix += `@prefix ${key} <${prefixes[key]}> .\n` - } + return prefixes + } - runInAction(() => { - this.prefixes = prefixes - this.rdf = `${rdfPrefix}\n` - }) + async loadDataTypes() { + try { + const statements = await this.dataGateway.get>(DATATYPE_PROPERTY_QUERY, getAndCheckDataTypeResponse) + runInAction(() => { + + this.literals = statements.map(statement => this.getUserFriendlyURI(statement.data_type_property.value)) + }) + } catch (err) { + throw err + } } + loadObjectProperties = async () => { + try { + const statements = await this.dataGateway.get>(OBJECT_PROPERTY_QUERY, getAndCheckObjectPropertyResponse) + runInAction(() => { + + this.relationships = statements.map(statement => this.getUserFriendlyURI(statement.object_property.value)) + }) + } catch (err) { + throw err + } + } addPrefix(prefix: string, namespace: string) { this.dataGateway.addPrefix(`${prefix}:`, namespace) } diff --git a/src/rdfInstanceViewer/RdfInstanceViewer.tsx b/src/rdfInstanceViewer/RdfInstanceViewer.tsx index 2a96b4c..9e4f7a1 100644 --- a/src/rdfInstanceViewer/RdfInstanceViewer.tsx +++ b/src/rdfInstanceViewer/RdfInstanceViewer.tsx @@ -1,24 +1,11 @@ -import { observer } from "mobx-react"; -import React, { useEffect } from "react"; -import { withInjection } from "../Core/Providers/injection"; -import { RdfInstancePresenter } from "./RdfInstancePresenter"; +import React from "react"; import ResizableDivs from "../Components/ResizableDivs/ResizableDivs"; import { Diagram } from "../Components/Diagram/Diagram"; import { Terminal } from "../Components/Terminal/Terminal"; -import { RdfPanelProps } from "../types"; -export const RdfInstanceComponent: React.FC = observer((props) => { - useEffect(() => { - if (!props.presenter) { - console.warn("No presenter found") - return - } - }, []) +export const RdfInstanceViewer: React.FC = () => ( + + + +) - return - - - -}) - -export const RdfInstanceViewer = withInjection({ presenter: RdfInstancePresenter })(RdfInstanceComponent) diff --git a/src/rdfInstanceViewer/Types/index.ts b/src/rdfInstanceViewer/Types/index.ts index 3370c31..c9e53d3 100644 --- a/src/rdfInstanceViewer/Types/index.ts +++ b/src/rdfInstanceViewer/Types/index.ts @@ -37,3 +37,23 @@ export const HierarchyResponseSchema = ResponseSchema.extend({ bindings: z.array(HierarchyStatement) }) }) + +export const DataTypeStatement = z.object({ + data_type_property: sparqlObject +}) + +export const DataTypeResponseSchema = ResponseSchema.extend({ + results: z.object({ + bindings: z.array(DataTypeStatement) + }) +}) + +export const ObjectPropertyStatement = z.object({ + object_property: sparqlObject +}) + +export const ObjectPropertyResponseSchema = ResponseSchema.extend({ + results: z.object({ + bindings: z.array(ObjectPropertyStatement) + }) +}) diff --git a/src/rdfInstanceViewer/__tests__/RdfInstance.spec.ts b/src/rdfInstanceViewer/__tests__/RdfInstance.spec.ts index 998d0b0..fe70023 100644 --- a/src/rdfInstanceViewer/__tests__/RdfInstance.spec.ts +++ b/src/rdfInstanceViewer/__tests__/RdfInstance.spec.ts @@ -4,10 +4,10 @@ import { AppTestHarness } from "../../TestTools/AppTestHarness"; import { QuadError, RdfInstancePresenter } from "../RdfInstancePresenter"; import { FakeHttpGateway } from "../../Core/FakeHttpGateway"; import { Types } from "../../Core/Types"; -import { GetRawPrefixDataStub } from "../../TestTools/GetRdfInputStub"; +import { GetRdfAClassOnly, GetPrefixRdfStub, GetRawPrefixDataStub, GetRdfAandBClassesOnly, GetRdfAandBClassesWithEdge, GetRdfInputStub, GetRdfAandBClassesWithEdgeAndLiteral } from "../../TestTools/GetRdfInputStub"; import { RdfInstanceRepository } from "../RdfInstanceRepository"; -import { Quad } from "@rdfjs/types"; -import { GetEdgeQuadStub, GetNodeQuadStub } from "../../TestTools/GetTripleStub"; +import { waitFor } from "@testing-library/react"; +import { Edge } from "reactflow"; let appTestHarness: AppTestHarness | null = null let rdfInstancePresenter: RdfInstancePresenter | null = null @@ -15,158 +15,136 @@ let rdfInstanceRepository: RdfInstanceRepository | null = null let dataGateway: FakeHttpGateway | null = null describe('rdfInstance', () => { - beforeEach(() => { - appTestHarness = new AppTestHarness() - appTestHarness.init() - dataGateway = appTestHarness.container.get(Types.IDataGateway) - rdfInstancePresenter = appTestHarness.container.get(RdfInstancePresenter) - rdfInstanceRepository = appTestHarness.container.get(RdfInstanceRepository) - }) describe('parsing rdf', () => { - + beforeEach(() => { + appTestHarness = new AppTestHarness() + appTestHarness.init() + dataGateway = appTestHarness.container.get(Types.IDataGateway) + rdfInstancePresenter = appTestHarness.container.get(RdfInstancePresenter) + rdfInstanceRepository = appTestHarness.container.get(RdfInstanceRepository) + }) it('should load prefixes and convert to valid rdf', () => { - if (dataGateway) { + if (dataGateway && rdfInstanceRepository) { // pivot dataGateway.getPrefixes = vi.fn().mockImplementation(() => GetRawPrefixDataStub) - - expect(rdfInstancePresenter?.viewModel.rdf).toMatchInlineSnapshot(` - "@prefix : . - @prefix xsd: . - @prefix dc: . - @prefix rdf: . - @prefix rdfs: . - @prefix owl: . - @prefix telicent: . - @prefix data: . - + expect(rdfInstanceRepository.rdf).toMatchInlineSnapshot(` + "@prefix : . + @prefix xsd: . + @prefix dc: . + @prefix rdf: . + @prefix rdfs: . + @prefix owl: . + @prefix telicent: . + @prefix data: . + @prefix ies: . " `) + expect(rdfInstanceRepository?.prefixes).toMatchInlineSnapshot(` { - ":": "http://telicent.io/ontology/", - "data:": "http://example.com/rdf/testdata#", - "dc:": "http://purl.org/dc/elements/1.1/", - "owl:": "http://www.w3.org/2002/07/owl#", - "rdf:": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", - "rdfs:": "http://www.w3.org/2000/01/rdf-schema#", - "telicent:": "http://telicent.io/ontology/", - "xsd:": "http://www.w3.org/2001/XMLSchema#", + "": { + "termType": "NamedNode", + "value": "http://telicent.io/ontology/", + }, + "data": { + "termType": "NamedNode", + "value": "http://example.com/rdf/testdata#", + }, + "dc": { + "termType": "NamedNode", + "value": "http://purl.org/dc/elements/1.1/", + }, + "ies": { + "termType": "NamedNode", + "value": "http://ies.data.gov.uk/ontology/ies4#", + }, + "owl": { + "termType": "NamedNode", + "value": "http://www.w3.org/2002/07/owl#", + }, + "rdf": { + "termType": "NamedNode", + "value": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + }, + "rdfs": { + "termType": "NamedNode", + "value": "http://www.w3.org/2000/01/rdf-schema#", + }, + "telicent": { + "termType": "NamedNode", + "value": "http://telicent.io/ontology/", + }, + "xsd": { + "termType": "NamedNode", + "value": "http://www.w3.org/2001/XMLSchema#", + }, } `) } else { - assert.fail("dataGateway is null") + assert.fail("dataGateway or rdfInstanceRepository is null") } }) - it('should parse valid rdf and add it to the appropriate edge and quad array', () => { - const quads: Array = [] - const nodes: Array = [] - const edges: Array = [] - const objects: Array = [] - + it('should parse valid rdf and generate nodes for reactflow', async () => { if (rdfInstancePresenter) { - const processData = rdfInstancePresenter.onDataPartial(nodes, edges, objects, quads) - processData(GetEdgeQuadStub()) - expect(quads).toEqual([GetEdgeQuadStub()]) - expect(edges).toEqual([GetEdgeQuadStub()]) - expect(nodes).toEqual([]) - expect(objects).toEqual([]) + rdfInstancePresenter.handleRdfInput(GetRdfInputStub) + await waitFor(() => { + expect(rdfInstancePresenter!.diagram.nodes.length).toBe(4) + expect(rdfInstancePresenter!.diagram.nodes[0].data.id).toBe("http://example.com/rdf/testdata#6cd17931-5c29-4cb9-8c26-745939aa9335") + expect(rdfInstancePresenter!.diagram.nodes[3].data.id).toBe("http://example.com/rdf/testdata#4c48ac99-61fd-4fa5-81e2-aab8e7648618") + }) } else { assert.fail("rdfInstancePresenter is null") } }) - it('should parse valid rdf and add it to the appropriate node and quad array', () => { - const quads: Array = [] - const nodes: Array = [] - const edges: Array = [] - const objects: Array = [] - + it('should parse valid rdf and generate edges for reactflow', async () => { if (rdfInstancePresenter) { - const processData = rdfInstancePresenter.onDataPartial(nodes, edges, objects, quads) - processData(GetNodeQuadStub()) - expect(quads).toEqual([GetNodeQuadStub()]) - expect(nodes).toEqual([GetNodeQuadStub()]) - expect(edges).toEqual([]) - expect(objects).toEqual([]) + rdfInstancePresenter.handleRdfInput(GetRdfInputStub) + await waitFor(() => { + // edges will have random ids due to nodes generating random unique ids + // this is because rdf identifiers are not unique + expect(rdfInstancePresenter!.diagram.edges.length).toBe(3) + }) } else { assert.fail("rdfInstancePresenter is null") } }) - it('should set nodes, edges, objects and prefixes on to rdfInstanceRepository and the rdf should be formatted', () => { - const quads: Array = [GetNodeQuadStub(), GetEdgeQuadStub()] - const nodes: Array = [GetNodeQuadStub()] - const edges: Array = [GetEdgeQuadStub()] - const objects: Array = [] - - if (rdfInstancePresenter) { - expect(rdfInstancePresenter.viewModel.nodes).toEqual([]) - expect(rdfInstancePresenter.viewModel.edges).toEqual([]) - expect(rdfInstancePresenter.viewModel.rdf).toMatchInlineSnapshot(` - "@prefix : . - @prefix xsd: . - @prefix dc: . - @prefix rdf: . - @prefix rdfs: . - @prefix owl: . - @prefix telicent: . - @prefix data: . - - " - `) - - // Pivot - rdfInstancePresenter.onEndPartial(nodes, edges, objects, quads)() - - expect(rdfInstancePresenter.viewModel.nodes).toMatchInlineSnapshot(` - [ - { - "data": { - "className": "person", - "name": "http://ies.data.gov.uk/ontology/ies4#Person", - "shortName": "P", - }, - "id": "http://telicent.io/data#0b791546-4f5c-4d58-9b62-7b7608af6468", - "position": { - "x": 0, - "y": 0, - }, - "type": "classInstanceNode", - }, - ] - `) - expect(rdfInstancePresenter.viewModel.edges).toMatchInlineSnapshot(` - [ - { - "id": "http://telicent.io/data#6cd17931-5c29-4cb9-8c26-745939aa9335-https://telicent.io/#0b791546-4f5c-4d58-9b62-7b7608af6468", - "label": "http://ies.data.gov.uk/ontology/ies4#aCopyOf", - "markerEnd": { - "type": "arrowclosed", - }, - "source": "http://telicent.io/data#6cd17931-5c29-4cb9-8c26-745939aa9335", - "target": "https://telicent.io/#0b791546-4f5c-4d58-9b62-7b7608af6468", - "type": "relationshipEdge", - }, - ] - `) - expect(rdfInstancePresenter.viewModel.rdf).toMatchInlineSnapshot(` - "@prefix : . - @prefix xsd: . - @prefix dc: . - @prefix rdf: . - @prefix rdfs: . - @prefix owl: . - @prefix telicent: . - @prefix data: . - - http://telicent.io/data#0b791546-4f5c-4d58-9b62-7b7608af6468 a http://ies.data.gov.uk/ontology/ies4#Person . - https://telicent.io/#0b791546-4f5c-4d58-9b62-7b7608af6468 http://ies.data.gov.uk/ontology/ies4#aCopyOf http://telicent.io/data#6cd17931-5c29-4cb9-8c26-745939aa9335 . - " - `) + it('should set nodes, edges, objects and prefixes on to rdfInstanceRepository and the rdf should be formatted', async () => { + + if (rdfInstancePresenter && rdfInstanceRepository) { + rdfInstancePresenter.handleRdfInput(GetRdfInputStub) + await waitFor(() => { + + expect(rdfInstanceRepository!.nodes.length).toBe(4) + expect(rdfInstanceRepository!.objectProperties.length).toBe(3) + expect(rdfInstanceRepository!.dataTypeProperties.length).toBe(0) + expect(rdfInstanceRepository!.rdf).toMatchInlineSnapshot(` + "@prefix : . + @prefix xsd: . + @prefix dc: . + @prefix rdf: . + @prefix rdfs: . + @prefix owl: . + @prefix telicent: . + @prefix data: . + @prefix ies: . + + data:6cd17931-5c29-4cb9-8c26-745939aa9335 rdf:type ies:BoundingState. + data:0b791546-4f5c-4d58-9b62-7b7608af6468 rdf:type ies:Person; + ies:aCopyOf data:6cd17931-5c29-4cb9-8c26-745939aa9335; + ies:EventParticipant data:4c48ac99-61fd-4fa5-81e2-aab8e7648618. + data:611bb20a-9c45-4d4e-b4ac-49a216eb18a8 rdf:type ies:GivenName; + ies:isStateOf data:0b791546-4f5c-4d58-9b62-7b7608af6468. + data:4c48ac99-61fd-4fa5-81e2-aab8e7648618 rdf:type ies:Event. + " + `) + + }) } else { - assert.fail("rdfInstancePresenter is null") + assert.fail("rdfInstancePresenter or rdfInstanceRepository is null") } }) @@ -229,45 +207,37 @@ describe('rdfInstance', () => { }) }) + describe('Adding Nodes manually', () => { - it('should fail to add a new node', () => { - if (rdfInstancePresenter) { + beforeEach(() => { + appTestHarness = new AppTestHarness() + appTestHarness.init() + dataGateway = appTestHarness.container.get(Types.IDataGateway) + rdfInstancePresenter = appTestHarness.container.get(RdfInstancePresenter) + rdfInstanceRepository = appTestHarness.container.get(RdfInstanceRepository) + }) + it('should fail to add a new node', async () => { + if (rdfInstancePresenter && rdfInstanceRepository) { + const consoleWarn = vi.spyOn(console, 'warn') rdfInstancePresenter.addNode() - expect(rdfInstancePresenter.viewModel.rdf).toMatchInlineSnapshot(` - "@prefix : . - @prefix xsd: . - @prefix dc: . - @prefix rdf: . - @prefix rdfs: . - @prefix owl: . - @prefix telicent: . - @prefix data: . - - " - `) + await waitFor(() => { + expect(consoleWarn).toHaveBeenCalledWith("cancelling creation of node, missing name or type") + expect(rdfInstanceRepository!.rdf).toBe(GetPrefixRdfStub) + }) } else { - assert.fail("rdfInstancePresenter is null") + assert.fail("rdfInstancePresenter or rdfInstanceRepository is null") } }) - it('should add a node successfully', () => { + it('should add a node successfully', async () => { if (rdfInstancePresenter) { - rdfInstancePresenter.newNodeName = "newName" - rdfInstancePresenter.newNodeType = "newType" + rdfInstancePresenter.newNodeName = "data:nodeA" + rdfInstancePresenter.newNodeType = "ies:nodeAType" rdfInstancePresenter.addNode() - expect(rdfInstancePresenter.viewModel.rdf).toMatchInlineSnapshot(` - "@prefix : . - @prefix xsd: . - @prefix dc: . - @prefix rdf: . - @prefix rdfs: . - @prefix owl: . - @prefix telicent: . - @prefix data: . - - - newName a newType ." - `) + await waitFor(() => { + + expect(rdfInstanceRepository!.rdf).toBe(GetRdfAClassOnly) + }) } else { assert.fail("rdfInstancePresenter is null") } @@ -275,47 +245,52 @@ describe('rdfInstance', () => { }) describe('Adding edges manually', () => { + + beforeEach(() => { + appTestHarness = new AppTestHarness() + appTestHarness.init() + dataGateway = appTestHarness.container.get(Types.IDataGateway) + rdfInstancePresenter = appTestHarness.container.get(RdfInstancePresenter) + rdfInstanceRepository = appTestHarness.container.get(RdfInstanceRepository) + }) + it("should fail to add a new edge", () => { + const warnSpy = vi.spyOn(console, 'warn') if (rdfInstancePresenter) { + rdfInstancePresenter.newEdgeType = "a" + rdfInstancePresenter.newEdgeSource = "s" + rdfInstancePresenter.newEdgeTarget = "t" rdfInstancePresenter.addEdge() - expect(rdfInstancePresenter.viewModel.rdf).toMatchInlineSnapshot(` - "@prefix : . - @prefix xsd: . - @prefix dc: . - @prefix rdf: . - @prefix rdfs: . - @prefix owl: . - @prefix telicent: . - @prefix data: . - - " - `) + expect(warnSpy).toHaveBeenCalledWith("Unable to find connecting nodes") } else { assert.fail("rdfInstancePresenter is null") } }) - it("should add an edge successfully", () => { - if (rdfInstancePresenter) { - rdfInstancePresenter.newEdgeType = "edgeType" - rdfInstancePresenter.newEdgeSource = "edgeSource" - rdfInstancePresenter.newEdgeTarget = "edgeTarget" + // setting the edge types and source + target seems to cause + // the viewModel to be re-rendered each time meaning the + // randomly generated id's are wrong when add edge is called. + // This should not happen as the values aren't observable. + // Requires investigation + it("should add an edge successfully", async () => { + if (rdfInstancePresenter && rdfInstanceRepository) { - rdfInstancePresenter.addEdge() + rdfInstancePresenter.handleRdfInput(GetRdfAandBClassesOnly) - expect(rdfInstancePresenter.viewModel.rdf).toMatchInlineSnapshot(` - "@prefix : . - @prefix xsd: . - @prefix dc: . - @prefix rdf: . - @prefix rdfs: . - @prefix owl: . - @prefix telicent: . - @prefix data: . + await waitFor(() => { + expect(rdfInstanceRepository!.rdf).toBe(GetRdfAandBClassesOnly) + }) + rdfInstancePresenter!.newEdgeType = "ies:edgeType" + rdfInstancePresenter!.newEdgeSource = rdfInstancePresenter!.diagram.nodes[0].id + rdfInstancePresenter!.newEdgeTarget = rdfInstancePresenter!.diagram.nodes[1].id + + rdfInstancePresenter.addEdge() + + await waitFor(() => { + expect(rdfInstanceRepository!.rdf).toBe(GetRdfAandBClassesWithEdge) + }) - edgeTarget edgeType edgeSource ." - `) } else { assert.fail("rdfInstancePresenter is null") } @@ -323,135 +298,109 @@ describe('rdfInstance', () => { }) describe("Delete Node manually", () => { - it("should delete node and joined edge successfully", () => { - - if (rdfInstancePresenter) { - rdfInstancePresenter.newNodeName = "nodeA" - rdfInstancePresenter.newNodeType = "nodeAType" - - rdfInstancePresenter.addNode() - - rdfInstancePresenter.newNodeName = "nodeB" - rdfInstancePresenter.newNodeType = "nodeBType" - - rdfInstancePresenter.addNode() - - rdfInstancePresenter.newEdgeType = "edgeType" - rdfInstancePresenter.newEdgeSource = "nodeA" - rdfInstancePresenter.newEdgeTarget = "nodeB" - - rdfInstancePresenter.addEdge() - - expect(rdfInstancePresenter.viewModel.rdf).toMatchInlineSnapshot(` - "@prefix : . - @prefix xsd: . - @prefix dc: . - @prefix rdf: . - @prefix rdfs: . - @prefix owl: . - @prefix telicent: . - @prefix data: . + beforeEach(() => { + appTestHarness = new AppTestHarness() + appTestHarness.init() + dataGateway = appTestHarness.container.get(Types.IDataGateway) + rdfInstancePresenter = appTestHarness.container.get(RdfInstancePresenter) + rdfInstanceRepository = appTestHarness.container.get(RdfInstanceRepository) + }) + it("should fail to delete node", () => { + const consoleWarn = vi.spyOn(console, 'warn') + rdfInstancePresenter?.deleteNode("doesnt exist") + expect(consoleWarn).toHaveBeenCalledWith("cannot find node to delete") + }) + it("should delete node successfully", async () => { - nodeA a nodeAType . - nodeB a nodeBType . - nodeB edgeType nodeA ." - `) - rdfInstancePresenter.deleteNodeAndAssociatedEdges("nodeA") - expect(rdfInstancePresenter.viewModel.rdf).toMatchInlineSnapshot(` - "@prefix : . - @prefix xsd: . - @prefix dc: . - @prefix rdf: . - @prefix rdfs: . - @prefix owl: . - @prefix telicent: . - @prefix data: . - - - nodeB a nodeBType ." - `) + if (rdfInstancePresenter) { + rdfInstancePresenter.handleRdfInput(GetRdfAandBClassesWithEdge) + + await waitFor(() => { + expect(rdfInstanceRepository!.rdf).toBe(GetRdfAandBClassesWithEdge) + }) + + const id = rdfInstancePresenter!.diagram.nodes[1].id + rdfInstancePresenter!.deleteNode(id) + + await waitFor(() => { + expect(rdfInstanceRepository!.rdf).toBe(`@prefix : . +@prefix xsd: . +@prefix dc: . +@prefix rdf: . +@prefix rdfs: . +@prefix owl: . +@prefix telicent: . +@prefix data: . +@prefix ies: . + +data:nodeA rdf:type ies:nodeAType; + ies:edgeType data:nodeB. +`) + }) } }) }) - describe("Delete Egde manually", () => { - it("should not attempt to delete anything if no rdf has been set", () => { - if (rdfInstancePresenter) { - rdfInstancePresenter.deleteEdge("source", "target", "label") - expect(rdfInstancePresenter.viewModel.rdf).toMatchInlineSnapshot(` - "@prefix : . - @prefix xsd: . - @prefix dc: . - @prefix rdf: . - @prefix rdfs: . - @prefix owl: . - @prefix telicent: . - @prefix data: . + describe("Delete Edge manually", () => { - " - `) + beforeEach(() => { + appTestHarness = new AppTestHarness() + appTestHarness.init() + dataGateway = appTestHarness.container.get(Types.IDataGateway) + rdfInstancePresenter = appTestHarness.container.get(RdfInstancePresenter) + rdfInstanceRepository = appTestHarness.container.get(RdfInstanceRepository) + }) + it("should delete edge successfully", async () => { + if (rdfInstancePresenter && rdfInstanceRepository) { + rdfInstancePresenter.handleRdfInput(GetRdfAandBClassesWithEdge) + + await waitFor(() => { + expect(rdfInstanceRepository!.rdf).toBe(GetRdfAandBClassesWithEdge) + }) + + rdfInstancePresenter.deleteEdges([{ + source: "idAForReactFlowDiagram", + target: "idBForReactFlowDiagram", + id: "http://example.com/rdf/testdata#nodeA--http://example.com/rdf/testdata#nodeB", + label: "ies:edgeType" + } as Edge]) + + await waitFor(() => { + expect(rdfInstanceRepository!.rdf).toBe(GetRdfAandBClassesOnly) + }) } else { assert.fail("rdfInstancePresenter is null") } }) - it("should delete edge successfully", () => { - if (rdfInstancePresenter) { - rdfInstancePresenter.newNodeName = "nodeA" - rdfInstancePresenter.newNodeType = "nodeAType" - - rdfInstancePresenter.addNode() - - rdfInstancePresenter.newNodeName = "nodeB" - rdfInstancePresenter.newNodeType = "nodeBType" - - rdfInstancePresenter.addNode() - - rdfInstancePresenter.newEdgeType = "edgeType" - rdfInstancePresenter.newEdgeSource = "nodeA" - rdfInstancePresenter.newEdgeTarget = "nodeB" - - rdfInstancePresenter.addEdge() + }) - expect(rdfInstancePresenter.viewModel.rdf).toMatchInlineSnapshot(` - "@prefix : . - @prefix xsd: . - @prefix dc: . - @prefix rdf: . - @prefix rdfs: . - @prefix owl: . - @prefix telicent: . - @prefix data: . + describe("Add Literal", () => { + beforeEach(() => { + appTestHarness = new AppTestHarness() + appTestHarness.init() + dataGateway = appTestHarness.container.get(Types.IDataGateway) + rdfInstancePresenter = appTestHarness.container.get(RdfInstancePresenter) + rdfInstanceRepository = appTestHarness.container.get(RdfInstanceRepository) + }) + it("should add a literal to a node", async () => { + if (rdfInstancePresenter && rdfInstanceRepository) { + rdfInstancePresenter.handleRdfInput(GetRdfAandBClassesWithEdge) + await waitFor(() => { + expect(rdfInstanceRepository!.rdf).toBe(GetRdfAandBClassesWithEdge) + }) - nodeA a nodeAType . - nodeB a nodeBType . - nodeB edgeType nodeA ." - `) + rdfInstancePresenter.selectedNode = "data:nodeB" + rdfInstancePresenter.addLiteral("ies:representativeValue", "Anderson") - // pivot - // TODO: check with Ian if source and target are the right way round - rdfInstancePresenter.deleteEdge("nodeA", "nodeB", "edgeType") - - expect(rdfInstancePresenter.rdfInstanceRepository.rdf).toMatchInlineSnapshot(` - "@prefix : . - @prefix xsd: . - @prefix dc: . - @prefix rdf: . - @prefix rdfs: . - @prefix owl: . - @prefix telicent: . - @prefix data: . - - - nodeA a nodeAType . - nodeB a nodeBType ." - `) + await waitFor(() => { + expect(rdfInstanceRepository!.rdf).toBe(GetRdfAandBClassesWithEdgeAndLiteral) + }) } else { assert.fail("rdfInstancePresenter is null") } }) }) - }) diff --git a/yarn.lock b/yarn.lock index 30f7d35..8e550ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1641,6 +1641,14 @@ "@types/mapbox__point-geometry" "*" "@types/pbf" "*" +"@types/n3@^1.16.4": + version "1.16.4" + resolved "https://registry.yarnpkg.com/@types/n3/-/n3-1.16.4.tgz#007f489eb848a6a8ac586b037b8eea281da5730f" + integrity sha512-6PmHRYCCdjbbBV2UVC/HjtL6/5Orx9ku2CQjuojucuHvNvPmnm6+02B18YGhHfvU25qmX2jPXyYPHsMNkn+w2w== + dependencies: + "@rdfjs/types" "^1.1.0" + "@types/node" "*" + "@types/node@*": version "20.12.7" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.7.tgz#04080362fa3dd6c5822061aa3124f5c152cff384" @@ -4039,7 +4047,7 @@ mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" -n3@^1.16.3, n3@^1.17.0: +n3@^1.16.3, n3@^1.17.0, n3@^1.17.3: version "1.17.3" resolved "https://registry.yarnpkg.com/n3/-/n3-1.17.3.tgz#28f33fae36812226bc677f17742afe32f7d2a105" integrity sha512-ZHc24eZi2GIJcJQVxtL6NT3g+mTHRNeTVfXWELzeUOirqLrh2AAyg0nfYZ/kryJWKFSCgO37DGB6Ok3qmGgEcA==