diff --git a/.github/actions/poetry_caching/action.yml b/.github/actions/poetry_caching/action.yml index fb76b1723..e185e7094 100644 --- a/.github/actions/poetry_caching/action.yml +++ b/.github/actions/poetry_caching/action.yml @@ -77,7 +77,12 @@ runs: POETRY_VERSION: ${{ inputs.poetry-version }} PYTHON_VERSION: ${{ inputs.python-version }} # Install poetry using the python version installed by setup-python step. - run: pipx install "poetry==$POETRY_VERSION" --python '${{ steps.setup-python.outputs.python-path }}' --verbose + run: | + pipx install "poetry==$POETRY_VERSION" --python '${{ steps.setup-python.outputs.python-path }}' --verbose + pipx ensurepath + # Ensure the poetry binary is available in the PATH. + # Test that the poetry binary is available. + poetry --version - name: Restore pip and poetry cached dependencies uses: actions/cache@v4 diff --git a/src/backend/base/langflow/alembic/script.py.mako b/src/backend/base/langflow/alembic/script.py.mako index bc9bca83a..6086a860c 100644 --- a/src/backend/base/langflow/alembic/script.py.mako +++ b/src/backend/base/langflow/alembic/script.py.mako @@ -11,6 +11,7 @@ from alembic import op import sqlalchemy as sa import sqlmodel from sqlalchemy.engine.reflection import Inspector +from langflow.utils import migration ${imports if imports else ""} # revision identifiers, used by Alembic. @@ -22,13 +23,9 @@ depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} def upgrade() -> None: conn = op.get_bind() - inspector = Inspector.from_engine(conn) # type: ignore - table_names = inspector.get_table_names() ${upgrades if upgrades else "pass"} def downgrade() -> None: conn = op.get_bind() - inspector = Inspector.from_engine(conn) # type: ignore - table_names = inspector.get_table_names() ${downgrades if downgrades else "pass"} diff --git a/src/backend/base/langflow/alembic/versions/1c79524817ed_add_unique_constraints_per_user_in_.py b/src/backend/base/langflow/alembic/versions/1c79524817ed_add_unique_constraints_per_user_in_.py new file mode 100644 index 000000000..0feec1b8b --- /dev/null +++ b/src/backend/base/langflow/alembic/versions/1c79524817ed_add_unique_constraints_per_user_in_.py @@ -0,0 +1,42 @@ +"""Add unique constraints per user in folder table + +Revision ID: 1c79524817ed +Revises: 3bb0ddf32dfb +Create Date: 2024-05-29 23:12:09.146880 + +""" + +from typing import Sequence, Union + +from alembic import op +from sqlalchemy.engine.reflection import Inspector + +# revision identifiers, used by Alembic. +revision: str = "1c79524817ed" +down_revision: Union[str, None] = "3bb0ddf32dfb" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + conn = op.get_bind() + inspector = Inspector.from_engine(conn) # type: ignore + constraints_names = [constraint["name"] for constraint in inspector.get_unique_constraints("folder")] + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("folder", schema=None) as batch_op: + if "unique_folder_name" not in constraints_names: + batch_op.create_unique_constraint("unique_folder_name", ["user_id", "name"]) + + # ### end Alembic commands ### + + +def downgrade() -> None: + conn = op.get_bind() + inspector = Inspector.from_engine(conn) # type: ignore + constraints_names = [constraint["name"] for constraint in inspector.get_unique_constraints("folder")] + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("folder", schema=None) as batch_op: + if "unique_folder_name" in constraints_names: + batch_op.drop_constraint("unique_folder_name", type_="unique") + + # ### end Alembic commands ### diff --git a/src/backend/base/langflow/alembic/versions/3bb0ddf32dfb_add_unique_constraints_per_user_in_flow_.py b/src/backend/base/langflow/alembic/versions/3bb0ddf32dfb_add_unique_constraints_per_user_in_flow_.py new file mode 100644 index 000000000..699df1437 --- /dev/null +++ b/src/backend/base/langflow/alembic/versions/3bb0ddf32dfb_add_unique_constraints_per_user_in_flow_.py @@ -0,0 +1,54 @@ +"""Add unique constraints per user in flow table + +Revision ID: 3bb0ddf32dfb +Revises: a72f5cf9c2f9 +Create Date: 2024-05-29 23:08:43.935040 + +""" + +from typing import Sequence, Union + +from alembic import op +from sqlalchemy.engine.reflection import Inspector + +# revision identifiers, used by Alembic. +revision: str = "3bb0ddf32dfb" +down_revision: Union[str, None] = "a72f5cf9c2f9" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + conn = op.get_bind() + inspector = Inspector.from_engine(conn) # type: ignore + # ### commands auto generated by Alembic - please adjust! ### + indexes_names = [index["name"] for index in inspector.get_indexes("flow")] + constraints_names = [constraint["name"] for constraint in inspector.get_unique_constraints("flow")] + with op.batch_alter_table("flow", schema=None) as batch_op: + if "ix_flow_endpoint_name" in indexes_names: + batch_op.drop_index("ix_flow_endpoint_name") + batch_op.create_index(batch_op.f("ix_flow_endpoint_name"), ["endpoint_name"], unique=False) + if "unique_flow_endpoint_name" not in constraints_names: + batch_op.create_unique_constraint("unique_flow_endpoint_name", ["user_id", "endpoint_name"]) + if "unique_flow_name" not in constraints_names: + batch_op.create_unique_constraint("unique_flow_name", ["user_id", "name"]) + + # ### end Alembic commands ### + + +def downgrade() -> None: + conn = op.get_bind() + inspector = Inspector.from_engine(conn) # type: ignore + # ### commands auto generated by Alembic - please adjust! ### + indexes_names = [index["name"] for index in inspector.get_indexes("flow")] + constraints_names = [constraint["name"] for constraint in inspector.get_unique_constraints("flow")] + with op.batch_alter_table("flow", schema=None) as batch_op: + if "unique_flow_name" in constraints_names: + batch_op.drop_constraint("unique_flow_name", type_="unique") + if "unique_flow_endpoint_name" in constraints_names: + batch_op.drop_constraint("unique_flow_endpoint_name", type_="unique") + if "ix_flow_endpoint_name" in indexes_names: + batch_op.drop_index(batch_op.f("ix_flow_endpoint_name")) + batch_op.create_index("ix_flow_endpoint_name", ["endpoint_name"], unique=1) + + # ### end Alembic commands ### diff --git a/src/backend/base/langflow/alembic/versions/a72f5cf9c2f9_add_endpoint_name_col.py b/src/backend/base/langflow/alembic/versions/a72f5cf9c2f9_add_endpoint_name_col.py new file mode 100644 index 000000000..3d6dd604c --- /dev/null +++ b/src/backend/base/langflow/alembic/versions/a72f5cf9c2f9_add_endpoint_name_col.py @@ -0,0 +1,52 @@ +"""Add endpoint name col + +Revision ID: a72f5cf9c2f9 +Revises: 29fe8f1f806b +Create Date: 2024-05-29 21:44:04.240816 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +import sqlmodel +from alembic import op +from sqlalchemy.engine.reflection import Inspector + +# revision identifiers, used by Alembic. +revision: str = "a72f5cf9c2f9" +down_revision: Union[str, None] = "29fe8f1f806b" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + conn = op.get_bind() + inspector = Inspector.from_engine(conn) # type: ignore + # ### commands auto generated by Alembic - please adjust! ### + column_names = [column["name"] for column in inspector.get_columns("flow")] + indexes = inspector.get_indexes("flow") + index_names = [index["name"] for index in indexes] + with op.batch_alter_table("flow", schema=None) as batch_op: + if "endpoint_name" not in column_names: + batch_op.add_column(sa.Column("endpoint_name", sqlmodel.sql.sqltypes.AutoString(), nullable=True)) + if "ix_flow_endpoint_name" not in index_names: + batch_op.create_index(batch_op.f("ix_flow_endpoint_name"), ["endpoint_name"], unique=True) + + # ### end Alembic commands ### + + +def downgrade() -> None: + conn = op.get_bind() + inspector = Inspector.from_engine(conn) # type: ignore + # ### commands auto generated by Alembic - please adjust! ### + column_names = [column["name"] for column in inspector.get_columns("flow")] + indexes = inspector.get_indexes("flow") + index_names = [index["name"] for index in indexes] + with op.batch_alter_table("flow", schema=None) as batch_op: + if "ix_flow_endpoint_name" in index_names: + batch_op.drop_index(batch_op.f("ix_flow_endpoint_name")) + if "endpoint_name" in column_names: + batch_op.drop_column("endpoint_name") + + # ### end Alembic commands ### diff --git a/src/backend/base/langflow/api/v1/endpoints.py b/src/backend/base/langflow/api/v1/endpoints.py index 006099b15..26633082e 100644 --- a/src/backend/base/langflow/api/v1/endpoints.py +++ b/src/backend/base/langflow/api/v1/endpoints.py @@ -53,10 +53,10 @@ def get_all( raise HTTPException(status_code=500, detail=str(exc)) from exc -@router.post("/run/{flow_id}", response_model=RunResponse, response_model_exclude_none=True) +@router.post("/run/{flow_id_or_name}", response_model=RunResponse, response_model_exclude_none=True) async def simplified_run_flow( db: Annotated[Session, Depends(get_session)], - flow_id: UUID, + flow_id_or_name: str, input_request: SimplifiedAPIRequest = SimplifiedAPIRequest(), stream: bool = False, api_key_user: User = Depends(api_key_security), @@ -111,8 +111,21 @@ async def simplified_run_flow( This endpoint provides a powerful interface for executing flows with enhanced flexibility and efficiency, supporting a wide range of applications by allowing for dynamic input and output configuration along with performance optimizations through session management and caching. """ session_id = input_request.session_id - + endpoint_name = None + flow_id_str = None try: + try: + flow_id = UUID(flow_id_or_name) + + except ValueError: + endpoint_name = flow_id_or_name + flow = db.exec( + select(Flow).where(Flow.endpoint_name == endpoint_name).where(Flow.user_id == api_key_user.id) + ).first() + if flow is None: + raise ValueError(f"Flow with endpoint name {endpoint_name} not found") + flow_id = flow.id + flow_id_str = str(flow_id) artifacts = {} if input_request.session_id: @@ -172,10 +185,13 @@ async def simplified_run_flow( # This means the Flow ID is not a valid UUID which means it can't find the flow raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc except ValueError as exc: - if f"Flow {flow_id_str} not found" in str(exc): + if flow_id_str and f"Flow {flow_id_str} not found" in str(exc): logger.error(f"Flow {flow_id_str} not found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc - elif f"Session {session_id} not found" in str(exc): + elif endpoint_name and f"Flow with endpoint name {endpoint_name} not found" in str(exc): + logger.error(f"Flow with endpoint name {endpoint_name} not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc + elif session_id and f"Session {session_id} not found" in str(exc): logger.error(f"Session {session_id} not found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc else: diff --git a/src/backend/base/langflow/api/v1/flows.py b/src/backend/base/langflow/api/v1/flows.py index 3b577d8c8..44b1fa275 100644 --- a/src/backend/base/langflow/api/v1/flows.py +++ b/src/backend/base/langflow/api/v1/flows.py @@ -135,30 +135,49 @@ def update_flow( settings_service=Depends(get_settings_service), ): """Update a flow.""" + try: + db_flow = read_flow( + session=session, + flow_id=flow_id, + current_user=current_user, + settings_service=settings_service, + ) + if not db_flow: + raise HTTPException(status_code=404, detail="Flow not found") + flow_data = flow.model_dump(exclude_unset=True) + if settings_service.settings.remove_api_keys: + flow_data = remove_api_keys(flow_data) + for key, value in flow_data.items(): + if value is not None: + setattr(db_flow, key, value) + db_flow.updated_at = datetime.now(timezone.utc) + if db_flow.folder_id is None: + default_folder = session.exec(select(Folder).where(Folder.name == DEFAULT_FOLDER_NAME)).first() + if default_folder: + db_flow.folder_id = default_folder.id + session.add(db_flow) + session.commit() + session.refresh(db_flow) + return db_flow + except Exception as e: + # If it is a validation error, return the error message + if hasattr(e, "errors"): + raise HTTPException(status_code=400, detail=str(e)) from e + elif "UNIQUE constraint failed" in str(e): + # Get the name of the column that failed + columns = str(e).split("UNIQUE constraint failed: ")[1].split(".")[1].split("\n")[0] + # UNIQUE constraint failed: flow.user_id, flow.name + # or UNIQUE constraint failed: flow.name + # if the column has id in it, we want the other column + column = columns.split(",")[1] if "id" in columns.split(",")[0] else columns.split(",")[0] - db_flow = read_flow( - session=session, - flow_id=flow_id, - current_user=current_user, - settings_service=settings_service, - ) - if not db_flow: - raise HTTPException(status_code=404, detail="Flow not found") - flow_data = flow.model_dump(exclude_unset=True) - if settings_service.settings.remove_api_keys: - flow_data = remove_api_keys(flow_data) - for key, value in flow_data.items(): - if value is not None: - setattr(db_flow, key, value) - db_flow.updated_at = datetime.now(timezone.utc) - if db_flow.folder_id is None: - default_folder = session.exec(select(Folder).where(Folder.name == DEFAULT_FOLDER_NAME)).first() - if default_folder: - db_flow.folder_id = default_folder.id - session.add(db_flow) - session.commit() - session.refresh(db_flow) - return db_flow + raise HTTPException( + status_code=400, detail=f"{column.capitalize().replace('_', ' ')} must be unique" + ) from e + elif isinstance(e, HTTPException): + raise e + else: + raise HTTPException(status_code=500, detail=str(e)) from e @router.delete("/{flow_id}", status_code=200) diff --git a/src/backend/base/langflow/services/database/models/flow/model.py b/src/backend/base/langflow/services/database/models/flow/model.py index 17b5e8931..ddfa98e9c 100644 --- a/src/backend/base/langflow/services/database/models/flow/model.py +++ b/src/backend/base/langflow/services/database/models/flow/model.py @@ -1,5 +1,6 @@ # Path: src/backend/langflow/services/database/models/flow/model.py +import re import warnings from datetime import datetime, timezone from typing import TYPE_CHECKING, Dict, Optional @@ -7,7 +8,9 @@ from uuid import UUID, uuid4 import emoji from emoji import purely_emoji # type: ignore +from fastapi import HTTPException, status from pydantic import field_serializer, field_validator +from sqlalchemy import UniqueConstraint from sqlmodel import JSON, Column, Field, Relationship, SQLModel from langflow.schema.schema import Record @@ -26,6 +29,24 @@ class FlowBase(SQLModel): is_component: Optional[bool] = Field(default=False, nullable=True) updated_at: Optional[datetime] = Field(default_factory=lambda: datetime.now(timezone.utc), nullable=True) folder_id: Optional[UUID] = Field(default=None, nullable=True) + endpoint_name: Optional[str] = Field(default=None, nullable=True, index=True) + + @field_validator("endpoint_name") + @classmethod + def validate_endpoint_name(cls, v): + # Endpoint name must be a string containing only letters, numbers, hyphens, and underscores + if v is not None: + if not isinstance(v, str): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Endpoint name must be a string", + ) + if not re.match(r"^[a-zA-Z0-9_-]+$", v): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Endpoint name must contain only letters, numbers, hyphens, and underscores", + ) + return v @field_validator("icon_bg_color") def validate_icon_bg_color(cls, v): @@ -128,6 +149,11 @@ class Flow(FlowBase, table=True): record = Record(data=data) return record + __table_args__ = ( + UniqueConstraint("user_id", "name", name="unique_flow_name"), + UniqueConstraint("user_id", "endpoint_name", name="unique_flow_endpoint_name"), + ) + class FlowCreate(FlowBase): user_id: Optional[UUID] = None @@ -145,3 +171,21 @@ class FlowUpdate(SQLModel): description: Optional[str] = None data: Optional[Dict] = None folder_id: Optional[UUID] = None + endpoint_name: Optional[str] = None + + @field_validator("endpoint_name") + @classmethod + def validate_endpoint_name(cls, v): + # Endpoint name must be a string containing only letters, numbers, hyphens, and underscores + if v is not None: + if not isinstance(v, str): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Endpoint name must be a string", + ) + if not re.match(r"^[a-zA-Z0-9_-]+$", v): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Endpoint name must contain only letters, numbers, hyphens, and underscores", + ) + return v diff --git a/src/backend/base/langflow/services/database/models/folder/model.py b/src/backend/base/langflow/services/database/models/folder/model.py index 6ce038c63..dc2dfaa80 100644 --- a/src/backend/base/langflow/services/database/models/folder/model.py +++ b/src/backend/base/langflow/services/database/models/folder/model.py @@ -1,6 +1,7 @@ from typing import TYPE_CHECKING, List, Optional from uuid import UUID, uuid4 +from sqlalchemy import UniqueConstraint from sqlmodel import Field, Relationship, SQLModel from langflow.services.database.models.flow.model import FlowRead @@ -30,6 +31,8 @@ class Folder(FolderBase, table=True): back_populates="folder", sa_relationship_kwargs={"cascade": "all, delete, delete-orphan"} ) + __table_args__ = (UniqueConstraint("user_id", "name", name="unique_folder_name"),) + class FolderCreate(FolderBase): components_list: Optional[List[UUID]] = None diff --git a/src/backend/base/langflow/utils/migration.py b/src/backend/base/langflow/utils/migration.py new file mode 100644 index 000000000..b85522c5b --- /dev/null +++ b/src/backend/base/langflow/utils/migration.py @@ -0,0 +1,65 @@ +from sqlalchemy.engine.reflection import Inspector + + +def table_exists(name, conn): + """ + Check if a table exists. + + Parameters: + name (str): The name of the table to check. + conn (sqlalchemy.engine.Engine or sqlalchemy.engine.Connection): The SQLAlchemy engine or connection to use. + + Returns: + bool: True if the table exists, False otherwise. + """ + inspector = Inspector.from_engine(conn) + return name in inspector.get_table_names() + + +def column_exists(table_name, column_name, conn): + """ + Check if a column exists in a table. + + Parameters: + table_name (str): The name of the table to check. + column_name (str): The name of the column to check. + conn (sqlalchemy.engine.Engine or sqlalchemy.engine.Connection): The SQLAlchemy engine or connection to use. + + Returns: + bool: True if the column exists, False otherwise. + """ + inspector = Inspector.from_engine(conn) + return column_name in [column["name"] for column in inspector.get_columns(table_name)] + + +def foreign_key_exists(table_name, fk_name, conn): + """ + Check if a foreign key exists in a table. + + Parameters: + table_name (str): The name of the table to check. + fk_name (str): The name of the foreign key to check. + conn (sqlalchemy.engine.Engine or sqlalchemy.engine.Connection): The SQLAlchemy engine or connection to use. + + Returns: + bool: True if the foreign key exists, False otherwise. + """ + inspector = Inspector.from_engine(conn) + return fk_name in [fk["name"] for fk in inspector.get_foreign_keys(table_name)] + + +def constraint_exists(table_name, constraint_name, conn): + """ + Check if a constraint exists in a table. + + Parameters: + table_name (str): The name of the table to check. + constraint_name (str): The name of the constraint to check. + conn (sqlalchemy.engine.Engine or sqlalchemy.engine.Connection): The SQLAlchemy engine or connection to use. + + Returns: + bool: True if the constraint exists, False otherwise. + """ + inspector = Inspector.from_engine(conn) + constraints = inspector.get_unique_constraints(table_name) + return constraint_name in [constraint["name"] for constraint in constraints] diff --git a/src/frontend/src/components/editFlowSettingsComponent/index.tsx b/src/frontend/src/components/editFlowSettingsComponent/index.tsx index 94ee4f19e..d8a6fc43e 100644 --- a/src/frontend/src/components/editFlowSettingsComponent/index.tsx +++ b/src/frontend/src/components/editFlowSettingsComponent/index.tsx @@ -9,11 +9,14 @@ export const EditFlowSettings: React.FC = ({ name, invalidNameList, description, + endpointName, maxLength = 50, setName, setDescription, + setEndpointName, }: InputProps): JSX.Element => { const [isMaxLength, setIsMaxLength] = useState(false); + const [isEndpointNameValid, setIsEndpointNameValid] = useState(true); const handleNameChange = (event: ChangeEvent) => { const { value } = event.target; @@ -29,6 +32,18 @@ export const EditFlowSettings: React.FC = ({ setDescription!(event.target.value); }; + const handleEndpointNameChange = (event: ChangeEvent) => { + // Validate the endpoint name + // use this regex r'^[a-zA-Z0-9_-]+$' + const isValid = + (/^[a-zA-Z0-9_-]+$/.test(event.target.value) && + event.target.value.length <= maxLength) || + // empty is also valid + event.target.value.length === 0; + setIsEndpointNameValid(isValid); + setEndpointName!(event.target.value); + }; + //this function is necessary to select the text when double clicking, this was not working with the onFocus event const handleFocus = (event) => event.target.select(); @@ -91,6 +106,32 @@ export const EditFlowSettings: React.FC = ({ )} + {setEndpointName && ( + + )} ); }; diff --git a/src/frontend/src/components/sidebarComponent/components/sideBarFolderButtons/index.tsx b/src/frontend/src/components/sidebarComponent/components/sideBarFolderButtons/index.tsx index 9a4b0fb64..ae40c4613 100644 --- a/src/frontend/src/components/sidebarComponent/components/sideBarFolderButtons/index.tsx +++ b/src/frontend/src/components/sidebarComponent/components/sideBarFolderButtons/index.tsx @@ -33,7 +33,7 @@ const SideBarFoldersButtonsComponent = ({ const [foldersNames, setFoldersNames] = useState({}); const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot); const [editFolders, setEditFolderName] = useState( - folders.map((obj) => ({ name: obj.name, edit: false })), + folders.map((obj) => ({ name: obj.name, edit: false })) ); const uploadFolder = useFolderStore((state) => state.uploadFolder); const currentFolder = pathname.split("/"); @@ -58,7 +58,7 @@ const SideBarFoldersButtonsComponent = ({ const { dragOver, dragEnter, dragLeave, onDrop } = useFileDrop( folderId, - handleFolderChange, + handleFolderChange ); const handleUploadFlowsToFolder = () => { @@ -73,7 +73,7 @@ const SideBarFoldersButtonsComponent = ({ addFolder({ name: "New Folder", parent_id: null, description: "" }).then( (res) => { getFoldersApi(true); - }, + } ); } @@ -91,8 +91,6 @@ const SideBarFoldersButtonsComponent = ({ folders.map((obj) => ({ name: obj.name, edit: false })); }, [folders]); - - return ( <>
@@ -120,7 +118,7 @@ const SideBarFoldersButtonsComponent = ({ <> {folders.map((item, index) => { const editFolderName = editFolders?.filter( - (folder) => folder.name === item.name, + (folder) => folder.name === item.name )[0]; return (
handleChangeFolder!(item.id!)} > @@ -206,7 +204,7 @@ const SideBarFoldersButtonsComponent = ({ folders.map((obj) => ({ name: obj.name, edit: false, - })), + })) ); } if (e.key === "Enter") { @@ -239,10 +237,10 @@ const SideBarFoldersButtonsComponent = ({ }; const updatedFolder = await updateFolder( body, - item.id!, + item.id! ); const updateFolders = folders.filter( - (f) => f.name !== item.name, + (f) => f.name !== item.name ); setFolders([...updateFolders, updatedFolder]); setFoldersNames({}); @@ -250,7 +248,7 @@ const SideBarFoldersButtonsComponent = ({ folders.map((obj) => ({ name: obj.name, edit: false, - })), + })) ); } else { setFoldersNames((old) => ({ diff --git a/src/frontend/src/controllers/API/index.ts b/src/frontend/src/controllers/API/index.ts index b6dc2ef86..2d0f46da3 100644 --- a/src/frontend/src/controllers/API/index.ts +++ b/src/frontend/src/controllers/API/index.ts @@ -61,7 +61,7 @@ export async function sendAll(data: sendAllProps) { } export async function postValidateCode( - code: string, + code: string ): Promise> { return await api.post(`${BASE_URL_API}validate/code`, { code }); } @@ -76,7 +76,7 @@ export async function postValidateCode( export async function postValidatePrompt( name: string, template: string, - frontend_node: APIClassType, + frontend_node: APIClassType ): Promise> { return api.post(`${BASE_URL_API}validate/prompt`, { name, @@ -149,7 +149,7 @@ export async function saveFlowToDatabase(newFlow: { * @throws Will throw an error if the update fails. */ export async function updateFlowInDatabase( - updatedFlow: FlowType, + updatedFlow: FlowType ): Promise { try { const response = await api.patch(`${BASE_URL_API}flows/${updatedFlow.id}`, { @@ -157,6 +157,7 @@ export async function updateFlowInDatabase( data: updatedFlow.data, description: updatedFlow.description, folder_id: updatedFlow.folder_id === "" ? null : updatedFlow.folder_id, + endpoint_name: updatedFlow.endpoint_name, }); if (response?.status !== 200) { @@ -326,7 +327,7 @@ export async function getHealth() { * */ export async function getBuildStatus( - flowId: string, + flowId: string ): Promise> { return await api.get(`${BASE_URL_API}build/${flowId}/status`); } @@ -339,7 +340,7 @@ export async function getBuildStatus( * */ export async function postBuildInit( - flow: FlowType, + flow: FlowType ): Promise> { return await api.post(`${BASE_URL_API}build/init/${flow.id}`, flow); } @@ -355,7 +356,7 @@ export async function postBuildInit( */ export async function uploadFile( file: File, - id: string, + id: string ): Promise> { const formData = new FormData(); formData.append("file", file); @@ -364,7 +365,7 @@ export async function uploadFile( export async function postCustomComponent( code: string, - apiClass: APIClassType, + apiClass: APIClassType ): Promise> { // let template = apiClass.template; return await api.post(`${BASE_URL_API}custom_component`, { @@ -377,7 +378,7 @@ export async function postCustomComponentUpdate( code: string, template: APITemplateType, field: string, - field_value: any, + field_value: any ): Promise> { return await api.post(`${BASE_URL_API}custom_component/update`, { code, @@ -399,7 +400,7 @@ export async function onLogin(user: LoginType) { headers: { "Content-Type": "application/x-www-form-urlencoded", }, - }, + } ); if (response.status === 200) { @@ -461,11 +462,11 @@ export async function addUser(user: UserInputType): Promise> { export async function getUsersPage( skip: number, - limit: number, + limit: number ): Promise> { try { const res = await api.get( - `${BASE_URL_API}users/?skip=${skip}&limit=${limit}`, + `${BASE_URL_API}users/?skip=${skip}&limit=${limit}` ); if (res.status === 200) { return res.data; @@ -502,7 +503,7 @@ export async function resetPassword(user_id: string, user: resetPasswordType) { try { const res = await api.patch( `${BASE_URL_API}users/${user_id}/reset-password`, - user, + user ); if (res.status === 200) { return res.data; @@ -576,7 +577,7 @@ export async function saveFlowStore( last_tested_version?: string; }, tags: string[], - publicFlow = false, + publicFlow = false ): Promise { try { const response = await api.post(`${BASE_URL_API}store/components/`, { @@ -705,7 +706,7 @@ export async function postStoreComponents(component: Component) { export async function getComponent(component_id: string) { try { const res = await api.get( - `${BASE_URL_API}store/components/${component_id}`, + `${BASE_URL_API}store/components/${component_id}` ); if (res.status === 200) { return res.data; @@ -720,7 +721,7 @@ export async function searchComponent( page?: number | null, limit?: number | null, status?: string | null, - tags?: string[], + tags?: string[] ): Promise { try { let url = `${BASE_URL_API}store/components/`; @@ -832,7 +833,7 @@ export async function updateFlowStore( }, tags: string[], publicFlow = false, - id: string, + id: string ): Promise { try { const response = await api.patch(`${BASE_URL_API}store/components/${id}`, { @@ -916,7 +917,7 @@ export async function deleteGlobalVariable(id: string) { export async function updateGlobalVariable( name: string, value: string, - id: string, + id: string ) { try { const response = api.patch(`${BASE_URL_API}variables/${id}`, { @@ -935,7 +936,7 @@ export async function getVerticesOrder( startNodeId?: string | null, stopNodeId?: string | null, nodes?: Node[], - Edges?: Edge[], + Edges?: Edge[] ): Promise> { // nodeId is optional and is a query parameter // if nodeId is not provided, the API will return all vertices @@ -955,19 +956,19 @@ export async function getVerticesOrder( return await api.post( `${BASE_URL_API}build/${flowId}/vertices`, data, - config, + config ); } export async function postBuildVertex( flowId: string, vertexId: string, - input_value: string, + input_value: string ): Promise> { // input_value is optional and is a query parameter return await api.post( `${BASE_URL_API}build/${flowId}/vertices/${vertexId}`, - input_value ? { inputs: { input_value: input_value } } : undefined, + input_value ? { inputs: { input_value: input_value } } : undefined ); } @@ -991,7 +992,7 @@ export async function getFlowPool({ } export async function deleteFlowPool( - flowId: string, + flowId: string ): Promise> { const config = {}; config["params"] = { flow_id: flowId }; @@ -999,7 +1000,7 @@ export async function deleteFlowPool( } export async function multipleDeleteFlowsComponents( - flowIds: string[], + flowIds: string[] ): Promise> { return await api.post(`${BASE_URL_API}flows/multiple_delete/`, { flow_ids: flowIds, @@ -1009,7 +1010,7 @@ export async function multipleDeleteFlowsComponents( export async function getTransactionTable( id: string, mode: "intersection" | "union", - params = {}, + params = {} ): Promise<{ rows: Array; columns: Array }> { const config = {}; config["params"] = { flow_id: id }; @@ -1024,7 +1025,7 @@ export async function getTransactionTable( export async function getMessagesTable( id: string, mode: "intersection" | "union", - params = {}, + params = {} ): Promise<{ rows: Array; columns: Array }> { const config = {}; config["params"] = { flow_id: id }; diff --git a/src/frontend/src/modals/apiModal/utils/get-curl-code.tsx b/src/frontend/src/modals/apiModal/utils/get-curl-code.tsx index e8eed8f8a..48be0ceeb 100644 --- a/src/frontend/src/modals/apiModal/utils/get-curl-code.tsx +++ b/src/frontend/src/modals/apiModal/utils/get-curl-code.tsx @@ -8,13 +8,14 @@ export default function getCurlCode( flowId: string, isAuth: boolean, tweaksBuildedObject, + endpointName?: string ): string { const tweaksObject = tweaksBuildedObject[0]; - + // show the endpoint name in the curl command if it exists return `curl -X POST \\ - ${window.location.protocol}//${ - window.location.host - }/api/v1/run/${flowId}?stream=false \\ + ${window.location.protocol}//${window.location.host}/api/v1/run/${ + endpointName || flowId + }?stream=false \\ -H 'Content-Type: application/json'\\${ !isAuth ? `\n -H 'x-api-key: '\\` : "" } diff --git a/src/frontend/src/modals/apiModal/views/index.tsx b/src/frontend/src/modals/apiModal/views/index.tsx index 6e614e03f..a0a614b06 100644 --- a/src/frontend/src/modals/apiModal/views/index.tsx +++ b/src/frontend/src/modals/apiModal/views/index.tsx @@ -18,11 +18,11 @@ import { buildContent } from "../utils/build-content"; import { buildTweaks } from "../utils/build-tweaks"; import { checkCanBuildTweakObject } from "../utils/check-can-build-tweak-object"; import { getChangesType } from "../utils/get-changes-types"; -import { getNodesWithDefaultValue } from "../utils/get-nodes-with-default-value"; -import { getValue } from "../utils/get-value"; -import getPythonApiCode from "../utils/get-python-api-code"; import getCurlCode from "../utils/get-curl-code"; +import { getNodesWithDefaultValue } from "../utils/get-nodes-with-default-value"; +import getPythonApiCode from "../utils/get-python-api-code"; import getPythonCode from "../utils/get-python-code"; +import { getValue } from "../utils/get-value"; import getWidgetCode from "../utils/get-widget-code"; import tabsArray from "../utils/tabs-array"; @@ -35,7 +35,7 @@ const ApiModal = forwardRef( flow: FlowType; children: ReactNode; }, - ref, + ref ) => { const tweak = useTweaksStore((state) => state.tweak); const addTweaks = useTweaksStore((state) => state.setTweak); @@ -47,7 +47,12 @@ const ApiModal = forwardRef( const [open, setOpen] = useState(false); const [activeTab, setActiveTab] = useState("0"); const pythonApiCode = getPythonApiCode(flow?.id, autoLogin, tweak); - const curl_code = getCurlCode(flow?.id, autoLogin, tweak); + const curl_code = getCurlCode( + flow?.id, + autoLogin, + tweak, + flow?.endpoint_name + ); const pythonCode = getPythonCode(flow?.name, tweak); const widgetCode = getWidgetCode(flow?.id, flow?.name, autoLogin); const tweaksCode = buildTweaks(flow); @@ -106,7 +111,7 @@ const ApiModal = forwardRef( buildTweakObject( nodeId, element.data.node.template[templateField].value, - element.data.node.template[templateField], + element.data.node.template[templateField] ); } }); @@ -123,7 +128,7 @@ const ApiModal = forwardRef( async function buildTweakObject( tw: string, changes: string | string[] | boolean | number | Object[] | Object, - template: TemplateVariableType, + template: TemplateVariableType ) { changes = getChangesType(changes, template); @@ -161,7 +166,12 @@ const ApiModal = forwardRef( const addCodes = (cloneTweak) => { const pythonApiCode = getPythonApiCode(flow?.id, autoLogin, cloneTweak); - const curl_code = getCurlCode(flow?.id, autoLogin, cloneTweak); + const curl_code = getCurlCode( + flow?.id, + autoLogin, + cloneTweak, + flow?.endpoint_name + ); const pythonCode = getPythonCode(flow?.name, cloneTweak); const widgetCode = getWidgetCode(flow?.id, flow?.name, autoLogin); @@ -204,7 +214,7 @@ const ApiModal = forwardRef( ); - }, + } ); export default ApiModal; diff --git a/src/frontend/src/modals/flowLogsModal/index.tsx b/src/frontend/src/modals/flowLogsModal/index.tsx index 70a8802c2..6f2a09633 100644 --- a/src/frontend/src/modals/flowLogsModal/index.tsx +++ b/src/frontend/src/modals/flowLogsModal/index.tsx @@ -1,19 +1,16 @@ +import { ColDef, ColGroupDef } from "ag-grid-community"; +import { AxiosError } from "axios"; import { useEffect, useRef, useState } from "react"; import IconComponent from "../../components/genericIconComponent"; +import TableComponent from "../../components/tableComponent"; import { Tabs, TabsList, TabsTrigger } from "../../components/ui/tabs"; +import { getMessagesTable, getTransactionTable } from "../../controllers/API"; +import useAlertStore from "../../stores/alertStore"; +import useFlowStore from "../../stores/flowStore"; import useFlowsManagerStore from "../../stores/flowsManagerStore"; import { FlowSettingsPropsType } from "../../types/components"; import { FlowType, NodeDataType } from "../../types/flow"; import BaseModal from "../baseModal"; -import TableComponent from "../../components/tableComponent"; -import { getMessagesTable, getTransactionTable } from "../../controllers/API"; -import { - ColDef, - ColGroupDef, - SizeColumnsToFitGridStrategy, -} from "ag-grid-community"; -import useAlertStore from "../../stores/alertStore"; -import useFlowStore from "../../stores/flowStore"; export default function FlowLogsModal({ open, @@ -41,8 +38,17 @@ export default function FlowLogsModal({ function handleClick(): void { currentFlow!.name = name; currentFlow!.description = description; - saveFlow(currentFlow!); - setOpen(false); + saveFlow(currentFlow!) + ?.then(() => { + setOpen(false); + }) + .catch((err) => { + useAlertStore.getState().setErrorData({ + title: "Error while saving changes", + list: [(err as AxiosError).response?.data.detail ?? ""], + }); + console.error(err); + }); } useEffect(() => { @@ -66,7 +72,7 @@ export default function FlowLogsModal({ .some((template) => template["stream"] && template["stream"].value); console.log( haStream, - nodes.map((nodes) => (nodes.data as NodeDataType).node!.template), + nodes.map((nodes) => (nodes.data as NodeDataType).node!.template) ); if (haStream) { setNoticeData({ diff --git a/src/frontend/src/modals/flowSettingsModal/index.tsx b/src/frontend/src/modals/flowSettingsModal/index.tsx index 1daeb0fe1..ea4ad09cb 100644 --- a/src/frontend/src/modals/flowSettingsModal/index.tsx +++ b/src/frontend/src/modals/flowSettingsModal/index.tsx @@ -3,6 +3,7 @@ import EditFlowSettings from "../../components/editFlowSettingsComponent"; import IconComponent from "../../components/genericIconComponent"; import { Button } from "../../components/ui/button"; import { SETTINGS_DIALOG_SUBTITLE } from "../../constants/constants"; +import useAlertStore from "../../stores/alertStore"; import useFlowsManagerStore from "../../stores/flowsManagerStore"; import { FlowSettingsPropsType } from "../../types/components"; import { FlowType } from "../../types/flow"; @@ -22,12 +23,23 @@ export default function FlowSettingsModal({ const [name, setName] = useState(currentFlow!.name); const [description, setDescription] = useState(currentFlow!.description); + const [endpoint_name, setEndpointName] = useState(currentFlow!.endpoint_name); function handleClick(): void { currentFlow!.name = name; currentFlow!.description = description; - saveFlow(currentFlow!); - setOpen(false); + currentFlow!.endpoint_name = endpoint_name; + saveFlow(currentFlow!) + ?.then(() => { + setOpen(false); + }) + .catch((err) => { + useAlertStore.getState().setErrorData({ + title: "Error while saving changes", + list: [(err as AxiosError).response?.data.detail ?? ""], + }); + console.error(err); + }); } const [nameLists, setNameList] = useState([]); @@ -41,7 +53,7 @@ export default function FlowSettingsModal({ }, [flows]); return ( - + Settings @@ -51,8 +63,10 @@ export default function FlowSettingsModal({ invalidNameList={nameLists} name={name} description={description} + endpointName={endpoint_name} setName={setName} setDescription={setDescription} + setEndpointName={setEndpointName} /> diff --git a/src/frontend/src/stores/flowsManagerStore.ts b/src/frontend/src/stores/flowsManagerStore.ts index 103b12cd7..52a8eec8c 100644 --- a/src/frontend/src/stores/flowsManagerStore.ts +++ b/src/frontend/src/stores/flowsManagerStore.ts @@ -1,4 +1,3 @@ -import { AxiosError } from "axios"; import { cloneDeep, debounce } from "lodash"; import { Edge, Node, Viewport, XYPosition } from "reactflow"; import { create } from "zustand"; @@ -87,12 +86,12 @@ const useFlowsManagerStore = create((set, get) => ({ if (dbData) { const { data, flows } = processFlows(dbData, false); const examples = flows.filter( - (flow) => flow.folder_id === starterFolderId, + (flow) => flow.folder_id === starterFolderId ); get().setExamples(examples); const flowsWithoutStarterFolder = flows.filter( - (flow) => flow.folder_id !== starterFolderId, + (flow) => flow.folder_id !== starterFolderId ); get().setFlows(flowsWithoutStarterFolder); @@ -120,7 +119,7 @@ const useFlowsManagerStore = create((set, get) => ({ if (get().currentFlow) { get().saveFlow( { ...get().currentFlow!, data: { nodes, edges, viewport } }, - true, + true ); } }, @@ -146,7 +145,7 @@ const useFlowsManagerStore = create((set, get) => ({ return updatedFlow; } return flow; - }), + }) ); //update tabs state @@ -155,11 +154,9 @@ const useFlowsManagerStore = create((set, get) => ({ } }) .catch((err) => { - useAlertStore.getState().setErrorData({ - title: "Error while saving changes", - list: [(err as AxiosError).message], - }); reject(err); + set({ saveLoading: false }); + throw err; }); }); }, SAVE_DEBOUNCE_TIME), @@ -197,7 +194,7 @@ const useFlowsManagerStore = create((set, get) => ({ flow?: FlowType, override?: boolean, position?: XYPosition, - fromDragAndDrop?: boolean, + fromDragAndDrop?: boolean ): Promise => { if (newProject) { let flowData = flow @@ -213,7 +210,7 @@ const useFlowsManagerStore = create((set, get) => ({ const newFlow = createNewFlow( flowData!, flow!, - folder_id || my_collection_id!, + folder_id || my_collection_id! ); const { id } = await saveFlowToDatabase(newFlow); newFlow.id = id; @@ -236,7 +233,7 @@ const useFlowsManagerStore = create((set, get) => ({ const newFlow = createNewFlow( flowData!, flow!, - folder_id || my_collection_id!, + folder_id || my_collection_id! ); const newName = addVersionToDuplicates(newFlow, get().flows); @@ -272,7 +269,7 @@ const useFlowsManagerStore = create((set, get) => ({ .getState() .paste( { nodes: flow!.data!.nodes, edges: flow!.data!.edges }, - position ?? { x: 10, y: 10 }, + position ?? { x: 10, y: 10 } ); } }, @@ -282,7 +279,7 @@ const useFlowsManagerStore = create((set, get) => ({ multipleDeleteFlowsComponents(id) .then(() => { const { data, flows } = processFlows( - get().flows.filter((flow) => !id.includes(flow.id)), + get().flows.filter((flow) => !id.includes(flow.id)) ); get().setFlows(flows); set({ isLoading: false }); @@ -302,7 +299,7 @@ const useFlowsManagerStore = create((set, get) => ({ deleteFlowFromDatabase(id) .then(() => { const { data, flows } = processFlows( - get().flows.filter((flow) => flow.id !== id), + get().flows.filter((flow) => flow.id !== id) ); get().setFlows(flows); set({ isLoading: false }); @@ -324,7 +321,7 @@ const useFlowsManagerStore = create((set, get) => ({ return new Promise((resolve) => { let componentFlow = get().flows.find( (componentFlow) => - componentFlow.is_component && componentFlow.name === key, + componentFlow.is_component && componentFlow.name === key ); if (componentFlow) { @@ -372,7 +369,7 @@ const useFlowsManagerStore = create((set, get) => ({ fileData, undefined, position, - true, + true ); resolve(id); } @@ -413,7 +410,7 @@ const useFlowsManagerStore = create((set, get) => ({ return get().addFlow( true, createFlowComponent(component, useDarkStore.getState().version), - override, + override ); }, takeSnapshot: () => { @@ -434,7 +431,7 @@ const useFlowsManagerStore = create((set, get) => ({ if (pastLength > 0) { past[currentFlowId] = past[currentFlowId].slice( pastLength - defaultOptions.maxHistorySize + 1, - pastLength, + pastLength ); past[currentFlowId].push(newState); diff --git a/src/frontend/src/types/components/index.ts b/src/frontend/src/types/components/index.ts index e23557a84..28848bcc0 100644 --- a/src/frontend/src/types/components/index.ts +++ b/src/frontend/src/types/components/index.ts @@ -269,9 +269,11 @@ export type IconComponentProps = { export type InputProps = { name: string | null; description: string | null; + endpointName?: string; maxLength?: number; setName?: (name: string) => void; setDescription?: (description: string) => void; + setEndpointName?: (endpointName: string) => void; invalidNameList?: string[]; }; @@ -517,7 +519,7 @@ export type nodeToolbarPropsType = { updateNodeCode?: ( newNodeClass: APIClassType, code: string, - name: string, + name: string ) => void; setShowState: (show: boolean | SetStateAction) => void; isOutdated?: boolean; @@ -567,7 +569,7 @@ export type chatMessagePropsType = { updateChat: ( chat: ChatMessageType, message: string, - stream_url?: string, + stream_url?: string ) => void; }; @@ -659,12 +661,12 @@ export type codeTabsPropsType = { value: string, node: NodeType, template: TemplateVariableType, - tweak: tweakType, + tweak: tweakType ) => string; buildTweakObject?: ( tw: string, changes: string | string[] | boolean | number | Object[] | Object, - template: TemplateVariableType, + template: TemplateVariableType ) => Promise; }; activeTweaks?: boolean; diff --git a/src/frontend/src/types/flow/index.ts b/src/frontend/src/types/flow/index.ts index d9123bebb..52171e6a8 100644 --- a/src/frontend/src/types/flow/index.ts +++ b/src/frontend/src/types/flow/index.ts @@ -7,6 +7,7 @@ export type FlowType = { id: string; data: ReactFlowJsonObject | null; description: string; + endpoint_name?: string; style?: FlowStyleType; is_component?: boolean; last_tested_version?: string; diff --git a/tests/test_database.py b/tests/test_database.py index 554a1fc4f..83503cd30 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -30,14 +30,14 @@ def json_style(): def test_create_flow(client: TestClient, json_flow: str, active_user, logged_in_headers): flow = orjson.loads(json_flow) data = flow["data"] - flow = FlowCreate(name="Test Flow", description="description", data=data) - response = client.post("api/v1/flows/", json=flow.dict(), headers=logged_in_headers) + flow = FlowCreate(name=str(uuid4()), description="description", data=data) + response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers) assert response.status_code == 201 assert response.json()["name"] == flow.name assert response.json()["data"] == flow.data # flow is optional so we can create a flow without a flow flow = FlowCreate(name="Test Flow") - response = client.post("api/v1/flows/", json=flow.dict(exclude_unset=True), headers=logged_in_headers) + response = client.post("api/v1/flows/", json=flow.model_dump(exclude_unset=True), headers=logged_in_headers) assert response.status_code == 201 assert response.json()["name"] == flow.name assert response.json()["data"] == flow.data @@ -46,14 +46,14 @@ def test_create_flow(client: TestClient, json_flow: str, active_user, logged_in_ def test_read_flows(client: TestClient, json_flow: str, active_user, logged_in_headers): flow_data = orjson.loads(json_flow) data = flow_data["data"] - flow = FlowCreate(name="Test Flow", description="description", data=data) - response = client.post("api/v1/flows/", json=flow.dict(), headers=logged_in_headers) + flow = FlowCreate(name=str(uuid4()), description="description", data=data) + response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers) assert response.status_code == 201 assert response.json()["name"] == flow.name assert response.json()["data"] == flow.data - flow = FlowCreate(name="Test Flow", description="description", data=data) - response = client.post("api/v1/flows/", json=flow.dict(), headers=logged_in_headers) + flow = FlowCreate(name=str(uuid4()), description="description", data=data) + response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers) assert response.status_code == 201 assert response.json()["name"] == flow.name assert response.json()["data"] == flow.data @@ -67,7 +67,7 @@ def test_read_flow(client: TestClient, json_flow: str, active_user, logged_in_he flow = orjson.loads(json_flow) data = flow["data"] flow = FlowCreate(name="Test Flow", description="description", data=data) - response = client.post("api/v1/flows/", json=flow.dict(), headers=logged_in_headers) + response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers) flow_id = response.json()["id"] # flow_id should be a UUID but is a string # turn it into a UUID flow_id = UUID(flow_id) @@ -83,7 +83,7 @@ def test_update_flow(client: TestClient, json_flow: str, active_user, logged_in_ data = flow["data"] flow = FlowCreate(name="Test Flow", description="description", data=data) - response = client.post("api/v1/flows/", json=flow.dict(), headers=logged_in_headers) + response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers) flow_id = response.json()["id"] updated_flow = FlowUpdate( @@ -91,7 +91,7 @@ def test_update_flow(client: TestClient, json_flow: str, active_user, logged_in_ description="updated description", data=data, ) - response = client.patch(f"api/v1/flows/{flow_id}", json=updated_flow.dict(), headers=logged_in_headers) + response = client.patch(f"api/v1/flows/{flow_id}", json=updated_flow.model_dump(), headers=logged_in_headers) assert response.status_code == 200 assert response.json()["name"] == updated_flow.name @@ -103,7 +103,7 @@ def test_delete_flow(client: TestClient, json_flow: str, active_user, logged_in_ flow = orjson.loads(json_flow) data = flow["data"] flow = FlowCreate(name="Test Flow", description="description", data=data) - response = client.post("api/v1/flows/", json=flow.dict(), headers=logged_in_headers) + response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers) flow_id = response.json()["id"] response = client.delete(f"api/v1/flows/{flow_id}", headers=logged_in_headers) assert response.status_code == 200 @@ -223,8 +223,8 @@ def test_update_flow_idempotency(client: TestClient, json_flow: str, active_user response = client.post("api/v1/flows/", json=flow_data.dict(), headers=logged_in_headers) flow_id = response.json()["id"] updated_flow = FlowCreate(name="Updated Flow", description="description", data=data) - response1 = client.put(f"api/v1/flows/{flow_id}", json=updated_flow.dict(), headers=logged_in_headers) - response2 = client.put(f"api/v1/flows/{flow_id}", json=updated_flow.dict(), headers=logged_in_headers) + response1 = client.put(f"api/v1/flows/{flow_id}", json=updated_flow.model_dump(), headers=logged_in_headers) + response2 = client.put(f"api/v1/flows/{flow_id}", json=updated_flow.model_dump(), headers=logged_in_headers) assert response1.json() == response2.json() @@ -237,8 +237,8 @@ def test_update_nonexistent_flow(client: TestClient, json_flow: str, active_user description="description", data=data, ) - response = client.patch(f"api/v1/flows/{uuid}", json=updated_flow.dict(), headers=logged_in_headers) - assert response.status_code == 404 + response = client.patch(f"api/v1/flows/{uuid}", json=updated_flow.model_dump(), headers=logged_in_headers) + assert response.status_code == 404, response.text def test_delete_nonexistent_flow(client: TestClient, active_user, logged_in_headers): diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py index e45b0898e..95109b215 100644 --- a/tests/test_endpoints.py +++ b/tests/test_endpoints.py @@ -652,7 +652,7 @@ def test_invalid_flow_id(client, created_api_key): headers = {"x-api-key": created_api_key.api_key} flow_id = "invalid-flow-id" response = client.post(f"/api/v1/run/{flow_id}", headers=headers) - assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, response.text + assert response.status_code == status.HTTP_404_NOT_FOUND, response.text headers = {"x-api-key": created_api_key.api_key} flow_id = UUID(int=0) response = client.post(f"/api/v1/run/{flow_id}", headers=headers)