Coverage for app/routers/nodes/attribute.py: 85%
186 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 app.config.config import NEO4J_HOST, NEO4J_PORT, NEO4J_USER, NEO4J_PASSWORD, RESERVED_NODE_ATTRIBUTE_NAMES, RESERVED_NODE_LABEL_NAMES
2from fastapi import APIRouter
3from fastapi import Depends, Path, Query, Body
4from fastapi.security import OAuth2PasswordBearer
5from neo4j import GraphDatabase, basic_auth
6from loguru import logger
8from app.utils.check_token import check_constellation_access, constellation_check_error
9from app.utils.send_sse_notification import send_constellation_notification
10from app.utils.response_format import generate_response, generate_error_response
11from app.utils.decode_ydoc import decode_ydoc, DecodeType, decode_attributes_inplace
12from app.utils.typing import Optional, List, JSONValue, serialize_value
14router = APIRouter(tags=["Node Attribute"])
16# Define the OAuth2 schema to get the JWT
17oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
19# Define the Neo4j driver
20driver = GraphDatabase.driver(
21 f"neo4j://{NEO4J_HOST}:{NEO4J_PORT}",
22 auth=basic_auth(NEO4J_USER, NEO4J_PASSWORD))
24# Get all attributes
25@router.get("/constellation/{constellation_uuid}/attributes",
26 summary="Get all attributes in the constellation",
27 description="Get all attributes in the constellation specified by its UUID",
28 response_description="List of attributes",
29 responses={
30 200: {
31 "description": "List of attributes",
32 "content": {
33 "application/json": {
34 "example": {
35 "success": True,
36 "data": [
37 "attribute1",
38 "attribute2",
39 "attribute3"
40 ],
41 "message": "Attributes retrieved",
42 "error": None
43 }
44 }
45 }
46 },
47 **constellation_check_error
48 }
49)
50async def read_attributes(constellation_uuid: str = Path(..., title="The UUID of the constellation"),
51 token: str = Depends(oauth2_scheme)):
52 test = check_constellation_access(token, constellation_uuid, "READ")
53 if test is not None:
54 return test
56 with driver.session() as session:
57 # Get all atributes of the nodes and links from the current constellation
58 result = session.run("""
59 MATCH (n {constellation_uuid: $constellation_uuid})-[r {constellation_uuid: $constellation_uuid}]-()
60 WHERE ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(n))
61 UNWIND keys(n) + keys(r) AS propertyKey
62 RETURN DISTINCT propertyKey
63 """,
64 constellation_uuid=constellation_uuid,
65 RESERVED_NODE_LABEL_NAMES=RESERVED_NODE_LABEL_NAMES
66 )
67 final_result: JSONValue = [record["propertyKey"] for record in result]
68 driver.close()
70 return generate_response(
71 status_code=200,
72 data=final_result,
73 message="Attributes retrieved"
74 )
76# Get all attributes of a specific node
77@router.get("/constellation/{constellation_uuid}/node/{node_uuid}/attributes",
78 summary="Get all attributes of a node",
79 description="Get all attributes of the node specified by its UUID",
80 response_description="List of attributes",
81 responses={
82 200: {
83 "description": "List of attributes",
84 "content": {
85 "application/json": {
86 "example": {
87 "success": True,
88 "data": [
89 "attribute1",
90 "attribute2",
91 "attribute3"
92 ],
93 "message": "Attributes retrieved",
94 "error": None
95 }
96 }
97 }
98 },
99 **constellation_check_error
100 }
101)
102async def read_node_attributes(constellation_uuid: str = Path(..., title="The UUID of the constellation"),
103 node_uuid: str = Path(..., title="The UUID of the node"),
104 token: str = Depends(oauth2_scheme)):
105 test = check_constellation_access(token, constellation_uuid, "READ")
106 if test is not None:
107 return test
109 with driver.session() as session:
110 # check if the node exists
111 result = session.run("""
112 MATCH (n {constellation_uuid: $constellation_uuid, node_uuid: $node_uuid})
113 WHERE ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(n))
114 RETURN n
115 """,
116 constellation_uuid=constellation_uuid,
117 node_uuid=node_uuid,
118 RESERVED_NODE_LABEL_NAMES=RESERVED_NODE_LABEL_NAMES
119 )
120 if len(result.data()) == 0:
121 return generate_error_response(
122 status_code=404,
123 error_code="NOT_FOUND",
124 error_message=f"Node {node_uuid} not found",
125 message=f"Node {node_uuid} not found"
126 )
128 result = session.run("""
129 MATCH (n {constellation_uuid: $constellation_uuid, node_uuid: $node_uuid})
130 WHERE ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(n))
131 RETURN keys(n)
132 """,
133 constellation_uuid=constellation_uuid,
134 node_uuid=node_uuid,
135 RESERVED_NODE_LABEL_NAMES=RESERVED_NODE_LABEL_NAMES
136 )
137 final_result: JSONValue = [record["keys(n)"] for record in result]
138 driver.close()
140 return generate_response(
141 status_code=200,
142 data=final_result,
143 message="Attributes retrieved"
144 )
146# Get the value of a specific attribute of a specific node
147@router.get("/constellation/{constellation_uuid}/node/{node_uuid}/attribute/{attribute}",
148 summary="Get the value of a specific attribute of a node",
149 description="Get the value of a specific attribute of the node specified by its UUID",
150 response_description="Value of the attribute",
151 responses={
152 200: {
153 "description": "Value of the attribute",
154 "content": {
155 "application/json": {
156 "example": {
157 "success": True,
158 "data": [
159 "value"
160 ],
161 "message": "Attribute retrieved",
162 "error": None
163 }
164 }
165 }
166 },
167 **constellation_check_error
168 }
169)
170async def read_node_attribute(constellation_uuid: str = Path(..., title="The UUID of the constellation"),
171 node_uuid: str = Path(..., title="The UUID of the node"),
172 attribute: str = Path(..., title="The attribute to get the value of"),
173 decode: Optional[DecodeType] = Query(default=None, description="Whether to decode the attributes or not. Options: 'xml', 'plain_text'"),
174 token: str = Depends(oauth2_scheme)):
175 test = check_constellation_access(token, constellation_uuid, "READ")
176 if test is not None:
177 return test
179 with driver.session() as session:
180 # check if the node exists
181 result = session.run("""
182 MATCH (n {constellation_uuid: $constellation_uuid, node_uuid: $node_uuid})
183 WHERE ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(n))
184 RETURN n
185 """,
186 constellation_uuid=constellation_uuid,
187 node_uuid=node_uuid,
188 RESERVED_NODE_LABEL_NAMES=RESERVED_NODE_LABEL_NAMES
189 )
190 if len(result.data()) == 0:
191 return generate_error_response(
192 status_code=404,
193 error_code="NOT_FOUND",
194 error_message=f"Node {node_uuid} not found",
195 message=f"Node {node_uuid} not found"
196 )
198 attribute = attribute.replace("`", "``") # Replace backticks with double backticks for Neo4j compatibility
200 result = session.run("""
201 MATCH (n {constellation_uuid: $constellation_uuid, node_uuid: $node_uuid})
202 WHERE ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(n))
203 RETURN n[$attribute] AS attribute
204 """,
205 constellation_uuid=constellation_uuid,
206 node_uuid=node_uuid,
207 RESERVED_NODE_LABEL_NAMES=RESERVED_NODE_LABEL_NAMES,
208 attribute=attribute
209 )
210 final_result: JSONValue = [serialize_value(record["attribute"]) for record in result]
211 driver.close()
213 if final_result[0] is None:
214 return generate_error_response(
215 status_code=404,
216 error_code="NOT_FOUND",
217 error_message=f"Attribute {attribute} not found for node {node_uuid}",
218 message=f"Attribute {attribute} not found for node {node_uuid}"
219 )
221 if decode:
222 # Decode the attributes if requested
223 final_result = [decode_ydoc(value, decode == DecodeType.PLAIN_TEXT) if isinstance(value, str) else value for value in final_result]
225 return generate_response(
226 status_code=200,
227 data=final_result,
228 message="Attribute retrieved"
229 )
231# Set the value of a specific attribute of a specific node
232@router.patch("/constellation/{constellation_uuid}/node/{node_uuid}/attribute/{attribute}",
233 summary="Set the value of a specific attribute of a node",
234 description="Set the value of a specific attribute of the node specified by its UUID",
235 response_description="Node with the updated attribute",
236 responses={
237 200: {
238 "description": "Node with the updated attribute",
239 "content": {
240 "application/json": {
241 "example": {
242 "success": True,
243 "data": [
244 {
245 "attributes": {
246 "node_uuid": 1,
247 "title": "example_title",
248 "content": "example_content"
249 },
250 "labels": [
251 "Node"
252 ]
253 }
254 ],
255 "message": "Attribute updated",
256 "error": None
257 }
258 }
259 }
260 },
261 **constellation_check_error
262 }
263)
264async def set_node_attribute(constellation_uuid: str = Path(..., title="The UUID of the constellation"),
265 node_uuid: str = Path(..., title="The UUID of the node"),
266 attribute: str = Path(..., title="The attribute to set the value of"),
267 value: str = Body(..., title="The value to set", embed=True),
268 decode: Optional[DecodeType] = Query(default=None, description="Whether to decode the attributes or not. Options: 'xml', 'plain_text'"),
269 in_filter: Optional[List[str]] = Query(default=None, description="Filter to include only specific attributes in the final results"),
270 out_filter: Optional[List[str]] = Query(default=None, description="Filter to exclude specific attributes from the final results"),
271 token: str = Depends(oauth2_scheme)):
272 test = check_constellation_access(token, constellation_uuid, "WRITE")
273 if test is not None:
274 return test
276 if in_filter is None:
277 in_filter = []
278 if out_filter is None:
279 out_filter = []
281 # Validate the attribute name
282 if attribute in RESERVED_NODE_ATTRIBUTE_NAMES:
283 return generate_error_response(
284 status_code=400,
285 error_code="BAD_REQUEST",
286 error_message=f"Attribute {attribute} cannot be set",
287 message=f"Attribute {attribute} cannot be set"
288 )
290 with driver.session() as session:
291 # check if the node exists
292 result = session.run("""
293 MATCH (n {constellation_uuid: $constellation_uuid, node_uuid: $node_uuid})
294 WHERE ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(n))
295 RETURN n
296 """,
297 constellation_uuid=constellation_uuid,
298 node_uuid=node_uuid,
299 RESERVED_NODE_LABEL_NAMES=RESERVED_NODE_LABEL_NAMES
300 )
301 if len(result.data()) == 0:
302 return generate_error_response(
303 status_code=404,
304 error_code="NOT_FOUND",
305 error_message=f"Node {node_uuid} not found",
306 message=f"Node {node_uuid} not found"
307 )
309 attribute = attribute.replace("`", "``") # Replace backticks with double backticks for Neo4j compatibility
311 # Check if the attribute exists
312 result = session.run("""
313 MATCH (n {constellation_uuid: $constellation_uuid, node_uuid: $node_uuid})
314 WHERE ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(n))
315 RETURN n[$attribute] AS attribute, coalesce(n.word_count, -1) AS word_count, properties(n) AS currrentAttributes
316 """,
317 constellation_uuid=constellation_uuid,
318 node_uuid=node_uuid,
319 RESERVED_NODE_LABEL_NAMES=RESERVED_NODE_LABEL_NAMES,
320 attribute=attribute
321 )
322 records = [record for record in result]
323 oldAttributeValue = [record["attribute"] for record in records][0]
324 if oldAttributeValue is None:
325 return generate_error_response(
326 status_code=404,
327 error_code="NOT_FOUND",
328 error_message=f"Attribute {attribute} not found for node {node_uuid}",
329 message=f"Attribute {attribute} not found for node {node_uuid}"
330 )
332 # Compute the new word count
333 currentWordCount = [record["word_count"] for record in records][0]
334 currentAttributes = [record["currrentAttributes"] for record in records][0]
336 if currentWordCount == -1: # word_count has not been computed yet
337 currentWordCount = 0
338 for key in currentAttributes.keys(): # Add all current attributes word count
339 if key not in RESERVED_NODE_ATTRIBUTE_NAMES:
340 currentWordCount += len(decode_ydoc(currentAttributes[key], True).split())
342 oldAttributeValueWordCount = len(decode_ydoc(oldAttributeValue, True).split()) if isinstance(oldAttributeValue, str) else 0
343 newAttributeValueWordCount = len(decode_ydoc(value, True).split())
344 wordCountDifference = newAttributeValueWordCount - oldAttributeValueWordCount
345 currentWordCount += wordCountDifference
347 result = session.run("""
348 MATCH (n {constellation_uuid: $constellation_uuid, node_uuid: $node_uuid})
349 WHERE ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(n))
350 SET n.created_at = coalesce(n.created_at, datetime())
351 SET n.updated_at = datetime()
352 SET n.word_count = $word_count
353 SET n[$attribute] = $value
354 RETURN properties(n) AS attributes, labels(n) as labels
355 """,
356 constellation_uuid=constellation_uuid,
357 node_uuid=node_uuid,
358 RESERVED_NODE_LABEL_NAMES=RESERVED_NODE_LABEL_NAMES,
359 word_count=currentWordCount,
360 attribute=attribute,
361 value=value
362 )
363 final_result: JSONValue = [{"attributes": {
364 key: serialize_value(value)
365 for key, value in record["attributes"].items()
366 if (len(in_filter) == 0 or key in in_filter) and (len(out_filter) == 0 or key not in out_filter)
367 }, "labels": record["labels"]} for record in result]
368 driver.close()
370 final_result = decode_attributes_inplace(final_result, decode)
372 # Send a notification to the SSE server
373 notification_result = send_constellation_notification(
374 constellation_uuid=constellation_uuid,
375 data=final_result,
376 message="Node attribute updated",
377 data_type="NODE_ATTRIBUTE_UPDATED",
378 token=token
379 )
380 if not notification_result:
381 logger.warning("Set node attribute: Error sending notification to SSE server")
383 return generate_response(
384 status_code=200,
385 data=final_result,
386 message="Attribute updated"
387 )
389# Delete a specific attribute of a specific node
390@router.delete("/constellation/{constellation_uuid}/node/{node_uuid}/attribute/{attribute}",
391 summary="Delete a specific attribute of a node",
392 description="Delete a specific attribute of the node specified by its UUID",
393 response_description="Node without the deleted attribute",
394 responses={
395 200: {
396 "description": "Node without the deleted attribute",
397 "content": {
398 "application/json": {
399 "example": {
400 "success": True,
401 "data": [
402 {
403 "attributes": {
404 "node_uuid": 1,
405 "title": "example_title",
406 "content": "example_content"
407 },
408 "labels": [
409 "Node"
410 ]
411 }
412 ],
413 "message": "Attribute deleted",
414 "error": None
415 }
416 }
417 }
418 },
419 **constellation_check_error
420 }
421)
422async def delete_node_attribute(constellation_uuid: str = Path(..., title="The UUID of the constellation"),
423 node_uuid: str = Path(..., title="The UUID of the node"),
424 attribute: str = Path(..., title="The attribute to delete"),
425 decode: Optional[DecodeType] = Query(default=None, description="Whether to decode the attributes or not. Options: 'xml', 'plain_text'"),
426 in_filter: Optional[List[str]] = Query(default=None, description="Filter to include only specific attributes in the final results"),
427 out_filter: Optional[List[str]] = Query(default=None, description="Filter to exclude specific attributes from the final results"),
428 token: str = Depends(oauth2_scheme)):
429 test = check_constellation_access(token, constellation_uuid, "WRITE")
430 if test is not None:
431 return test
433 if in_filter is None:
434 in_filter = []
435 if out_filter is None:
436 out_filter = []
438 # Validate the attribute name
439 if attribute in RESERVED_NODE_ATTRIBUTE_NAMES:
440 return generate_error_response(
441 status_code=400,
442 error_code="BAD_REQUEST",
443 error_message=f"Attribute {attribute} cannot be deleted",
444 message=f"Attribute {attribute} cannot be deleted"
445 )
447 with driver.session() as session:
448 # check if the node exists
449 result = session.run("""
450 MATCH (n {constellation_uuid: $constellation_uuid, node_uuid: $node_uuid})
451 WHERE ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(n))
452 RETURN n
453 """,
454 constellation_uuid=constellation_uuid,
455 node_uuid=node_uuid,
456 RESERVED_NODE_LABEL_NAMES=RESERVED_NODE_LABEL_NAMES
457 )
458 if len(result.data()) == 0:
459 return generate_error_response(
460 status_code=404,
461 error_code="NOT_FOUND",
462 error_message=f"Node {node_uuid} not found",
463 message=f"Node {node_uuid} not found"
464 )
466 attribute = attribute.replace("`", "``") # Replace backticks with double backticks for Neo4j compatibility
468 # Check if the attribute exists
469 result = session.run("""
470 MATCH (n {constellation_uuid: $constellation_uuid, node_uuid: $node_uuid})
471 WHERE ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(n))
472 RETURN n[$attribute] AS attribute, coalesce(n.word_count, -1) AS word_count, properties(n) AS currrentAttributes
473 """,
474 constellation_uuid=constellation_uuid,
475 node_uuid=node_uuid,
476 RESERVED_NODE_LABEL_NAMES=RESERVED_NODE_LABEL_NAMES,
477 attribute=attribute
478 )
479 records = [record for record in result]
480 oldAttributeValue = [record["attribute"] for record in records][0]
481 if oldAttributeValue is None:
482 return generate_error_response(
483 status_code=404,
484 error_code="NOT_FOUND",
485 error_message=f"Attribute {attribute} not found for node {node_uuid}",
486 message=f"Attribute {attribute} not found for node {node_uuid}"
487 )
489 # Compute the new word count
490 currentWordCount = [record["word_count"] for record in records][0]
491 currentAttributes = [record["currrentAttributes"] for record in records][0]
493 if currentWordCount == -1: # word_count has not been computed yet
494 currentWordCount = 0
495 for key in currentAttributes.keys(): # Add all current attributes word count
496 if key not in RESERVED_NODE_ATTRIBUTE_NAMES:
497 currentWordCount += len(decode_ydoc(currentAttributes[key], True).split())
499 oldAttributeValueWordCount = len(decode_ydoc(oldAttributeValue, True).split()) if isinstance(oldAttributeValue, str) else 0
500 currentWordCount -= oldAttributeValueWordCount
502 result = session.run("""
503 MATCH (n {constellation_uuid: $constellation_uuid, node_uuid: $node_uuid})
504 WHERE ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(n))
505 SET n.created_at = coalesce(n.created_at, datetime())
506 SET n.updated_at = datetime()
507 SET n.word_count = $word_count
508 REMOVE n[$attribute]
509 RETURN properties(n) AS attributes, labels(n) as labels
510 """,
511 constellation_uuid=constellation_uuid,
512 node_uuid=node_uuid,
513 RESERVED_NODE_LABEL_NAMES=RESERVED_NODE_LABEL_NAMES,
514 word_count=currentWordCount,
515 attribute=attribute
516 )
517 final_result: JSONValue = [{"attributes": {
518 key: serialize_value(value)
519 for key, value in record["attributes"].items()
520 if (len(in_filter) == 0 or key in in_filter) and (len(out_filter) == 0 or key not in out_filter)
521 }, "labels": record["labels"]} for record in result]
522 driver.close()
524 final_result = decode_attributes_inplace(final_result, decode)
526 # Send a notification to the SSE server
527 notification_result = send_constellation_notification(
528 constellation_uuid=constellation_uuid,
529 data=final_result,
530 message="Node attribute deleted",
531 data_type="NODE_ATTRIBUTE_DELETED",
532 token=token
533 )
534 if not notification_result:
535 logger.warning("Delete node attribute: Error sending notification to SSE server")
537 return generate_response(
538 status_code=200,
539 data=final_result,
540 message="Attribute deleted"
541 )
543# Get all nodes with a specific attribute
544@router.get("/constellation/{constellation_uuid}/nodes/attribute/{attribute}",
545 summary="Get all nodes with a specific attribute",
546 description="Get all nodes with the attribute given in parameter",
547 response_description="List of nodes",
548 responses={
549 200: {
550 "description": "List of nodes",
551 "content": {
552 "application/json": {
553 "example": {
554 "success": True,
555 "data": [
556 {
557 "attributes": {
558 "node_uuid": 1,
559 "title": "example_title",
560 "content": "example_content"
561 },
562 "labels": [
563 "Node"
564 ]
565 }
566 ],
567 "message": "Nodes retrieved",
568 "error": None
569 }
570 }
571 }
572 },
573 **constellation_check_error
574 }
575)
576async def read_nodes_with_attribute(constellation_uuid: str = Path(..., title="The UUID of the constellation"),
577 attribute: str = Path(..., title="The attribute to search for"),
578 decode: Optional[DecodeType] = Query(default=None, description="Whether to decode the attributes or not. Options: 'xml', 'plain_text'"),
579 in_filter: Optional[List[str]] = Query(default=None, description="Filter to include only specific attributes in the final results"),
580 out_filter: Optional[List[str]] = Query(default=None, description="Filter to exclude specific attributes from the final results"),
581 token: str = Depends(oauth2_scheme)):
582 test = check_constellation_access(token, constellation_uuid, "READ")
583 if test is not None:
584 return test
586 if in_filter is None:
587 in_filter = []
588 if out_filter is None:
589 out_filter = []
591 attribute = attribute.replace("`", "``") # Replace backticks with double backticks for Neo4j compatibility
593 with driver.session() as session:
594 result = session.run("""
595 MATCH (n {constellation_uuid: $constellation_uuid})
596 WHERE ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(n)) AND n[$attribute] IS NOT NULL
597 RETURN properties(n) AS attributes, labels(n) as labels
598 """,
599 constellation_uuid=constellation_uuid,
600 RESERVED_NODE_LABEL_NAMES=RESERVED_NODE_LABEL_NAMES,
601 attribute=attribute
602 )
603 final_result: JSONValue = [{"attributes": {
604 key: serialize_value(value)
605 for key, value in record["attributes"].items()
606 if (len(in_filter) == 0 or key in in_filter) and (len(out_filter) == 0 or key not in out_filter)
607 }, "labels": record["labels"]} for record in result]
608 driver.close()
610 final_result = decode_attributes_inplace(final_result, decode)
612 return generate_response(
613 status_code=200,
614 data=final_result,
615 message="Nodes retrieved"
616 )
618# Add an attribute to a node
619@router.post("/constellation/{constellation_uuid}/node/{node_uuid}/attribute/{attribute}",
620 summary="Add an attribute to a node",
621 description="Add the attribute given in parameter to the node specified by its UUID",
622 response_description="Node with the added attribute",
623 responses={
624 200: {
625 "description": "Node with the added attribute",
626 "content": {
627 "application/json": {
628 "example": {
629 "success": True,
630 "data": [
631 {
632 "attributes": {
633 "node_uuid": 1,
634 "title": "example_title",
635 "content": "example_content"
636 },
637 "labels": [
638 "Node"
639 ]
640 }
641 ],
642 "message": "Attribute added",
643 "error": None
644 }
645 }
646 }
647 },
648 **constellation_check_error
649 }
650)
651async def add_attribute(constellation_uuid: str = Path(..., title="The UUID of the constellation"),
652 node_uuid: str = Path(..., title="The UUID of the node"),
653 attribute: str = Path(..., title="The attribute to add"),
654 value: str = Body(..., title="The value of the attribute", embed=True),
655 decode: Optional[DecodeType] = Query(default=None, description="Whether to decode the attributes or not. Options: 'xml', 'plain_text'"),
656 in_filter: Optional[List[str]] = Query(default=None, description="Filter to include only specific attributes in the final results"),
657 out_filter: Optional[List[str]] = Query(default=None, description="Filter to exclude specific attributes from the final results"),
658 token: str = Depends(oauth2_scheme)):
659 test = check_constellation_access(token, constellation_uuid, "WRITE")
660 if test is not None:
661 return test
663 if in_filter is None:
664 in_filter = []
665 if out_filter is None:
666 out_filter = []
668 # Validate the attribute name
669 if attribute in RESERVED_NODE_ATTRIBUTE_NAMES:
670 return generate_error_response(
671 status_code=400,
672 error_code="BAD_REQUEST",
673 error_message=f"Attribute {attribute} cannot be added",
674 message=f"Attribute {attribute} cannot be added"
675 )
677 with driver.session() as session:
678 # check if the node exists
679 result = session.run("""
680 MATCH (n {constellation_uuid: $constellation_uuid, node_uuid: $node_uuid})
681 WHERE ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(n))
682 RETURN n
683 """,
684 constellation_uuid=constellation_uuid,
685 node_uuid=node_uuid,
686 RESERVED_NODE_LABEL_NAMES=RESERVED_NODE_LABEL_NAMES
687 )
688 if len(result.data()) == 0:
689 return generate_error_response(
690 status_code=404,
691 error_code="NOT_FOUND",
692 error_message=f"Node {node_uuid} not found",
693 message=f"Node {node_uuid} not found"
694 )
696 attribute = attribute.replace("`", "``") # Replace backticks with double backticks for Neo4j compatibility
698 # Check if the attribute already exists
699 result = session.run("""
700 MATCH (n {constellation_uuid: $constellation_uuid, node_uuid: $node_uuid})
701 WHERE ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(n))
702 RETURN n[$attribute] AS attribute, coalesce(n.word_count, -1) AS word_count, properties(n) AS currrentAttributes
703 """,
704 constellation_uuid=constellation_uuid,
705 node_uuid=node_uuid,
706 RESERVED_NODE_LABEL_NAMES=RESERVED_NODE_LABEL_NAMES,
707 attribute=attribute
708 )
709 records = [record for record in result]
710 if [record["attribute"] for record in records][0] is not None:
711 return generate_error_response(
712 status_code=400,
713 error_code="BAD_REQUEST",
714 error_message=f"Attribute {attribute} already exists for node {node_uuid}",
715 message=f"Attribute {attribute} already exists for node {node_uuid}"
716 )
718 # Compute the new word count
719 currentWordCount = [record["word_count"] for record in records][0]
720 currentAttributes = [record["currrentAttributes"] for record in records][0]
722 if currentWordCount == -1: # word_count has not been computed yet
723 currentWordCount = 0
724 for key in currentAttributes.keys(): # Add all current attributes word count
725 if key not in RESERVED_NODE_ATTRIBUTE_NAMES:
726 currentWordCount += len(decode_ydoc(currentAttributes[key], True).split())
728 newAttributeValueWordCount = len(decode_ydoc(value, True).split())
729 currentWordCount += newAttributeValueWordCount
731 # Add the attribute to the node
732 result = session.run("""
733 MATCH (n {constellation_uuid: $constellation_uuid, node_uuid: $node_uuid})
734 WHERE ALL(label IN $RESERVED_NODE_LABEL_NAMES WHERE NOT label IN labels(n))
735 SET n.created_at = coalesce(n.created_at, datetime())
736 SET n.updated_at = datetime()
737 SET n.word_count = $word_count
738 SET n[$attribute] = $value
739 RETURN properties(n) AS attributes, labels(n) as labels
740 """,
741 constellation_uuid=constellation_uuid,
742 node_uuid=node_uuid,
743 RESERVED_NODE_LABEL_NAMES=RESERVED_NODE_LABEL_NAMES,
744 word_count=currentWordCount,
745 attribute=attribute,
746 value=value
747 )
748 final_result: JSONValue = [{"attributes": {
749 key: serialize_value(value)
750 for key, value in record["attributes"].items()
751 if (len(in_filter) == 0 or key in in_filter) and (len(out_filter) == 0 or key not in out_filter)
752 }, "labels": record["labels"]} for record in result]
753 driver.close()
755 final_result = decode_attributes_inplace(final_result, decode)
757 # Send a notification to the SSE server
758 notification_result = send_constellation_notification(
759 constellation_uuid=constellation_uuid,
760 data=final_result,
761 message="Node attribute added",
762 data_type="NODE_ATTRIBUTE_ADDED",
763 token=token
764 )
765 if not notification_result:
766 logger.warning("Add node attribute: Error sending notification to SSE server")
768 return generate_response(
769 status_code=200,
770 data=final_result,
771 message="Attribute added"
772 )