diff --git a/app/src/app.tsx b/app/src/app.tsx index 9e6567a..420fb00 100644 --- a/app/src/app.tsx +++ b/app/src/app.tsx @@ -3,6 +3,7 @@ import { StrictMode } from 'react'; import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import WorkspacesPage from './pages/workspaces_page'; +import { ReactFlowProvider } from '@xyflow/react'; import './index.scss'; import MappingPage from './pages/mapping_page'; import OntologiesPage from './pages/ontologies_page'; @@ -28,7 +29,11 @@ const router = createBrowserRouter([ }, { path: '/workspaces/:uuid/mapping/:mapping_uuid', - element: , + element: ( + + + + ), }, ]); diff --git a/app/src/lib/api/mapping_service/types.ts b/app/src/lib/api/mapping_service/types.ts index 53d4a19..cb3d24d 100644 --- a/app/src/lib/api/mapping_service/types.ts +++ b/app/src/lib/api/mapping_service/types.ts @@ -3,6 +3,7 @@ export type MappingNodeType = 'entity' | 'literal' | 'uri_ref'; export type MappingNode = { id: string; type: 'entity'; + position: { x: number; y: number }; label: string; uri_pattern: string; rdf_type: string[]; @@ -12,6 +13,7 @@ export type MappingNode = { export type MappingLiteral = { id: string; type: 'literal'; + position: { x: number; y: number }; label: string; value: string; literal_type: string; @@ -20,6 +22,7 @@ export type MappingLiteral = { export type MappingURIRef = { id: string; type: 'uri_ref'; + position: { x: number; y: number }; uri_pattern: string; }; diff --git a/app/src/pages/mapping_page/components/MainPanel/index.tsx b/app/src/pages/mapping_page/components/MainPanel/index.tsx index cbc3458..c406bec 100644 --- a/app/src/pages/mapping_page/components/MainPanel/index.tsx +++ b/app/src/pages/mapping_page/components/MainPanel/index.tsx @@ -15,14 +15,16 @@ import { } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; -import { useCallback } from 'react'; +import { useCallback, useEffect } from 'react'; import { MappingGraph } from '../../../../lib/api/mapping_service/types'; import ConnectionLineComponent from '@/pages/mapping_page/components/MainPanel/components/ConnectionLineComponent'; import { EntityNode } from '@/pages/mapping_page/components/MainPanel/components/EntityNode'; import FloatingEdge from '@/pages/mapping_page/components/MainPanel/components/FloatingEdge'; import { LiteralNode } from '@/pages/mapping_page/components/MainPanel/components/LiteralNode'; -import { URIRefNode } from '@/pages/mapping_page/components/MainPanel/components/UriRefNode'; + +import { URIRefNode } from '@/pages/mapping_page/components/MainPanel/components/URIRefNode'; +import { useBackendMappingGraph } from '@/pages/mapping_page/hooks/useBackendMappingGraph'; import { XYEdgeType, XYNodeTypes } from './types'; type MainPanelProps = { @@ -51,10 +53,17 @@ const defaultEdgeOptions = { const MainPanel = ({ initialGraph }: MainPanelProps) => { const reactflow = useReactFlow(); + const { nodes: initialNodes, edges: initialEdges } = + useBackendMappingGraph(initialGraph); const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); + useEffect(() => { + setNodes(initialNodes); + setEdges(initialEdges); + }, [initialNodes, initialEdges, setNodes, setEdges]); + const onConnect = useCallback( (params: Connection) => { setEdges(edges => addEdge(params, edges)); @@ -75,8 +84,11 @@ const MainPanel = ({ initialGraph }: MainPanelProps) => { uri_pattern: '', properties: [], type: 'entity', + position: reactflow.screenToFlowPosition({ + x: e.clientX, + y: e.clientY, + }), }, - position: reactflow.screenToFlowPosition({ x: e.clientX, y: e.clientY, @@ -98,6 +110,10 @@ const MainPanel = ({ initialGraph }: MainPanelProps) => { id: `node-${nodes.length}`, uri_pattern: 'http://example.com/', type: 'uri_ref', + position: reactflow.screenToFlowPosition({ + x: e.clientX, + y: e.clientY, + }), }, position: reactflow.screenToFlowPosition({ x: e.clientX, @@ -112,6 +128,10 @@ const MainPanel = ({ initialGraph }: MainPanelProps) => { const handleAddLiteralNode = useCallback( (e: React.MouseEvent) => { + const position = reactflow.screenToFlowPosition({ + x: e.clientX, + y: e.clientY, + }); setNodes(nodes => [ ...nodes, { @@ -122,11 +142,9 @@ const MainPanel = ({ initialGraph }: MainPanelProps) => { value: '', literal_type: 'string', type: 'literal', + position: position, }, - position: reactflow.screenToFlowPosition({ - x: e.clientX, - y: e.clientY, - }), + position: position, type: 'literal', }, ]); diff --git a/app/src/pages/mapping_page/components/MainPanel/utils_old.js b/app/src/pages/mapping_page/components/MainPanel/utils_old.js deleted file mode 100644 index d9a6efc..0000000 --- a/app/src/pages/mapping_page/components/MainPanel/utils_old.js +++ /dev/null @@ -1,105 +0,0 @@ -import { Position } from '@xyflow/react'; - -// this helper function returns the intersection point -// of the line between the center of the intersectionNode and the target node -function getNodeIntersection( - intersectionNode, - intersectionNodeHandleId, - targetNode, - targetNodeHandleId, -) { - // https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a - - const intersectionNodePosition = intersectionNode.internals.positionAbsolute; - const targetPosition = targetNode.internals.positionAbsolute; - - const intersectionNodeHandle = [ - ...(intersectionNode.internals.handleBounds.source || []), - ...(intersectionNode.internals.handleBounds.target || []), - ].find(handle => handle.id === intersectionNodeHandleId); - - const targetNodeHandle = [ - ...(targetNode.internals.handleBounds.source || []), - ...(targetNode.internals.handleBounds.target || []), - ].find(handle => handle.id === targetNodeHandleId); - - const w = intersectionNodeHandle.width / 2; - const h = intersectionNodeHandle.height / 2; - - const x2 = intersectionNodeHandle.x + intersectionNodePosition.x + w; - const y2 = intersectionNodeHandle.y + intersectionNodePosition.y + h; - const x1 = targetNodeHandle.x + targetPosition.x + targetNodeHandle.width / 2; - const y1 = - targetNodeHandle.y + targetPosition.y + targetNodeHandle.height / 2; - - const xx1 = (x1 - x2) / (2 * w) - (y1 - y2) / (2 * h); - const yy1 = (x1 - x2) / (2 * w) + (y1 - y2) / (2 * h); - const a = 1 / (Math.abs(xx1) + Math.abs(yy1) || 1); - const xx3 = a * xx1; - const yy3 = a * yy1; - - // Calculate y normally - const y = h * (-xx3 + yy3) + y2; - - // Clamp x to either left or right edge - const x = - x1 < x2 - ? intersectionNodeHandle.x + intersectionNodePosition.x // left edge - : intersectionNodeHandle.x + - intersectionNodePosition.x + - intersectionNodeHandle.width; // right edge - - return { x, y }; -} - -// returns the position (top,right,bottom or right) passed node compared to the intersection point -function getEdgePosition(node, intersectionPoint) { - const n = { ...node.measured.positionAbsolute, ...node }; - const nx = Math.round(n.x); - const ny = Math.round(n.y); - const px = Math.round(intersectionPoint.x); - const py = Math.round(intersectionPoint.y); - - if (px <= nx + 1) { - return Position.Left; - } - if (px >= nx + n.measured.width - 1) { - return Position.Right; - } - if (py <= ny + 1) { - return Position.Top; - } - if (py >= n.y + n.measured.height - 1) { - return Position.Bottom; - } - - return Position.Top; -} - -// returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge -export function getEdgeParams(source, sourceHandleId, target, targetHandleId) { - const sourceIntersectionPoint = getNodeIntersection( - source, - sourceHandleId, - target, - targetHandleId, - ); - const targetIntersectionPoint = getNodeIntersection( - target, - targetHandleId, - source, - sourceHandleId, - ); - - const sourcePos = getEdgePosition(source, sourceIntersectionPoint); - const targetPos = getEdgePosition(target, targetIntersectionPoint); - - return { - sx: sourceIntersectionPoint.x, - sy: sourceIntersectionPoint.y, - tx: targetIntersectionPoint.x, - ty: targetIntersectionPoint.y, - sourcePos, - targetPos, - }; -} diff --git a/app/src/pages/mapping_page/components/NodeProperties/components/EntityProperties.tsx b/app/src/pages/mapping_page/components/NodeProperties/components/EntityProperties.tsx index 361751d..f6516c4 100644 --- a/app/src/pages/mapping_page/components/NodeProperties/components/EntityProperties.tsx +++ b/app/src/pages/mapping_page/components/NodeProperties/components/EntityProperties.tsx @@ -6,19 +6,56 @@ import { OntologyClass, Property, } from '@/lib/api/ontology_api/types'; -import { EntityNodeType } from '@/pages/mapping_page/components/MainPanel/types'; +import { + EntityNodeType, + XYEdgeType, +} from '@/pages/mapping_page/components/MainPanel/types'; import useClassOrderer from '@/pages/mapping_page/hooks/useClassOrderer'; import useDomainOrderer from '@/pages/mapping_page/hooks/useDomainOrderer'; import useMappingPage from '@/pages/mapping_page/state'; import { FormGroup, H5, InputGroup, MenuItem } from '@blueprintjs/core'; -import { ItemListRendererProps, MultiSelect } from '@blueprintjs/select'; -import { useReactFlow } from '@xyflow/react'; +import { + ItemListRendererProps, + ItemRenderer, + MultiSelect, +} from '@blueprintjs/select'; +import { getConnectedEdges, useEdges, useReactFlow } from '@xyflow/react'; import { useCallback, useMemo } from 'react'; import './styles.scss'; +const renderMenuItemProperty: ItemRenderer = ( + item, + { handleClick, modifiers }, +) => ( + 0 ? item.label[0].value : item.full_uri} + onClick={handleClick} + active={modifiers.active} + /> +); + +const renderMenuItemClass: ItemRenderer = ( + item, + { handleClick, modifiers }, +) => ( + 0 ? item.label[0].value : item.full_uri} + onClick={handleClick} + active={modifiers.active} + /> +); + const EntityNodeProperties = ({ node }: { node: EntityNodeType }) => { const ontologies = useMappingPage(state => state.ontologies); + const edges = useEdges(); + const reactflow = useReactFlow(); const properties = useMemo<(Property & { group: string })[]>( @@ -68,6 +105,39 @@ const EntityNodeProperties = ({ node }: { node: EntityNodeType }) => { const possibleClasses = useClassOrderer(node); const possibleProperties = useDomainOrderer(node); + const classItems = useMemo( + () => + possibleClasses.classes.filter( + c => !rdfType.some(r => r.full_uri === c.full_uri), + ), + [possibleClasses.classes, rdfType], + ); + + const propertyItems = useMemo( + () => + possibleProperties.filter( + p => !properties.some(prop => prop.full_uri === p.full_uri), + ), + [possibleProperties, properties], + ); + + const handlePropertyRemove = useCallback( + (item: NamedNode & { group: string }) => { + if (!reactflow) return; + console.log('Removing property', item.full_uri); + const connectedEdges = getConnectedEdges([node], edges).filter( + edge => edge.sourceHandle === item.full_uri, + ); + + if (connectedEdges.length > 0) { + reactflow.deleteElements({ edges: connectedEdges }); + } + }, + // Reason: we only want to update the node when the node id or reactflow changes + // eslint-disable-next-line react-hooks/exhaustive-deps + [edges, node.id, reactflow], + ); + const updateNode = useCallback( ( label: string | null, @@ -75,6 +145,15 @@ const EntityNodeProperties = ({ node }: { node: EntityNodeType }) => { rdfType: (OntologyClass & { group: string })[] | null, properties: (Property & { group: string })[] | null, ) => { + if (!reactflow) return; + if ( + label === null && + uriPattern === null && + rdfType === null && + properties === null + ) + return; + console.log('Updating node', node.id); reactflow.updateNode(node.id, { data: { ...node.data, @@ -85,7 +164,9 @@ const EntityNodeProperties = ({ node }: { node: EntityNodeType }) => { }, }); }, - [node, reactflow], + // Reason: we only want to update the node when the node id or reactflow changes + // eslint-disable-next-line react-hooks/exhaustive-deps + [node.id, reactflow], ); const createNewClassItemFromQuery = (query: string) => { @@ -198,20 +279,9 @@ const EntityNodeProperties = ({ node }: { node: EntityNodeType }) => { null, ) } - itemRenderer={(item, { handleClick, modifiers }) => ( - 0 ? item.label[0].value : item.full_uri} - onClick={handleClick} - active={modifiers.active} - /> - )} + itemRenderer={renderMenuItemClass} itemPredicate={itemSearch} - items={possibleClasses.classes.filter( - c => !rdfType.some(r => r.full_uri === c.full_uri), - )} + items={classItems} onItemSelect={item => { try { new URL(item.full_uri); @@ -248,28 +318,18 @@ const EntityNodeProperties = ({ node }: { node: EntityNodeType }) => { matchTargetWidth: true, popoverClassName: 'popover-scroll', }} - onRemove={item => + onRemove={item => { + handlePropertyRemove(item); updateNode( null, null, null, properties.filter(p => p !== item), - ) - } - itemRenderer={(item, { handleClick, modifiers }) => ( - 0 ? item.label[0].value : item.full_uri} - onClick={handleClick} - active={modifiers.active} - /> - )} + ); + }} + itemRenderer={renderMenuItemProperty} itemPredicate={itemSearch} - items={possibleProperties.filter( - p => !properties.some(prop => prop.full_uri === p.full_uri), - )} + items={propertyItems} onItemSelect={item => { try { new URL(item.full_uri); diff --git a/app/src/pages/mapping_page/components/NodeProperties/components/LiteralProperties.tsx b/app/src/pages/mapping_page/components/NodeProperties/components/LiteralProperties.tsx index 52755bb..c79fc34 100644 --- a/app/src/pages/mapping_page/components/NodeProperties/components/LiteralProperties.tsx +++ b/app/src/pages/mapping_page/components/NodeProperties/components/LiteralProperties.tsx @@ -34,6 +34,9 @@ const LiteralNodeProperties = ({ node }: { node: LiteralNodeType }) => { literalType: string | null, value: string | null, ) => { + if (label === null && literalType === null && value === null) { + return; + } reactflow.updateNode(node.id, { data: { ...node.data, diff --git a/app/src/pages/mapping_page/components/NodeProperties/components/URIRefProperties.tsx b/app/src/pages/mapping_page/components/NodeProperties/components/URIRefProperties.tsx index e3da108..6dea366 100644 --- a/app/src/pages/mapping_page/components/NodeProperties/components/URIRefProperties.tsx +++ b/app/src/pages/mapping_page/components/NodeProperties/components/URIRefProperties.tsx @@ -11,6 +11,9 @@ const URIRefProperties = ({ node }: { node: URIRefNodeType }) => { const updateNode = useCallback( (uriPattern: string | null) => { + if (uriPattern === null) { + return; + } reactflow.updateNode(node.id, { data: { ...node.data, @@ -18,7 +21,8 @@ const URIRefProperties = ({ node }: { node: URIRefNodeType }) => { }, }); }, - [node, reactflow], + // eslint-disable-next-line react-hooks/exhaustive-deps + [node.id, reactflow], ); return ( diff --git a/app/src/pages/mapping_page/components/NodeProperties/index.tsx b/app/src/pages/mapping_page/components/NodeProperties/index.tsx index 54153a4..1071225 100644 --- a/app/src/pages/mapping_page/components/NodeProperties/index.tsx +++ b/app/src/pages/mapping_page/components/NodeProperties/index.tsx @@ -1,32 +1,16 @@ import LiteralNodeProperties from '@/pages/mapping_page/components/NodeProperties/components/LiteralProperties'; import URIRefProperties from '@/pages/mapping_page/components/NodeProperties/components/URIRefProperties'; import { NonIdealState } from '@blueprintjs/core'; -import { useNodes } from '@xyflow/react'; -import { useEffect, useMemo, useState } from 'react'; +import { useStore } from '@xyflow/react'; import { EntityNodeType, LiteralNodeType, URIRefNodeType, - XYNodeTypes, } from '../MainPanel/types'; import EntityNodeProperties from './components/EntityProperties'; const NodeProperties = () => { - const nodes = useNodes(); - - const selectedNodes = useMemo(() => { - return nodes.filter(node => node.selected); - }, [nodes]); - - const [selectedNode, setSelectedNode] = useState(null); - - useEffect(() => { - if (selectedNodes.length === 1) { - setSelectedNode(selectedNodes[0]); - } else { - setSelectedNode(null); - } - }, [selectedNodes]); + const selectedNode = useStore(state => state.nodes.find(n => n.selected)); if (!selectedNode) { return ( diff --git a/app/src/pages/mapping_page/hooks/useBackendMappingGraph.ts b/app/src/pages/mapping_page/hooks/useBackendMappingGraph.ts new file mode 100644 index 0000000..6085bce --- /dev/null +++ b/app/src/pages/mapping_page/hooks/useBackendMappingGraph.ts @@ -0,0 +1,63 @@ +import { MappingGraph } from '@/lib/api/mapping_service/types'; +import { + XYEdgeType, + XYNodeTypes, +} from '@/pages/mapping_page/components/MainPanel/types'; +import { useEffect, useState } from 'react'; + +export function useBackendMappingGraph(mappingGraph: MappingGraph | null) { + const [nodes, setNodes] = useState([]); + const [edges, setEdges] = useState([]); + + useEffect(() => { + if (!mappingGraph) { + setNodes([]); + setEdges([]); + } + + const nodes = mappingGraph?.nodes.map(node => { + switch (node.type) { + case 'entity': + return { + id: node.id, + position: node.position, + type: 'entity', + data: node, + }; + case 'literal': + return { + id: node.id, + position: node.position, + type: 'literal', + data: node, + }; + case 'uri_ref': + return { + id: node.id, + position: node.position, + type: 'uri_ref', + data: node, + }; + } + }); + + const edges = mappingGraph?.edges.map(edge => ({ + id: edge.id, + source: edge.source, + target: edge.target, + sourceHandle: edge.source_handle, + targetHandle: edge.target_handle, + data: edge, + })); + + setNodes(nodes || []); + setEdges(edges || []); + + return () => { + setNodes([]); + setEdges([]); + }; + }, [mappingGraph]); + + return { nodes, edges }; +} diff --git a/app/src/pages/mapping_page/index.tsx b/app/src/pages/mapping_page/index.tsx index e9debb7..069d9b0 100644 --- a/app/src/pages/mapping_page/index.tsx +++ b/app/src/pages/mapping_page/index.tsx @@ -1,4 +1,8 @@ -import { ReactFlowProvider } from '@xyflow/react'; +import { + XYEdgeType, + XYNodeTypes, +} from '@/pages/mapping_page/components/MainPanel/types'; +import { useEdges, useNodes } from '@xyflow/react'; import { languages } from 'monaco-editor'; import { useEffect, useRef, useState } from 'react'; import { @@ -40,10 +44,14 @@ const MappingPage = () => { const loadMapping = useMappingPage(state => state.loadMapping); const saveMapping = useMappingPage(state => state.saveMapping); + const nodes = useNodes(); + const edges = useEdges(); + useRegisterTheme('mapping-theme', mapping_theme); useRegisterLanguage('mapping_language', mapping_language, {}); useRegisterCompletionItemProvider('mapping_language', [ { + // eslint-disable-next-line @typescript-eslint/no-unused-vars provideCompletionItems(model, position, context, token) { const word = model.getWordUntilPosition(position); @@ -110,47 +118,45 @@ const MappingPage = () => { const handleSave = () => { if (mapping && props.uuid && props.mapping_uuid) { - saveMapping(props.uuid, props.mapping_uuid, mapping); + saveMapping(props.uuid, props.mapping_uuid, mapping, nodes, edges); } }; return ( - -
- -
- - - setIsCollapsed(true)} - defaultSize={20} - minSize={10} - maxSize={50} - > - - - {!isCollapsed && ( - - )} - - - - -
+
+ +
+ + + setIsCollapsed(true)} + defaultSize={20} + minSize={10} + maxSize={50} + > + + + {!isCollapsed && ( + + )} + + + +
- +
); }; diff --git a/app/src/pages/mapping_page/state.ts b/app/src/pages/mapping_page/state.ts index 75ac069..46efb76 100644 --- a/app/src/pages/mapping_page/state.ts +++ b/app/src/pages/mapping_page/state.ts @@ -1,3 +1,7 @@ +import { + XYEdgeType, + XYNodeTypes, +} from '@/pages/mapping_page/components/MainPanel/types'; import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; import MappingService from '../../lib/api/mapping_service'; @@ -24,7 +28,9 @@ interface MappingPageStateActions { saveMapping: ( workspaceUuid: string, mappingUuid: string, - mapping: MappingGraph, + mappingGraph: MappingGraph, + nodes: XYNodeTypes[], + edges: XYEdgeType[], ) => Promise; } @@ -73,9 +79,29 @@ const functions: ZustandActions = ( async saveMapping( workspaceUuid: string, mappingUuid: string, - mapping: MappingGraph, + mappingGraph: MappingGraph, + nodes: XYNodeTypes[], + edges: XYEdgeType[], ) { set({ isLoading: 'Saving mapping...' }); + // Convert nodes and edges to MappingGraph, sync nodes position and id with node.data + // and edges source, sourceHandle, target, targetHandle and id with edge.data + const mapping = { + ...mappingGraph, + nodes: nodes.map(node => ({ + ...node.data, + position: node.position, + id: node.id, + })), + edges: edges.map(edge => ({ + ...edge.data, + source: edge.source, + target: edge.target, + source_handle: edge.sourceHandle, + target_handle: edge.targetHandle, + id: edge.id, + })), + } as MappingGraph; try { await MappingService.updateMapping(workspaceUuid, mappingUuid, mapping); set({ error: null }); diff --git a/server/logger.py b/server/logger.py index 4748503..b144e00 100644 --- a/server/logger.py +++ b/server/logger.py @@ -32,6 +32,7 @@ def setup_logging( drop_color_message_key, timestamper, structlog.processors.StackInfoRenderer(), + structlog.processors.format_exc_info, ] if json_logs: diff --git a/server/models/mapping.py b/server/models/mapping.py index 41f4f27..dbc8503 100644 --- a/server/models/mapping.py +++ b/server/models/mapping.py @@ -1,8 +1,6 @@ from dataclasses import dataclass from enum import StrEnum -from server.models.ontology import Property - class MappingNodeType(StrEnum): ENTITY = "entity" @@ -10,6 +8,37 @@ class MappingNodeType(StrEnum): URIRef = "uri_ref" +@dataclass(kw_only=True) +class Position: + """ + A position in a mapping graph. + + Attributes: + x (int): The x-coordinate of the position + y (int): The y-coordinate of the position + """ + + x: int + y: int + + def to_dict(self): + return { + "x": self.x, + "y": self.y, + } + + @classmethod + def from_dict(cls, data): + if "x" not in data: + raise ValueError("x is required") + if "y" not in data: + raise ValueError("y is required") + return cls( + x=data["x"], + y=data["y"], + ) + + @dataclass(kw_only=True) class MappingNode: """ @@ -22,6 +51,7 @@ class MappingNode: uri_pattern (str): The URI pattern of the node rdf_type (list[str]): The RDF type/s of the node properties (list[str]): The properties of the node + position (Position): The position of the node """ id: str @@ -30,6 +60,7 @@ class MappingNode: uri_pattern: str rdf_type: list[str] properties: list[str] + position: Position def to_dict(self): return { @@ -39,6 +70,7 @@ def to_dict(self): "uri_pattern": self.uri_pattern, "rdf_type": self.rdf_type, "properties": self.properties, + "position": self.position.to_dict(), } @classmethod @@ -51,6 +83,8 @@ def from_dict(cls, data): raise ValueError("label is required") if "uri_pattern" not in data: raise ValueError("uri_pattern is required") + if "position" not in data: + raise ValueError("position is required") if "rdf_type" not in data: data["rdf_type"] = [] if "properties" not in data: @@ -62,6 +96,7 @@ def from_dict(cls, data): uri_pattern=data["uri_pattern"], rdf_type=data["rdf_type"], properties=data["properties"], + position=Position.from_dict(data["position"]), ) @@ -76,6 +111,7 @@ class MappingLiteral: label (str): The label of the literal value (str): The value of the literal literal_type (str): The type of the literal + position (Position): The position of the literal """ id: str @@ -83,6 +119,7 @@ class MappingLiteral: label: str value: str literal_type: str + position: Position def to_dict(self): return { @@ -91,6 +128,7 @@ def to_dict(self): "label": self.label, "value": self.value, "literal_type": self.literal_type, + "position": self.position.to_dict(), } @classmethod @@ -105,12 +143,15 @@ def from_dict(cls, data): raise ValueError("value is required") if "literal_type" not in data: raise ValueError("literal_type is required") + if "position" not in data: + raise ValueError("position is required") return cls( id=data["id"], type=data["type"], label=data["label"], value=data["value"], literal_type=data["literal_type"], + position=Position.from_dict(data["position"]), ) @@ -122,18 +163,21 @@ class MappingURIRef: Attributes: id (str): The ID of the URI reference type (MappingNodeType): The type of the URI reference - uri (str): The URI of the URI reference + uri_pattern (str): The URI pattern of the URI reference + position (Position): The position of the URI reference """ id: str type: MappingNodeType uri_pattern: str + position: Position def to_dict(self): return { "id": self.id, "type": self.type, "uri_pattern": self.uri_pattern, + "position": self.position.to_dict(), } @classmethod @@ -144,10 +188,13 @@ def from_dict(cls, data): raise ValueError("type is required") if "uri_pattern" not in data: raise ValueError("uri_pattern is required") + if "position" not in data: + raise ValueError("position is required") return cls( id=data["id"], type=data["type"], uri_pattern=data["uri_pattern"], + position=Position.from_dict(data["position"]), ) diff --git a/server/services/local/local_mapping_service.py b/server/services/local/local_mapping_service.py index 9769907..1776414 100644 --- a/server/services/local/local_mapping_service.py +++ b/server/services/local/local_mapping_service.py @@ -79,8 +79,11 @@ def update_mapping( self.logger.info(f"Updating mapping {mapping_id}") self._fs_service.upload_file( - mapping_id, - json.dumps(graph.to_dict()).encode("utf-8"), + name=mapping_id, + content=json.dumps(graph.to_dict()).encode( + "utf-8" + ), + uuid=mapping_id, allow_overwrite=True, )