Coverage for app/routers/link.py: 91%
174 statements
« prev ^ index » next coverage.py v7.9.2, created at 2026-02-19 12:47 +0000
« prev ^ index » next coverage.py v7.9.2, created at 2026-02-19 12:47 +0000
1from fastapi import APIRouter
2from fastapi import Depends, Path, Query, Body
3from fastapi.security import OAuth2PasswordBearer
4from neo4j import GraphDatabase, basic_auth
5from app.config.config import NEO4J_HOST, NEO4J_PORT, NEO4J_USER, NEO4J_PASSWORD, RESERVED_NODE_LABEL_NAMES
6from uuid import uuid4
7from loguru import logger
9from app.routers.links import attribute
10from app.utils.check_token import check_constellation_access, constellation_check_error
11from app.utils.send_sse_notification import send_constellation_notification
12from app.utils.response_format import generate_response, generate_error_response
13from app.utils.decode_ydoc import decode_attributes_inplace, DecodeType
14from app.utils.typing import Optional, List, JSONValue, serialize_value
16router = APIRouter()
18# Define the OAuth2 schema to get the JWT
19oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
21# Define the Neo4j driver
22driver = GraphDatabase.driver(
23 f"neo4j://{NEO4J_HOST}:{NEO4J_PORT}",
24 auth=basic_auth(NEO4J_USER, NEO4J_PASSWORD))
26router.include_router(attribute.router)
28# Get all links
29@router.get("/constellation/{constellation_uuid}/links",
30 summary="Get all links",
31 description="Get all links in the constellation.",
32 response_description="All links in the constellation",
33 responses={
34 200: {
35 "description": "All links successfully returned",
36 "content": {
37 "application/json": {
38 "example": {
39 "success": True,
40 "data": [
41 {
42 "start_node": 1,
43 "end_node": 2,
44 "type": "example_type",
45 "attributes": {
46 "link_uuid": 1
47 }
48 }
49 ],
50 "message": "All links successfully returned",
51 "error": None
52 }
53 }
54 }
55 },
56 **constellation_check_error,
57 },
58 tags=["Link Base"]
59)
60async def read_links(constellation_uuid: str = Path(..., description="The UUID of the constellation to get the links from"),
61 decode: Optional[DecodeType] = Query(default=None, description="Whether to decode the attributes or not. Options: 'xml', 'plain_text'"),
62 in_filter: Optional[List[str]] = Query(default=None, description="Filter to include only specific attributes in the final results"),
63 out_filter: Optional[List[str]] = Query(default=None, description="Filter to exclude specific attributes from the final results"),
64 token: str = Depends(oauth2_scheme)):
65 test = check_constellation_access(token, constellation_uuid, "READ")
66 if test is not None:
67 return test
69 if in_filter is None:
70 in_filter = []
71 if out_filter is None:
72 out_filter = []
74 with driver.session() as session:
75 result = session.run("""
76 MATCH (n {constellation_uuid: $constellation_uuid})-[r {constellation_uuid: $constellation_uuid}]-(m {constellation_uuid: $constellation_uuid})
77 WHERE ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(n)) AND ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(m))
78 RETURN n,m,r, properties(r) AS attributes
79 """,
80 constellation_uuid=constellation_uuid,
81 RESERVED_NODE_LABEL_NAMES=RESERVED_NODE_LABEL_NAMES
82 )
83 # get the node_uuid of the start and end nodes of the link and the type of the link
84 final_result: JSONValue = []
85 already_added_links: List[int] = []
86 for record in result:
87 if record["r"].id not in already_added_links:
88 already_added_links.append(record["r"].id)
89 if record["n"].id == record["r"].nodes[0].id:
90 final_result.append({"start_node": record["n"]["node_uuid"], "end_node": record["m"]["node_uuid"], "type": record["r"].type, "attributes": {
91 key: serialize_value(value)
92 for key, value in record["attributes"].items()
93 if (len(in_filter) == 0 or key in in_filter) and (len(out_filter) == 0 or key not in out_filter)
94 }})
95 else:
96 final_result.append({"start_node": record["m"]["node_uuid"], "end_node": record["n"]["node_uuid"], "type": record["r"].type, "attributes": {
97 key: serialize_value(value)
98 for key, value in record["attributes"].items()
99 if (len(in_filter) == 0 or key in in_filter) and (len(out_filter) == 0 or key not in out_filter)
100 }})
101 driver.close()
103 final_result = decode_attributes_inplace(final_result, decode)
105 return generate_response(
106 status_code=200,
107 data=final_result,
108 message="All links successfully returned"
109 )
111# Get a specific link (all link to/from a specific node)
112@router.get("/constellation/{constellation_uuid}/link/node/{node_uuid}",
113 summary="Get all links to/from a specific node",
114 description="Get all links to/from a specific node.",
115 response_description="All links to/from a specific node",
116 responses={
117 200: {
118 "description": "All links successfully returned",
119 "content": {
120 "application/json": {
121 "example": {
122 "success": True,
123 "data": [
124 {
125 "start_node": 1,
126 "end_node": 2,
127 "type": "example_type",
128 "attributes": {
129 "link_uuid": 1
130 }
131 }
132 ],
133 "message": "All links successfully returned",
134 "error": None
135 }
136 }
137 }
138 },
139 **constellation_check_error,
140 },
141 tags=["Link Base"]
142)
143async def read_link(constellation_uuid: str = Path(..., description="The UUID of the constellation to get the links from"),
144 node_uuid: str = Path(..., description="The UUID of the node to get the links from"),
145 decode: Optional[DecodeType] = Query(default=None, description="Whether to decode the attributes or not. Options: 'xml', 'plain_text'"),
146 in_filter: Optional[List[str]] = Query(default=None, description="Filter to include only specific attributes in the final results"),
147 out_filter: Optional[List[str]] = Query(default=None, description="Filter to exclude specific attributes from the final results"),
148 token: str = Depends(oauth2_scheme)):
149 test = check_constellation_access(token, constellation_uuid, "READ")
150 if test is not None:
151 return test
153 if in_filter is None:
154 in_filter = []
155 if out_filter is None:
156 out_filter = []
158 with driver.session() as session:
159 # Check if the node exists
160 result = session.run("""
161 MATCH (n {constellation_uuid: $constellation_uuid, node_uuid: $node_uuid})
162 WHERE ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(n))
163 RETURN n
164 """,
165 constellation_uuid=constellation_uuid,
166 node_uuid=node_uuid,
167 RESERVED_NODE_LABEL_NAMES=RESERVED_NODE_LABEL_NAMES
168 )
169 if len([record for record in result]) != 1:
170 return generate_error_response(
171 status_code=404,
172 error_code="NOT_FOUND",
173 error_message=f"Node {node_uuid} not found",
174 message=f"Node {node_uuid} not found"
175 )
177 result = session.run("""
178 MATCH (n {constellation_uuid: $constellation_uuid, node_uuid: $node_uuid})-[r {constellation_uuid: $constellation_uuid}]-(m {constellation_uuid: $constellation_uuid})
179 WHERE ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(n)) AND ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(m))
180 RETURN n,m,r, properties(r) AS attributes
181 """,
182 constellation_uuid=constellation_uuid,
183 node_uuid=node_uuid,
184 RESERVED_NODE_LABEL_NAMES=RESERVED_NODE_LABEL_NAMES
185 )
186 final_result: JSONValue = []
187 already_added_links: list[int] = []
188 for record in result:
189 if record["r"].id not in already_added_links:
190 already_added_links.append(record["r"].id)
191 if record["n"].id == record["r"].nodes[0].id:
192 final_result.append({"start_node": record["n"]["node_uuid"], "end_node": record["m"]["node_uuid"], "type": record["r"].type, "attributes": {
193 key: serialize_value(value)
194 for key, value in record["attributes"].items()
195 if (len(in_filter) == 0 or key in in_filter) and (len(out_filter) == 0 or key not in out_filter)
196 }})
197 else:
198 final_result.append({"start_node": record["m"]["node_uuid"], "end_node": record["n"]["node_uuid"], "type": record["r"].type, "attributes": {
199 key: serialize_value(value)
200 for key, value in record["attributes"].items()
201 if (len(in_filter) == 0 or key in in_filter) and (len(out_filter) == 0 or key not in out_filter)
202 }})
203 driver.close()
205 final_result = decode_attributes_inplace(final_result, decode)
207 return generate_response(
208 status_code=200,
209 data=final_result,
210 message="All links successfully returned"
211 )
213# Get a specific link by its UUID
214@router.get("/constellation/{constellation_uuid}/link/{link_uuid}",
215 summary="Get a link by UUID",
216 description="Get a link by its UUID.",
217 response_description="The link specified by its UUID",
218 responses={
219 200: {
220 "description": "Link successfully returned",
221 "content": {
222 "application/json": {
223 "example": {
224 "success": True,
225 "data": [
226 {
227 "start_node": 1,
228 "end_node": 2,
229 "type": "example_type",
230 "attributes": {
231 "link_uuid": 1
232 }
233 }
234 ],
235 "message": "Link successfully returned",
236 "error": None
237 }
238 }
239 }
240 },
241 **constellation_check_error,
242 },
243 tags=["Link Base"]
244)
245async def read_link_by_uuid(constellation_uuid: str = Path(..., description="The UUID of the constellation to get the link from"),
246 link_uuid: str = Path(..., description="The UUID of the link to get"),
247 decode: Optional[DecodeType] = Query(default=None, description="Whether to decode the attributes or not. Options: 'xml', 'plain_text'"),
248 in_filter: Optional[List[str]] = Query(default=None, description="Filter to include only specific attributes in the final results"),
249 out_filter: Optional[List[str]] = Query(default=None, description="Filter to exclude specific attributes from the final results"),
250 token: str = Depends(oauth2_scheme)):
251 test = check_constellation_access(token, constellation_uuid, "READ")
252 if test is not None:
253 return test
255 if in_filter is None:
256 in_filter = []
257 if out_filter is None:
258 out_filter = []
260 with driver.session() as session:
261 result = session.run("""
262 MATCH (n {constellation_uuid: $constellation_uuid})-[r {constellation_uuid: $constellation_uuid, link_uuid: $link_uuid}]->(m {constellation_uuid: $constellation_uuid})
263 WHERE ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(n)) AND ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(m))
264 RETURN r, n, m, properties(r) AS attributes
265 """,
266 constellation_uuid=constellation_uuid,
267 link_uuid=link_uuid,
268 RESERVED_NODE_LABEL_NAMES=RESERVED_NODE_LABEL_NAMES
269 )
270 final_result: JSONValue = [{"start_node": record["n"]["node_uuid"], "end_node": record["m"]["node_uuid"], "type": record["r"].type, "attributes": {
271 key: serialize_value(value)
272 for key, value in record["attributes"].items()
273 if (len(in_filter) == 0 or key in in_filter) and (len(out_filter) == 0 or key not in out_filter)
274 }} for record in result]
275 driver.close()
277 if len(final_result) == 0:
278 return generate_error_response(
279 status_code=404,
280 error_code="NOT_FOUND",
281 error_message=f"Link {link_uuid} not found",
282 message=f"Link {link_uuid} not found"
283 )
285 final_result = decode_attributes_inplace(final_result, decode)
287 return generate_response(
288 status_code=200,
289 data=final_result,
290 message="Link successfully returned"
291 )
293# Create a new link
294@router.post("/constellation/{constellation_uuid}/link",
295 summary="Create a new link",
296 description="Create a new link between two nodes.",
297 response_description="The new link",
298 responses={
299 200: {
300 "description": "Link successfully created",
301 "content": {
302 "application/json": {
303 "example": {
304 "success": True,
305 "data": [
306 {
307 "start_node": 1,
308 "end_node": 2,
309 "type": "example_type",
310 "attributes": {
311 "link_uuid": 1
312 }
313 }
314 ],
315 "message": "Link successfully created",
316 "error": None
317 }
318 }
319 }
320 },
321 **constellation_check_error,
322 },
323 tags=["Link Base"]
324)
325async def create_link(constellation_uuid: str = Path(..., description="The UUID of the constellation to create the link in"),
326 start_node: str = Body(..., description="The UUID of the start node"),
327 end_node: str = Body(..., description="The UUID of the end node"),
328 link_type: str = Body(..., description="The type of the link"),
329 decode: Optional[DecodeType] = Query(default=None, description="Whether to decode the attributes or not. Options: 'xml', 'plain_text'"),
330 in_filter: Optional[List[str]] = Query(default=None, description="Filter to include only specific attributes in the final results"),
331 out_filter: Optional[List[str]] = Query(default=None, description="Filter to exclude specific attributes from the final results"),
332 token: str = Depends(oauth2_scheme)):
333 test = check_constellation_access(token, constellation_uuid, "WRITE")
334 if test is not None:
335 return test
337 if in_filter is None:
338 in_filter = []
339 if out_filter is None:
340 out_filter = []
342 if link_type == "":
343 return generate_error_response(
344 status_code=400,
345 error_code="BAD_REQUEST",
346 error_message="Link type cannot be empty",
347 message="Link type cannot be empty"
348 )
350 link_type = link_type.replace("`", "``") # Replace backticks with double backticks for Neo4j compatibility
352 with driver.session() as session:
353 # Check if n and m exists
354 result = session.run("""
355 MATCH (n {constellation_uuid: $constellation_uuid, node_uuid: $start_node}), (m {constellation_uuid: $constellation_uuid, node_uuid: $end_node})
356 WHERE ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(n)) AND ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(m))
357 RETURN n, m
358 """,
359 constellation_uuid=constellation_uuid,
360 start_node=start_node,
361 end_node=end_node,
362 RESERVED_NODE_LABEL_NAMES=RESERVED_NODE_LABEL_NAMES
363 )
364 records = [record for record in result]
365 if len(records) == 0 or records[0]["n"] is None or records[0]["m"] is None:
366 return generate_error_response(
367 status_code=404,
368 error_code="NOT_FOUND",
369 error_message=f"Node {start_node} or Node {end_node} not found",
370 message=f"Node {start_node} or Node {end_node} not found"
371 )
373 # Check if the link already exists
374 existing_links = session.run("""
375 MATCH (n {constellation_uuid: $constellation_uuid, node_uuid: $start_node})-[r]->(m {constellation_uuid: $constellation_uuid, node_uuid: $end_node})
376 WHERE ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(n)) AND ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(m))
377 RETURN r
378 """,
379 constellation_uuid=constellation_uuid,
380 start_node=start_node,
381 end_node=end_node,
382 RESERVED_NODE_LABEL_NAMES=RESERVED_NODE_LABEL_NAMES
383 )
384 if existing_links:
385 for link in existing_links:
386 if link["r"].type == link_type:
387 return generate_error_response(
388 status_code=400,
389 error_code="LINK_ALREADY_EXISTS",
390 error_message=f"Link {link_type} already exists between the two nodes",
391 message=f"Link {link_type} already exists between the two nodes"
392 )
394 # Create the link
395 result = session.run("""
396 MATCH (n {constellation_uuid: $constellation_uuid, node_uuid: $start_node}), (m {constellation_uuid: $constellation_uuid, node_uuid: $end_node})
397 WHERE ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(n)) AND ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(m))
398 CREATE (n)-[r:$($link_type) {constellation_uuid: $constellation_uuid, link_uuid: $link_uuid}]->(m)
399 SET r.created_at = datetime()
400 SET r.updated_at = datetime()
401 SET r.word_count = 0
402 SET n.link_count = COUNT { (n)--() }
403 SET m.link_count = CASE
404 WHEN n = m THEN n.link_count
405 ELSE COUNT { (m)--() }
406 END
407 RETURN n,m,r, properties(r) AS attributes
408 """,
409 constellation_uuid=constellation_uuid,
410 start_node=start_node,
411 end_node=end_node,
412 RESERVED_NODE_LABEL_NAMES=RESERVED_NODE_LABEL_NAMES,
413 link_type=link_type,
414 link_uuid=str(uuid4()),
415 )
416 final_result: JSONValue = [{"start_node": record["n"]["node_uuid"], "end_node": record["m"]["node_uuid"], "type": record["r"].type, "attributes": {
417 key: serialize_value(value)
418 for key, value in record["attributes"].items()
419 if (len(in_filter) == 0 or key in in_filter) and (len(out_filter) == 0 or key not in out_filter)
420 }} for record in result]
421 driver.close()
423 final_result = decode_attributes_inplace(final_result, decode)
425 # Send a notification to the SSE server
426 notification_result = send_constellation_notification(
427 constellation_uuid=constellation_uuid,
428 data=final_result,
429 message="Link created",
430 data_type="LINK_CREATED",
431 token=token
432 )
433 if not notification_result:
434 logger.warning("Create link: Error sending notification to SSE server")
436 return generate_response(
437 status_code=200,
438 data=final_result,
439 message="Link successfully created"
440 )
442# Delete a link
443@router.delete("/constellation/{constellation_uuid}/link/{link_uuid}",
444 summary="Delete a link",
445 description="Delete a link between two nodes.",
446 response_description="The deleted link",
447 responses={
448 200: {
449 "description": "Link successfully deleted",
450 "content": {
451 "application/json": {
452 "example": {
453 "success": True,
454 "data": None,
455 "message": "Link successfully deleted",
456 "error": None
457 }
458 }
459 }
460 },
461 **constellation_check_error,
462 },
463 tags=["Link Base"]
464)
465async def delete_link(constellation_uuid: str = Path(..., description="The UUID of the constellation to delete the link in"),
466 link_uuid: str = Path(..., description="The UUID of the link to delete"),
467 token: str = Depends(oauth2_scheme)):
468 test = check_constellation_access(token, constellation_uuid, "WRITE")
469 if test is not None:
470 return test
472 with driver.session() as session:
473 # Check if the link exists
474 result = session.run("""
475 MATCH (n)-[r {constellation_uuid: $constellation_uuid, link_uuid: $link_uuid}]->(m)
476 WHERE ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(n)) AND ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(m))
477 RETURN n,m,r
478 """,
479 constellation_uuid=constellation_uuid,
480 link_uuid=link_uuid,
481 RESERVED_NODE_LABEL_NAMES=RESERVED_NODE_LABEL_NAMES
482 )
483 if len([record for record in result]) != 1:
484 return generate_error_response(
485 status_code=404,
486 error_code="NOT_FOUND",
487 error_message="Link not found",
488 message="Link not found"
489 )
491 result = session.run("""
492 MATCH (n)-[r {constellation_uuid: $constellation_uuid, link_uuid: $link_uuid}]->(m)
493 WHERE ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(n)) AND ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(m))
494 DELETE r
495 WITH n, m
496 SET n.link_count = COUNT { (n)--() }
497 SET m.link_count = CASE
498 WHEN n = m THEN n.link_count
499 ELSE COUNT { (m)--() }
500 END
501 """,
502 constellation_uuid=constellation_uuid,
503 link_uuid=link_uuid,
504 RESERVED_NODE_LABEL_NAMES=RESERVED_NODE_LABEL_NAMES
505 )
506 driver.close()
508 # Send a notification to the SSE server
509 notification_result = send_constellation_notification(
510 constellation_uuid=constellation_uuid,
511 data={
512 "link_uuid": link_uuid,
513 "message": "Link deleted successfully"
514 },
515 message="Link deleted",
516 data_type="LINK_DELETED",
517 token=token
518 )
519 if not notification_result:
520 logger.warning("Delete link: Error sending notification to SSE server")
522 return generate_response(
523 status_code=200,
524 data=None,
525 message="Link successfully deleted"
526 )
528# Update a link
529@router.patch("/constellation/{constellation_uuid}/link/{link_uuid}",
530 summary="Update a link",
531 description="Update a link between two nodes.",
532 response_description="The updated link",
533 responses={
534 200: {
535 "description": "Link successfully updated",
536 "content": {
537 "application/json": {
538 "example": {
539 "success": True,
540 "data": [
541 {
542 "start_node": 1,
543 "end_node": 2,
544 "type": "example_type",
545 "attributes": {
546 "link_uuid": 1
547 }
548 }
549 ],
550 "message": "Link successfully updated",
551 "error": None
552 }
553 }
554 }
555 },
556 **constellation_check_error,
557 },
558 tags=["Link Base"]
559)
560async def update_link(constellation_uuid: str = Path(..., description="The UUID of the constellation to update the link in"),
561 link_uuid: str = Path(..., description="The UUID of the link to update"),
562 new_start_node: Optional[str] = Body(None, description="The new UUID of the start node (optional)"),
563 new_end_node: Optional[str] = Body(None, description="The new UUID of the end node (optional)"),
564 new_link_type: Optional[str] = Body(None, description="The new type of the link (optional)"),
565 decode: Optional[DecodeType] = Query(default=None, description="Whether to decode the attributes or not. Options: 'xml', 'plain_text'"),
566 in_filter: Optional[List[str]] = Query(default=None, description="Filter to include only specific attributes in the final results"),
567 out_filter: Optional[List[str]] = Query(default=None, description="Filter to exclude specific attributes from the final results"),
568 token: str = Depends(oauth2_scheme)):
569 test = check_constellation_access(token, constellation_uuid, "WRITE")
570 if test is not None:
571 return test
573 if in_filter is None:
574 in_filter = []
575 if out_filter is None:
576 out_filter = []
578 if (new_start_node is None or new_start_node == "") and \
579 (new_end_node is None or new_end_node == "") and \
580 (new_link_type is None or new_link_type == ""):
581 return generate_error_response(
582 status_code=400,
583 error_code="BAD_REQUEST",
584 error_message="No changes provided",
585 message="No changes provided"
586 )
588 # Delete the old link and create a new one
589 with driver.session() as session:
590 # Check if the link exists
591 result = session.run("""
592 MATCH (n)-[r {constellation_uuid: $constellation_uuid, link_uuid: $link_uuid}]->(m)
593 WHERE ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(n)) AND ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(m))
594 RETURN n,m,r, properties(r) AS attributes
595 """,
596 constellation_uuid=constellation_uuid,
597 link_uuid=link_uuid,
598 RESERVED_NODE_LABEL_NAMES=RESERVED_NODE_LABEL_NAMES
599 )
600 check_result = [record for record in result]
601 if len(check_result) == 0:
602 return generate_error_response(
603 status_code=404,
604 error_code="NOT_FOUND",
605 error_message="Link not found",
606 message="Link not found"
607 )
609 start_node = check_result[0]["n"]["node_uuid"]
610 end_node = check_result[0]["m"]["node_uuid"]
611 link_type = check_result[0]["r"].type
612 link_attributes = check_result[0]["attributes"]
614 logger.debug(f"Old link data: start_node={start_node}, end_node={end_node}, link_type={link_type}, attributes={link_attributes}")
616 # Check if the new start and end nodes exist and if they don't already have a link of the same type
617 check_start_node = start_node if new_start_node is None or new_start_node == "" else new_start_node
618 check_end_node = end_node if new_end_node is None or new_end_node == "" else new_end_node
619 check_link_type = link_type if new_link_type is None or new_link_type == "" else new_link_type
621 # Check the start and end nodes exist
622 result = session.run("""
623 MATCH (n {constellation_uuid: $constellation_uuid, node_uuid: $start_node}), (m {constellation_uuid: $constellation_uuid, node_uuid: $end_node})
624 WHERE ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(n)) AND ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(m))
625 RETURN n, m
626 """,
627 constellation_uuid=constellation_uuid,
628 start_node=check_start_node,
629 end_node=check_end_node,
630 RESERVED_NODE_LABEL_NAMES=RESERVED_NODE_LABEL_NAMES
631 )
632 records = [record for record in result]
633 if len(records) == 0 or records[0]["n"] is None or records[0]["m"] is None:
634 return generate_error_response(
635 status_code=404,
636 error_code="NODE_NOT_FOUND",
637 error_message=f"Node {check_start_node} or Node {check_end_node} not found",
638 message=f"Node {check_start_node} or Node {check_end_node} not found"
639 )
640 # Check if the link already exists
641 existing_links = session.run("""
642 MATCH (n {constellation_uuid: $constellation_uuid, node_uuid: $start_node})-[r]->(m {constellation_uuid: $constellation_uuid, node_uuid: $end_node})
643 WHERE ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(n)) AND ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(m))
644 RETURN r
645 """,
646 constellation_uuid=constellation_uuid,
647 start_node=check_start_node,
648 end_node=check_end_node,
649 RESERVED_NODE_LABEL_NAMES=RESERVED_NODE_LABEL_NAMES
650 )
651 if existing_links:
652 for link in existing_links:
653 if link["r"].type == check_link_type and link["r"]["link_uuid"] != link_uuid:
654 return generate_error_response(
655 status_code=400,
656 error_code="LINK_ALREADY_EXISTS",
657 error_message=f"Link {check_link_type} already exists between the two nodes",
658 message=f"Link {check_link_type} already exists between the two nodes"
659 )
661 # Delete the link
662 result = session.run("""
663 MATCH (n)-[r {constellation_uuid: $constellation_uuid, link_uuid: $link_uuid}]->(m)
664 WHERE ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(n)) AND ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(m))
665 DELETE r
666 WITH n, m
667 SET n.link_count = COUNT { (n)--() }
668 SET m.link_count = CASE
669 WHEN n = m THEN n.link_count
670 ELSE COUNT { (m)--() }
671 END
672 """,
673 constellation_uuid=constellation_uuid,
674 link_uuid=link_uuid,
675 RESERVED_NODE_LABEL_NAMES=RESERVED_NODE_LABEL_NAMES
676 )
678 if new_start_node is None or new_start_node == "":
679 new_start_node = start_node
680 if new_end_node is None or new_end_node == "":
681 new_end_node = end_node
682 if new_link_type is None or new_link_type == "":
683 new_link_type = link_type
685 # Recreate the link with the new data
686 result = session.run("""
687 MATCH (n {constellation_uuid: $constellation_uuid, node_uuid: $start_node}), (m {constellation_uuid: $constellation_uuid, node_uuid: $end_node})
688 WHERE ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(n)) AND ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(m))
689 CREATE (n)-[r:$($new_link_type) $link_attributes]->(m)
690 SET r.created_at = coalesce(r.created_at, datetime())
691 SET r.updated_at = datetime()
692 SET n.link_count = COUNT { (n)--() }
693 SET m.link_count = CASE
694 WHEN n = m THEN n.link_count
695 ELSE COUNT { (m)--() }
696 END
697 RETURN n,m,r, properties(r) AS attributes
698 """,
699 constellation_uuid=constellation_uuid,
700 start_node=new_start_node,
701 end_node=new_end_node,
702 RESERVED_NODE_LABEL_NAMES=RESERVED_NODE_LABEL_NAMES,
703 new_link_type=new_link_type,
704 link_attributes=link_attributes,
705 )
706 final_result: JSONValue = [{"start_node": record["n"]["node_uuid"], "end_node": record["m"]["node_uuid"], "type": record["r"].type, "attributes": {
707 key: serialize_value(value)
708 for key, value in record["attributes"].items()
709 if (len(in_filter) == 0 or key in in_filter) and (len(out_filter) == 0 or key not in out_filter)
710 }} for record in result]
711 driver.close()
713 final_result = decode_attributes_inplace(final_result, decode)
715 # Send a notification to the SSE server
716 notification_result = send_constellation_notification(
717 constellation_uuid=constellation_uuid,
718 data=final_result,
719 message="Link updated",
720 data_type="LINK_UPDATED",
721 token=token
722 )
723 if not notification_result:
724 logger.warning("Update link: Error sending notification to SSE server")
726 return generate_response(
727 status_code=200,
728 data=final_result,
729 message="Link successfully updated"
730 )