diff --git a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml index a9e9be01f4a38..fecc061c91c79 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml +++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml @@ -4474,6 +4474,12 @@ paths: schema: $ref: '#/components/schemas/HTTPExceptionResponse' description: Not Found + '409': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPExceptionResponse' + description: Conflict '422': description: Validation Error content: diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dags.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dags.py index 62c7eec91ac4f..3b1f275a8b5da 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dags.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dags.py @@ -430,7 +430,7 @@ def patch_dags( @dags_router.post( "/{dag_id}/favorite", status_code=status.HTTP_204_NO_CONTENT, - responses=create_openapi_http_exception_doc([status.HTTP_404_NOT_FOUND]), + responses=create_openapi_http_exception_doc([status.HTTP_404_NOT_FOUND, status.HTTP_409_CONFLICT]), dependencies=[Depends(requires_access_dag(method="GET")), Depends(action_logging())], ) def favorite_dag(dag_id: str, session: SessionDep, user: GetUserDep): @@ -440,6 +440,19 @@ def favorite_dag(dag_id: str, session: SessionDep, user: GetUserDep): raise HTTPException(status.HTTP_404_NOT_FOUND, detail=f"Dag with id '{dag_id}' not found") user_id = str(user.get_id()) + + favorite_exists = session.execute( + select(DagFavorite) + .where( + DagFavorite.dag_id == dag_id, + DagFavorite.user_id == user_id, + ) + .limit(1) + ).first() + + if favorite_exists: + raise HTTPException(status.HTTP_409_CONFLICT, detail="Dag is already marked as favorite") + session.execute(insert(DagFavorite).values(dag_id=dag_id, user_id=user_id)) diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts index baf23138c35a8..600c15d40349b 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts @@ -1998,6 +1998,7 @@ export class DagService { 401: 'Unauthorized', 403: 'Forbidden', 404: 'Not Found', + 409: 'Conflict', 422: 'Validation Error' } }); diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts index 91ddaf68f7d26..8329aed1ee4e2 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts @@ -6638,6 +6638,10 @@ export type $OpenApiTs = { * Not Found */ 404: HTTPExceptionResponse; + /** + * Conflict + */ + 409: HTTPExceptionResponse; /** * Validation Error */ diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py index fa2a2a7099255..73d2d08913cb7 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py @@ -1223,6 +1223,19 @@ def test_favoriting_already_favorited_dag_returns_409(self, test_client): response = test_client.post(f"/dags/{DAG1_ID}/favorite") assert response.status_code == 409 + assert response.json()["detail"] == "Dag is already marked as favorite" + + def test_favorite_dag_existence_check_is_bounded(self, test_client): + """The existing-favorite existence probe must ask the DB for one row.""" + with capture_orm_selects("dag_favorite") as statements: + response = test_client.post(f"/dags/{DAG1_ID}/favorite") + + assert response.status_code == 204 + assert statements, "expected the endpoint to query the dag_favorite table" + for sql in statements: + assert re.search(r"\bLIMIT 1\b", sql), ( + f"favorite existence check is not bounded to one row: {sql}" + ) class TestUnfavoriteDag(TestDagEndpoint):