From 7fd5853020f405f39f65f706d9ac17b82f3d1b02 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Pardou?= <jeremie@baserow.io>
Date: Mon, 17 Feb 2025 09:19:23 +0000
Subject: [PATCH] Final fix for service responses

---
 .../builder/api/workflow_actions/views.py     |  4 +-
 .../data_providers/data_provider_types.py     |  7 ++--
 .../contrib/builder/data_sources/handler.py   |  2 +-
 .../contrib/builder/data_sources/service.py   |  2 +-
 .../builder/workflow_actions/handler.py       | 13 ++++---
 .../builder/workflow_actions/service.py       |  6 ++-
 .../contrib/dashboard/data_sources/handler.py |  2 +-
 .../local_baserow/service_types.py            | 32 ++++++++--------
 .../src/baserow/core/formula/registries.py    |  3 +-
 backend/src/baserow/core/services/handler.py  |  4 +-
 .../src/baserow/core/services/registries.py   |  7 ++--
 backend/src/baserow/core/services/types.py    |  8 +++-
 .../test_data_provider_types.py               | 10 ++---
 .../test_delete_row_service_type.py           |  4 +-
 .../test_get_row_service_type.py              | 16 ++++----
 .../test_list_rows_service_type.py            | 38 ++++++++++++-------
 .../test_upsert_row_service_type.py           |  6 +--
 .../local_baserow/service_types.py            |  6 +--
 ...est_grouped_aggregate_rows_service_type.py | 30 +++++++--------
 19 files changed, 111 insertions(+), 89 deletions(-)

diff --git a/backend/src/baserow/contrib/builder/api/workflow_actions/views.py b/backend/src/baserow/contrib/builder/api/workflow_actions/views.py
index b57288113..8d7ee3264 100644
--- a/backend/src/baserow/contrib/builder/api/workflow_actions/views.py
+++ b/backend/src/baserow/contrib/builder/api/workflow_actions/views.py
@@ -404,6 +404,4 @@ class DispatchBuilderWorkflowActionView(APIView):
             request.user, workflow_action, dispatch_context  # type: ignore
         )
 
-        if not isinstance(response, Response):
-            response = Response(response)
-        return response
+        return Response(response.data, status=response.status)
diff --git a/backend/src/baserow/contrib/builder/data_providers/data_provider_types.py b/backend/src/baserow/contrib/builder/data_providers/data_provider_types.py
index 2b00dcc1c..23689d00a 100644
--- a/backend/src/baserow/contrib/builder/data_providers/data_provider_types.py
+++ b/backend/src/baserow/contrib/builder/data_providers/data_provider_types.py
@@ -5,8 +5,6 @@ from django.conf import settings
 from django.core.cache import cache
 from django.utils.translation import gettext as _
 
