44from typing import Annotated , Any
55
66import asyncpg .exceptions # type: ignore[import-untyped]
7- import sqlalchemy
87import sqlalchemy .exc
98from common_library .async_tools import maybe_await
109from common_library .basic_types import DEFAULT_FACTORY
1110from common_library .errors_classes import OsparcErrorMixin
1211from pydantic import BaseModel , ConfigDict , Field
13- from simcore_postgres_database .utils_aiosqlalchemy import map_db_exception
1412from sqlalchemy .dialects .postgresql import insert as pg_insert
1513
1614from ._protocols import DBConnection
1715from .aiopg_errors import ForeignKeyViolation , UniqueViolation
1816from .models .projects_node_to_pricing_unit import projects_node_to_pricing_unit
1917from .models .projects_nodes import projects_nodes
18+ from .utils_aiosqlalchemy import map_db_exception
2019
2120
2221#
@@ -71,6 +70,22 @@ class ProjectNodeCreate(BaseModel):
7170 def get_field_names (cls , * , exclude : set [str ]) -> set [str ]:
7271 return cls .model_fields .keys () - exclude
7372
73+ def model_dump_as_node (self ) -> dict [str , Any ]:
74+ """Converts a ProjectNode from the database to a Node model for the API.
75+
76+ Handles field mapping and excludes database-specific fields that are not
77+ part of the Node model.
78+ """
79+ # Get all ProjectNode fields except those that don't belong in Node
80+ exclude_fields = {"node_id" , "required_resources" }
81+ return self .model_dump (
82+ # NOTE: this setup ensures using the defaults provided in Node model when the db does not
83+ # provide them, e.g. `state`
84+ exclude = exclude_fields ,
85+ exclude_none = True ,
86+ exclude_unset = True ,
87+ )
88+
7489 model_config = ConfigDict (frozen = True )
7590
7691
@@ -80,6 +95,22 @@ class ProjectNode(ProjectNodeCreate):
8095
8196 model_config = ConfigDict (from_attributes = True )
8297
98+ def model_dump_as_node (self ) -> dict [str , Any ]:
99+ """Converts a ProjectNode from the database to a Node model for the API.
100+
101+ Handles field mapping and excludes database-specific fields that are not
102+ part of the Node model.
103+ """
104+ # Get all ProjectNode fields except those that don't belong in Node
105+ exclude_fields = {"node_id" , "required_resources" , "created" , "modified" }
106+ return self .model_dump (
107+ # NOTE: this setup ensures using the defaults provided in Node model when the db does not
108+ # provide them, e.g. `state`
109+ exclude = exclude_fields ,
110+ exclude_none = True ,
111+ exclude_unset = True ,
112+ )
113+
83114
84115@dataclass (frozen = True , kw_only = True )
85116class ProjectNodesRepo :
@@ -103,17 +134,18 @@ async def add(
103134 """
104135 if not nodes :
105136 return []
137+
138+ values = [
139+ {
140+ "project_uuid" : f"{ self .project_uuid } " ,
141+ ** node .model_dump (mode = "json" ),
142+ }
143+ for node in nodes
144+ ]
145+
106146 insert_stmt = (
107147 projects_nodes .insert ()
108- .values (
109- [
110- {
111- "project_uuid" : f"{ self .project_uuid } " ,
112- ** node .model_dump (exclude_unset = True , mode = "json" ),
113- }
114- for node in nodes
115- ]
116- )
148+ .values (values )
117149 .returning (
118150 * [
119151 c
@@ -129,14 +161,17 @@ async def add(
129161 rows = await maybe_await (result .fetchall ())
130162 assert isinstance (rows , list ) # nosec
131163 return [ProjectNode .model_validate (r ) for r in rows ]
164+
132165 except ForeignKeyViolation as exc :
133166 # this happens when the project does not exist, as we first check the node exists
134167 raise ProjectNodesProjectNotFoundError (
135168 project_uuid = self .project_uuid
136169 ) from exc
170+
137171 except UniqueViolation as exc :
138172 # this happens if the node already exists on creation
139173 raise ProjectNodesDuplicateNodeError from exc
174+
140175 except sqlalchemy .exc .IntegrityError as exc :
141176 raise map_db_exception (
142177 exc ,
0 commit comments