-from rest_framework.response import Response
-
 from baserow.contrib.builder.data_providers.exceptions import (
     DataProviderChunkInvalidException,
     FormDataProviderChunkInvalidException,
@@ -31,6 +29,7 @@ from baserow.contrib.builder.workflow_actions.handler import (
 from baserow.core.formula.exceptions import FormulaRecursion, InvalidBaserowFormula
 from baserow.core.formula.registries import DataProviderType
 from baserow.core.services.dispatch_context import DispatchContext
+from baserow.core.services.types import DispatchResult
 from baserow.core.user_sources.constants import DEFAULT_USER_ROLE_PREFIX
 from baserow.core.user_sources.user_source_user import UserSourceUser
 from baserow.core.utils import get_value_at_path
@@ -449,7 +448,7 @@ class PreviousActionProviderType(DataProviderType):
         self,
         dispatch_context: DispatchContext,
         workflow_action: WorkflowAction,
-        result: Any,
+        dispatch_result: DispatchResult,
     ) -> None:
         """
         If the current_dispatch_id exists in the request data, create a unique
@@ -470,7 +469,7 @@ class PreviousActionProviderType(DataProviderType):
             )
             cache.set(
                 cache_key,
-                {} if isinstance(result, Response) else result,
+                dispatch_result.data,
                 timeout=settings.BUILDER_DISPATCH_ACTION_CACHE_TTL_SECONDS,
             )
 
diff --git a/backend/src/baserow/contrib/builder/data_sources/handler.py b/backend/src/baserow/contrib/builder/data_sources/handler.py
index 83957995a..1f8154ad1 100644
--- a/backend/src/baserow/contrib/builder/data_sources/handler.py
+++ b/backend/src/baserow/contrib/builder/data_sources/handler.py
@@ -448,7 +448,7 @@ class DataSourceHandler:
             # it later
             dispatch_context.cache["data_source_contents"][
                 data_source.id
-            ] = service_dispatch
+            ] = service_dispatch.data
 
         return dispatch_context.cache["data_source_contents"][data_source.id]
 
diff --git a/backend/src/baserow/contrib/builder/data_sources/service.py b/backend/src/baserow/contrib/builder/data_sources/service.py
index fb476393b..366390e25 100644
--- a/backend/src/baserow/contrib/builder/data_sources/service.py
+++ b/backend/src/baserow/contrib/builder/data_sources/service.py
@@ -384,7 +384,7 @@ class DataSourceService:
         Dispatch the service related to the data_source if the user has the permission.
 
         :param user: The current user.
-        :param data_sources: The data source to be dispatched.
+        :param data_source: The data source to be dispatched.
         :param dispatch_context: The context used for the dispatch.
         :return: return the dispatch result.
         """
diff --git a/backend/src/baserow/contrib/builder/workflow_actions/handler.py b/backend/src/baserow/contrib/builder/workflow_actions/handler.py
index 952e3f612..635642f4d 100644
--- a/backend/src/baserow/contrib/builder/workflow_actions/handler.py
+++ b/backend/src/baserow/contrib/builder/workflow_actions/handler.py
@@ -1,4 +1,4 @@
-from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional
+from typing import TYPE_CHECKING, Dict, Iterable, List, Optional
 from zipfile import ZipFile
 
 from django.core.files.storage import Storage
@@ -25,6 +25,7 @@ from baserow.contrib.builder.workflow_actions.registries import (
 )
 from baserow.core.exceptions import IdDoesNotExist
 from baserow.core.services.handler import ServiceHandler
+from baserow.core.services.types import DispatchResult
 from baserow.core.workflow_actions.handler import WorkflowActionHandler
 from baserow.core.workflow_actions.models import WorkflowAction
 from baserow.core.workflow_actions.registries import WorkflowActionType
@@ -174,7 +175,7 @@ class BuilderWorkflowActionHandler(WorkflowActionHandler):
         self,
         workflow_action: BuilderWorkflowServiceAction,
         dispatch_context: BuilderDispatchContext,
-    ) -> Any:
+    ) -> DispatchResult:
         """
         Dispatch the service related to the workflow_action.
 
@@ -185,11 +186,13 @@ class BuilderWorkflowActionHandler(WorkflowActionHandler):
         :return: The result of dispatching the workflow action.
         """
 
-        result = ServiceHandler().dispatch_service(
+        dispatch_result = ServiceHandler().dispatch_service(
             workflow_action.service.specific, dispatch_context
         )
 
         for data_provider in builder_data_provider_type_registry.get_all():
-            data_provider.post_dispatch(dispatch_context, workflow_action, result)
+            data_provider.post_dispatch(
+                dispatch_context, workflow_action, dispatch_result
+            )
 
-        return result
+        return dispatch_result
diff --git a/backend/src/baserow/contrib/builder/workflow_actions/service.py b/backend/src/baserow/contrib/builder/workflow_actions/service.py
index 9ba890535..dc02ca5f8 100644
--- a/backend/src/baserow/contrib/builder/workflow_actions/service.py
+++ b/backend/src/baserow/contrib/builder/workflow_actions/service.py
@@ -40,6 +40,7 @@ from baserow.contrib.builder.workflow_actions.workflow_action_types import (
     BuilderWorkflowActionType,
 )
 from baserow.core.handler import CoreHandler
+from baserow.core.services.types import DispatchResult
 
 if TYPE_CHECKING:
     from baserow.contrib.builder.models import Builder
@@ -341,4 +342,7 @@ class BuilderWorkflowActionService:
             "external", {}
         ).get(workflow_action.service.id, [])
 
-        return self.remove_unused_field_names(result, field_names)
+        return DispatchResult(
+            data=self.remove_unused_field_names(result.data, field_names),
+            status=result.status,
+        )
diff --git a/backend/src/baserow/contrib/dashboard/data_sources/handler.py b/backend/src/baserow/contrib/dashboard/data_sources/handler.py
index 4735fd831..02f20f1a5 100644
--- a/backend/src/baserow/contrib/dashboard/data_sources/handler.py
+++ b/backend/src/baserow/contrib/dashboard/data_sources/handler.py
@@ -307,7 +307,7 @@ class DashboardDataSourceHandler:
             data_source.service.specific, dispatch_context
         )
 
-        return service_dispatch
+        return service_dispatch.data
 
     def export_data_source(
         self,
diff --git a/backend/src/baserow/contrib/integrations/local_baserow/service_types.py b/backend/src/baserow/contrib/integrations/local_baserow/service_types.py
index 8cb979518..55efbd031 100644
--- a/backend/src/baserow/contrib/integrations/local_baserow/service_types.py
+++ b/backend/src/baserow/contrib/integrations/local_baserow/service_types.py
@@ -17,7 +17,6 @@ from django.db.models import QuerySet
 
 from rest_framework import serializers
 from rest_framework.exceptions import ValidationError as DRFValidationError
-from rest_framework.response import Response
 
 from baserow.contrib.builder.data_providers.exceptions import (
     DataProviderChunkInvalidException,
@@ -103,6 +102,7 @@ from baserow.core.services.registries import (
     ServiceType,
 )
 from baserow.core.services.types import (
+    DispatchResult,
     ServiceDict,
     ServiceFilterDictSubClass,
     ServiceSortDictSubClass,
@@ -1099,7 +1099,7 @@ class LocalBaserowListRowsUserServiceType(
             "public_allowed_properties": only_field_names,
         }
 
-    def dispatch_transform(self, dispatch_data: Dict[str, Any]) -> Any:
+    def dispatch_transform(self, dispatch_data: Dict[str, Any]) -> DispatchResult:
         """
         Given the rows found in `dispatch_data`, serializes them.
 
@@ -1120,10 +1120,12 @@ class LocalBaserowListRowsUserServiceType(
             field_ids=field_ids,
         )
 
-        return {
-            "results": serializer(dispatch_data["results"], many=True).data,
-            "has_next_page": dispatch_data["has_next_page"],
-        }
+        return DispatchResult(
+            data={
+                "results": serializer(dispatch_data["results"], many=True).data,
+                "has_next_page": dispatch_data["has_next_page"],
+            }
+        )
 
     def get_record_names(
         self,
@@ -1508,7 +1510,7 @@ class LocalBaserowAggregateRowsUserServiceType(
     def dispatch_transform(
         self,
         data: Dict[str, Any],
-    ) -> Dict[str, Any]:
+    ) -> DispatchResult:
         """
         Responsible for transforming the data returned by the `dispatch_data`
         method into a format that can be used by the frontend.
@@ -1517,7 +1519,7 @@ class LocalBaserowAggregateRowsUserServiceType(
         :return: A dictionary containing the aggregation result.
         """
 
-        return data["data"]
+        return DispatchResult(data=data["data"])
 
     def extract_properties(self, path: List[str], **kwargs) -> List[str]:
         """
@@ -1714,7 +1716,7 @@ class LocalBaserowGetRowUserServiceType(
             **kwargs,
         )
 
-    def dispatch_transform(self, dispatch_data: Dict[str, Any]) -> Any:
+    def dispatch_transform(self, dispatch_data: Dict[str, Any]) -> DispatchResult:
         """
         Responsible for serializing the `dispatch_data` row.
 
@@ -1737,7 +1739,7 @@ class LocalBaserowGetRowUserServiceType(
 
         serialized_row = serializer(dispatch_data["data"]).data
 
-        return serialized_row
+        return DispatchResult(data=serialized_row)
 
     def resolve_service_formulas(
         self,
@@ -2069,7 +2071,7 @@ class LocalBaserowUpsertRowServiceType(
     def enhance_queryset(self, queryset):
         return super().enhance_queryset(queryset).prefetch_related("field_mappings")
 
-    def dispatch_transform(self, dispatch_data: Dict[str, Any]) -> Any:
+    def dispatch_transform(self, dispatch_data: Dict[str, Any]) -> DispatchResult:
         """
         Responsible for serializing the `dispatch_data` row.
 
@@ -2091,7 +2093,7 @@ class LocalBaserowUpsertRowServiceType(
         )
         serialized_row = serializer(dispatch_data["data"]).data
 
-        return serialized_row
+        return DispatchResult(data=serialized_row)
 
     def resolve_service_formulas(
         self,
@@ -2331,17 +2333,17 @@ class LocalBaserowDeleteRowServiceType(
         resolved_values = super().resolve_service_formulas(service, dispatch_context)
         return self.resolve_row_id(resolved_values, service, dispatch_context)
 
-    def dispatch_transform(self, dispatch_data: Dict[str, Any]) -> Response:
+    def dispatch_transform(self, dispatch_data: Dict[str, Any]) -> DispatchResult:
         """
         The delete row action's `dispatch_data` will contain an empty
         `data` dictionary. When we get to this method and wish to transform
         the data, we can simply return a 204 response.
 
         :param dispatch_data: The `dispatch_data` result.
-        :return: A 204 response.
+        :return: A dispatch result with no data, and a 204 status code.
         """
 
-        return Response(status=204)
+        return DispatchResult(status=204)
 
     def dispatch_data(
         self,
diff --git a/backend/src/baserow/core/formula/registries.py b/backend/src/baserow/core/formula/registries.py
index a42471fbe..40ff72db6 100644
--- a/backend/src/baserow/core/formula/registries.py
+++ b/backend/src/baserow/core/formula/registries.py
@@ -15,6 +15,7 @@ from baserow.core.formula.types import (
 )
 from baserow.core.registry import Instance, Registry
 from baserow.core.services.dispatch_context import DispatchContext
+from baserow.core.services.types import DispatchResult
 from baserow.core.workflow_actions.models import WorkflowAction
 
 
@@ -183,7 +184,7 @@ class DataProviderType(
         self,
         dispatch_context: DispatchContext,
         workflow_action: WorkflowAction,
-        result: Any,
+        dispatch_result: DispatchResult,
     ) -> None:
         """
         This hook is called after a Workflow Action has been dispatched. It is
diff --git a/backend/src/baserow/core/services/handler.py b/backend/src/baserow/core/services/handler.py
index 74f8845ec..8713a4a76 100644
--- a/backend/src/baserow/core/services/handler.py
+++ b/backend/src/baserow/core/services/handler.py
@@ -18,7 +18,7 @@ from baserow.core.storage import ExportZipFile
 from baserow.core.utils import extract_allowed
 
 from .dispatch_context import DispatchContext
-from .types import ServiceForUpdate, UpdatedService
+from .types import DispatchResult, ServiceForUpdate, UpdatedService
 
 
 class ServiceHandler:
@@ -202,7 +202,7 @@ class ServiceHandler:
         self,
         service: Service,
         dispatch_context: DispatchContext,
-    ) -> Any:
+    ) -> DispatchResult:
         """
         Dispatch the given service.
 
diff --git a/backend/src/baserow/core/services/registries.py b/backend/src/baserow/core/services/registries.py
index ce2223fcf..1dd48d867 100644
--- a/backend/src/baserow/core/services/registries.py
+++ b/backend/src/baserow/core/services/registries.py
@@ -19,6 +19,7 @@ from baserow.core.registry import (
     Registry,
 )
 from baserow.core.services.dispatch_context import DispatchContext
+from baserow.core.services.types import DispatchResult
 
 from .exceptions import ServiceTypeDoesNotExist
 from .models import Service
@@ -195,13 +196,13 @@ class ServiceType(
     def dispatch_transform(
         self,
         data: Any,
-    ) -> Any:
+    ) -> DispatchResult:
         """
         Responsible for taking the `dispatch_data` result and transforming its value
         for API consumer's consumption.
 
         :param data: The `dispatch_data` result.
-        :return: The transformed `dispatch_transform` result if any.
+        :return: The transformed `dispatch_transform` result.
         """
 
     def dispatch_data(
@@ -224,7 +225,7 @@ class ServiceType(
         self,
         service: ServiceSubClass,
         dispatch_context: DispatchContext,
-    ) -> Any:
+    ) -> DispatchResult:
         """
         Responsible for calling `dispatch_data` and `dispatch_transform` to execute
         the service's task, and generating the dispatch's response, respectively.
diff --git a/backend/src/baserow/core/services/types.py b/backend/src/baserow/core/services/types.py
index 789132528..5208afbdd 100644
--- a/backend/src/baserow/core/services/types.py
+++ b/backend/src/baserow/core/services/types.py
@@ -1,4 +1,4 @@
-from dataclasses import dataclass
+from dataclasses import dataclass, field
 from typing import NewType, Optional, TypedDict, TypeVar
 
 from baserow.core.formula.runtime_formula_context import RuntimeFormulaContext
@@ -25,6 +25,12 @@ class ServiceSortDict(TypedDict):
     order: str
 
 
+@dataclass
+class DispatchResult:
+    data: dict = field(default_factory=dict)
+    status: int = 200
+
+
 @dataclass
 class UpdatedService:
     service: Service
diff --git a/backend/tests/baserow/contrib/builder/data_providers/test_data_provider_types.py b/backend/tests/baserow/contrib/builder/data_providers/test_data_provider_types.py
index 63f469848..c09163296 100644
--- a/backend/tests/baserow/contrib/builder/data_providers/test_data_provider_types.py
+++ b/backend/tests/baserow/contrib/builder/data_providers/test_data_provider_types.py
@@ -7,7 +7,6 @@ from django.http import HttpRequest
 from django.shortcuts import reverse
 
 import pytest
-from rest_framework.response import Response
 
 from baserow.contrib.builder.data_providers.data_provider_types import (
     CurrentRecordDataProviderType,
@@ -39,6 +38,7 @@ from baserow.contrib.database.fields.handler import FieldHandler
 from baserow.core.formula.exceptions import InvalidBaserowFormula
 from baserow.core.formula.registries import DataProviderType
 from baserow.core.services.exceptions import ServiceImproperlyConfigured
+from baserow.core.services.types import DispatchResult
 from baserow.core.user_sources.constants import DEFAULT_USER_ROLE_PREFIX
 from baserow.core.user_sources.user_source_user import UserSourceUser
 from baserow.core.utils import MirrorDict
@@ -984,7 +984,7 @@ def test_previous_action_data_provider_post_dispatch_caches_result():
     workflow_action.id = 100
 
     mock_cache_key = "mock-cache-key"
-    mock_result = {"mock-key": "mock-value"}
+    mock_result = DispatchResult(data={"mock-key": "mock-value"})
     previous_action_data_provider.get_dispatch_action_cache_key = MagicMock(
         return_value=mock_cache_key
     )
@@ -1001,12 +1001,12 @@ def test_previous_action_data_provider_post_dispatch_caches_result():
     )
     mock_cache.set.assert_called_once_with(
         mock_cache_key,
-        mock_result,
+        mock_result.data,
         timeout=settings.BUILDER_DISPATCH_ACTION_CACHE_TTL_SECONDS,
     )
 
 
-def test_previous_action_data_provider_post_dispatch_with_response_doesnt_cache_result():
+def test_previous_action_data_provider_post_dispatch_with_empty_response_cache_result():
     """
     Ensure that when a current_dispatch_id is present in the request, the
     provided result is cached.
@@ -1026,7 +1026,7 @@ def test_previous_action_data_provider_post_dispatch_with_response_doesnt_cache_
     workflow_action.id = 100
 
     mock_cache_key = "mock-cache-key"
-    mock_result = Response(status=204)
+    mock_result = DispatchResult(status=204)
     previous_action_data_provider.get_dispatch_action_cache_key = MagicMock(
         return_value=mock_cache_key
     )
diff --git a/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_delete_row_service_type.py b/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_delete_row_service_type.py
index c723c3f96..d7ee99e21 100644
--- a/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_delete_row_service_type.py
+++ b/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_delete_row_service_type.py
@@ -1,7 +1,6 @@
 from unittest.mock import Mock
 
 import pytest
-from rest_framework.response import Response
 
 from baserow.contrib.database.rows.handler import RowHandler
 from baserow.contrib.integrations.local_baserow.models import LocalBaserowDeleteRow
@@ -158,5 +157,4 @@ def test_local_baserow_delete_row_service_dispatch_transform(data_fixture):
     service_type = LocalBaserowDeleteRowServiceType()
     dispatch_data = {"data": {}, "baserow_table_model": Mock()}
     result = service_type.dispatch_transform(dispatch_data)
-    assert isinstance(result, Response)
-    assert result.status_code == 204
+    assert result.status == 204
diff --git a/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_get_row_service_type.py b/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_get_row_service_type.py
index 6e8ea0318..66ebb9e6e 100644
--- a/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_get_row_service_type.py
+++ b/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_get_row_service_type.py
@@ -212,7 +212,7 @@ def test_local_baserow_get_row_service_dispatch_transform(data_fixture):
     )
     result = service_type.dispatch_transform(dispatch_data)
 
-    assert result == {
+    assert result.data == {
         "id": rows[1].id,
         fields[0].db_column: "Audi",
         fields[1].db_column: "Orange",
@@ -325,7 +325,7 @@ def test_local_baserow_get_row_service_dispatch_data_with_service_integer_search
     )
     result = service_type.dispatch_transform(dispatch_data)
 
-    assert result == {
+    assert result.data == {
         "id": rows[2].id,
         fields[0].db_column: "42",
         "order": AnyStr(),
@@ -771,7 +771,7 @@ def test_dispatch_transform_passes_field_ids(mock_get_serializer, field_names):
 
     results = service_type.dispatch_transform(dispatch_data)
 
-    assert results == mock_serializer_instance.data
+    assert results.data == mock_serializer_instance.data
     mock_get_serializer.assert_called_once_with(
         dispatch_data["baserow_table_model"],
         RowSerializer,
@@ -851,7 +851,7 @@ def test_can_dispatch_interesting_table(data_fixture):
     # Normal dispatch
     result = service.get_type().dispatch(service, dispatch_context)
 
-    assert len(result.keys()) == table.field_set.count() + 2
+    assert len(result.data.keys()) == table.field_set.count() + 2
 
     # Now can we dispatch the table if all fields are hidden?
     field_names = {
@@ -866,7 +866,7 @@ def test_can_dispatch_interesting_table(data_fixture):
     # means that the enhance_by_field is filtered to only used field.
     result = service.get_type().dispatch(service, dispatch_context)
 
-    assert len(result.keys()) == 1 + 1  # We also have the order at that point
+    assert len(result.data.keys()) == 1 + 1  # We also have the order at that point
 
     # Test with a filter on a single select field. Single select have a select_related
     single_select_field = table.field_set.get(name="single_select")
@@ -879,7 +879,7 @@ def test_can_dispatch_interesting_table(data_fixture):
 
     dispatch_context = FakeDispatchContext(public_allowed_properties=field_names)
 
-    assert len(result.keys()) == 1 + 1
+    assert len(result.data.keys()) == 1 + 1
 
     # Let's remove the filter to not interfer with the sort
     service_filter.delete()
@@ -890,7 +890,7 @@ def test_can_dispatch_interesting_table(data_fixture):
     )
 
     dispatch_context = FakeDispatchContext(public_allowed_properties=field_names)
-    assert len(result.keys()) == 1 + 1
+    assert len(result.data.keys()) == 1 + 1
 
     service_sort.delete()
 
@@ -899,4 +899,4 @@ def test_can_dispatch_interesting_table(data_fixture):
     service.save()
 
     dispatch_context = FakeDispatchContext(public_allowed_properties=field_names)
-    assert len(result.keys()) == 1 + 1
+    assert len(result.data.keys()) == 1 + 1
diff --git a/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_list_rows_service_type.py b/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_list_rows_service_type.py
index 1cc042ccf..94e1423c9 100644
--- a/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_list_rows_service_type.py
+++ b/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_list_rows_service_type.py
@@ -231,7 +231,7 @@ def test_local_baserow_list_rows_service_dispatch_transform(data_fixture):
     )
     result = service_type.dispatch_transform(dispatch_data)
 
-    assert [dict(r) for r in result["results"]] == [
+    assert [dict(r) for r in result.data["results"]] == [
         {
             "id": rows[0].id,
             fields[0].db_column: "BMW",
@@ -245,7 +245,7 @@ def test_local_baserow_list_rows_service_dispatch_transform(data_fixture):
             "order": AnyStr(),
         },
     ]
-    assert result["has_next_page"] is False
+    assert result.data["has_next_page"] is False
 
 
 @pytest.mark.django_db
@@ -943,7 +943,7 @@ def test_can_dispatch_table_with_deleted_field(data_fixture):
     result = service.get_type().dispatch(service, dispatch_context)
 
     assert (
-        len(result["results"][0].keys()) == 2 + 1
+        len(result.data["results"][0].keys()) == 2 + 1
     )  # We also have the order at that point
 
 
@@ -975,8 +975,8 @@ def test_can_dispatch_interesting_table(data_fixture):
     # Normal dispatch
     result = service.get_type().dispatch(service, dispatch_context)
 
-    assert len(result["results"]) == 2
-    assert len(result["results"][0].keys()) == table.field_set.count() + 2
+    assert len(result.data["results"]) == 2
+    assert len(result.data["results"][0].keys()) == table.field_set.count() + 2
 
     # Now can we dispatch the table if all fields are hidden?
     field_names = {
@@ -992,7 +992,7 @@ def test_can_dispatch_interesting_table(data_fixture):
     result = service.get_type().dispatch(service, dispatch_context)
 
     assert (
-        len(result["results"][0].keys()) == 1 + 1
+        len(result.data["results"][0].keys()) == 1 + 1
     )  # We also have the order at that point
 
     # Test with a filter on a single select field. Single select have a select_related
@@ -1000,15 +1000,19 @@ def test_can_dispatch_interesting_table(data_fixture):
     service_filter = data_fixture.create_local_baserow_table_service_filter(
         service=service,
         field=single_select_field,
-        value="'A'",
+        type="not_equal",
+        value="'Nothing'",
+        value_is_formula=True,
         order=0,
     )
 
     dispatch_context = FakeDispatchContext(public_allowed_properties=field_names)
 
-    assert len(result["results"][0].keys()) == 1 + 1
+    result = service.get_type().dispatch(service, dispatch_context)
 
-    # Let's remove the filter to not interfer with the sort
+    assert len(result.data["results"][0].keys()) == 2 + 1
+
+    # Let's remove the filter to not interfere with the sort
     service_filter.delete()
 
     # Test with a sort
@@ -1017,16 +1021,22 @@ def test_can_dispatch_interesting_table(data_fixture):
     )
 
     dispatch_context = FakeDispatchContext(public_allowed_properties=field_names)
-    assert len(result["results"][0].keys()) == 1 + 1
+
+    result = service.get_type().dispatch(service, dispatch_context)
+
+    assert len(result.data["results"][0].keys()) == 2 + 1
 
     service_sort.delete()
 
-    # Now with a search
-    service.search_query = "'A'"
+    # Now with a search query
+    service.search_query = "1"
     service.save()
 
     dispatch_context = FakeDispatchContext(public_allowed_properties=field_names)
-    assert len(result["results"][0].keys()) == 1 + 1
+
+    result = service.get_type().dispatch(service, dispatch_context)
+
+    assert len(result.data["results"][0].keys()) == 1 + 1
 
 
 @pytest.mark.parametrize(
@@ -1063,7 +1073,7 @@ def test_dispatch_transform_passes_field_ids(mock_get_serializer, field_names):
 
     results = service_type.dispatch_transform(dispatch_data)
 
-    assert results == {
+    assert results.data == {
         "has_next_page": False,
         "results": mock_serializer_instance.data,
     }
diff --git a/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_upsert_row_service_type.py b/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_upsert_row_service_type.py
index 29a9247de..c1d8ce9d1 100644
--- a/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_upsert_row_service_type.py
+++ b/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_upsert_row_service_type.py
@@ -361,7 +361,7 @@ def test_local_baserow_upsert_row_service_dispatch_transform(
     )
 
     serialized_row = service_type.dispatch_transform(dispatch_data)
-    assert dict(serialized_row) == {
+    assert dict(serialized_row.data) == {
         "id": dispatch_data["data"].id,
         "order": "1.00000000000000000000",
         ingredient.db_column: str(2),
@@ -473,7 +473,7 @@ def test_local_baserow_upsert_row_service_dispatch_data_convert_value(data_fixtu
     )
     serialized_row = service_type.dispatch_transform(dispatch_data)
 
-    assert dict(serialized_row) == {
+    assert dict(serialized_row.data) == {
         "id": 1,
         "order": "1.00000000000000000000",
         # The string 'true' was converted to a boolean value
@@ -722,7 +722,7 @@ def test_dispatch_transform_passes_field_ids(
 
     results = service_type.dispatch_transform(dispatch_data)
 
-    assert results == mock_serializer_instance.data
+    assert results.data == mock_serializer_instance.data
     mock_get_serializer.assert_called_once_with(
         dispatch_data["baserow_table_model"],
         RowSerializer,
diff --git a/enterprise/backend/src/baserow_enterprise/integrations/local_baserow/service_types.py b/enterprise/backend/src/baserow_enterprise/integrations/local_baserow/service_types.py
index 2b9583afc..654197005 100644
--- a/enterprise/backend/src/baserow_enterprise/integrations/local_baserow/service_types.py
+++ b/enterprise/backend/src/baserow_enterprise/integrations/local_baserow/service_types.py
@@ -25,7 +25,7 @@ from baserow.contrib.integrations.local_baserow.service_types import (
 from baserow.core.services.dispatch_context import DispatchContext
 from baserow.core.services.exceptions import ServiceImproperlyConfigured
 from baserow.core.services.registries import DispatchTypes
-from baserow.core.services.types import ServiceSortDictSubClass
+from baserow.core.services.types import DispatchResult, ServiceSortDictSubClass
 from baserow.core.utils import atomic_if_not_already
 from baserow_enterprise.api.integrations.local_baserow.serializers import (
     LocalBaserowTableServiceAggregationGroupBySerializer,
@@ -512,5 +512,5 @@ class LocalBaserowGroupedAggregateRowsUserServiceType(
     def dispatch_transform(
         self,
         data: any,
-    ) -> any:
-        return data["data"]
+    ) -> DispatchResult:
+        return DispatchResult(data=data["data"])
diff --git a/enterprise/backend/tests/baserow_enterprise_tests/integrations/local_baserow/service_types/test_grouped_aggregate_rows_service_type.py b/enterprise/backend/tests/baserow_enterprise_tests/integrations/local_baserow/service_types/test_grouped_aggregate_rows_service_type.py
index 161150579..c493737ab 100644
--- a/enterprise/backend/tests/baserow_enterprise_tests/integrations/local_baserow/service_types/test_grouped_aggregate_rows_service_type.py
+++ b/enterprise/backend/tests/baserow_enterprise_tests/integrations/local_baserow/service_types/test_grouped_aggregate_rows_service_type.py
@@ -953,7 +953,7 @@ def test_grouped_aggregate_rows_service_dispatch(data_fixture):
 
     result = ServiceHandler().dispatch_service(service, dispatch_context)
 
-    assert result == {
+    assert result.data == {
         "result": {
             f"field_{field.id}": Decimal("20"),
             f"field_{field_2.id}": Decimal("8"),
@@ -1003,7 +1003,7 @@ def test_grouped_aggregate_rows_service_dispatch_with_view(data_fixture):
 
     result = ServiceHandler().dispatch_service(service, dispatch_context)
 
-    assert result == {
+    assert result.data == {
         "result": {
             f"field_{field.id}": Decimal("6"),
             f"field_{field_2.id}": Decimal("4"),
@@ -1053,7 +1053,7 @@ def test_grouped_aggregate_rows_service_dispatch_with_service_filters(data_fixtu
 
     result = ServiceHandler().dispatch_service(service, dispatch_context)
 
-    assert result == {
+    assert result.data == {
         "result": {
             f"field_{field.id}": Decimal("6"),
             f"field_{field_2.id}": Decimal("4"),
@@ -1278,7 +1278,7 @@ def test_grouped_aggregate_rows_service_dispatch_with_total_aggregation(data_fix
 
     result = ServiceHandler().dispatch_service(service, dispatch_context)
 
-    assert result == {
+    assert result.data == {
         "result": {
             f"field_{field.id}": 75.0,
             f"field_{field_2.id}": 25.0,
@@ -1358,7 +1358,7 @@ def test_grouped_aggregate_rows_service_dispatch_group_by(data_fixture):
 
     result = ServiceHandler().dispatch_service(service, dispatch_context)
 
-    assert result == {
+    assert result.data == {
         "result": [
             {
                 f"field_{field.id}": Decimal("1"),
@@ -1424,7 +1424,7 @@ def test_grouped_aggregate_rows_service_dispatch_group_by_id(data_fixture):
 
     result = ServiceHandler().dispatch_service(service, dispatch_context)
 
-    assert result == {
+    assert result.data == {
         "result": [
             {
                 f"field_{field.id}": Decimal("2"),
@@ -1554,7 +1554,7 @@ def test_grouped_aggregate_rows_service_dispatch_sort_by_series_with_group_by(
 
     result = ServiceHandler().dispatch_service(service, dispatch_context)
 
-    assert result == {
+    assert result.data == {
         "result": [
             {
                 f"field_{field.id}": Decimal("90"),
@@ -1655,7 +1655,7 @@ def test_grouped_aggregate_rows_service_dispatch_sort_by_series_with_group_by_ro
 
     result = ServiceHandler().dispatch_service(service, dispatch_context)
 
-    assert result == {
+    assert result.data == {
         "result": [
             {
                 f"field_{field.id}": None,
@@ -1764,7 +1764,7 @@ def test_grouped_aggregate_rows_service_dispatch_sort_by_series_without_group_by
     result = ServiceHandler().dispatch_service(service, dispatch_context)
 
     # the results are still a dictionary, not sorted on the backend
-    assert result == {
+    assert result.data == {
         "result": {
             f"field_{field.id}": Decimal("9"),
             f"field_{field_2.id}": Decimal("14"),
@@ -1869,7 +1869,7 @@ def test_grouped_aggregate_rows_service_dispatch_sort_by_group_by_field(data_fix
 
     result = ServiceHandler().dispatch_service(service, dispatch_context)
 
-    assert result == {
+    assert result.data == {
         "result": [
             {
                 f"field_{field.id}": None,
@@ -1962,7 +1962,7 @@ def test_grouped_aggregate_rows_service_dispatch_sort_by_group_by_row_id(data_fi
 
     result = ServiceHandler().dispatch_service(service, dispatch_context)
 
-    assert result == {
+    assert result.data == {
         "result": [
             {
                 f"field_{field.id}": "",
@@ -2207,7 +2207,7 @@ def test_grouped_aggregate_rows_service_dispatch_sort_by_series_with_group_by_ig
 
     result = ServiceHandler().dispatch_service(service, dispatch_context)
 
-    assert result == {
+    assert result.data == {
         "result": [
             {
                 f"field_{field.id}": None,
@@ -2298,7 +2298,7 @@ def test_grouped_aggregate_rows_service_dispatch_max_buckets_sort_on_group_by_fi
 
     result = ServiceHandler().dispatch_service(service, dispatch_context)
 
-    assert result == {
+    assert result.data == {
         "result": [
             {
                 f"field_{field.id}": Decimal("10"),
@@ -2385,7 +2385,7 @@ def test_grouped_aggregate_rows_service_dispatch_max_buckets_sort_on_series(
 
     result = ServiceHandler().dispatch_service(service, dispatch_context)
 
-    assert result == {
+    assert result.data == {
         "result": [
             {
                 f"field_{field.id}": Decimal("10"),
@@ -2472,7 +2472,7 @@ def test_grouped_aggregate_rows_service_dispatch_max_buckets_sort_on_primary_fie
 
     result = ServiceHandler().dispatch_service(service, dispatch_context)
 
-    assert result == {
+    assert result.data == {
         "result": [
             {
                 f"field_{field.id}": Decimal("10"),